Skip to content

Commit a5cb344

Browse files
committed
feat: modal text input
1 parent 6faa780 commit a5cb344

File tree

10 files changed

+129
-13
lines changed

10 files changed

+129
-13
lines changed

spotify_player/src/config/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ pub struct AppConfig {
123123
pub seek_duration_secs: u16,
124124

125125
pub sort_artist_albums_by_type: bool,
126+
127+
pub modal_search: bool,
126128
}
127129

128130
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -357,6 +359,8 @@ impl Default for AppConfig {
357359
seek_duration_secs: 5,
358360

359361
sort_artist_albums_by_type: false,
362+
363+
modal_search: false,
360364
}
361365
}
362366
}

spotify_player/src/event/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use crate::{
99
state::{
1010
ActionListItem, Album, AlbumId, Artist, ArtistFocusState, ArtistId, ArtistPopupAction,
1111
BrowsePageUIState, Context, ContextId, ContextPageType, ContextPageUIState, DataReadGuard,
12-
Focusable, Id, Item, ItemId, LibraryFocusState, LibraryPageUIState, PageState, PageType,
13-
PlayableId, Playback, PlaylistCreateCurrentField, PlaylistFolderItem, PlaylistId,
12+
Focusable, Id, InputMode, Item, ItemId, LibraryFocusState, LibraryPageUIState, PageState,
13+
PageType, PlayableId, Playback, PlaylistCreateCurrentField, PlaylistFolderItem, PlaylistId,
1414
PlaylistPopupAction, PopupState, SearchFocusState, SearchPageUIState, SharedState, ShowId,
1515
Track, TrackId, TrackOrder, UIStateGuard, USER_LIKED_TRACKS_ID,
1616
USER_RECENTLY_PLAYED_TRACKS_ID, USER_TOP_TRACKS_ID,
@@ -705,6 +705,11 @@ fn handle_global_command(
705705
line_input: LineInput::default(),
706706
current_query: String::new(),
707707
state: SearchPageUIState::new(),
708+
mode: if config::get_config().app_config.modal_search {
709+
Some(InputMode::default())
710+
} else {
711+
None
712+
},
708713
});
709714
}
710715
Command::BrowsePage => {
@@ -855,6 +860,18 @@ fn handle_global_command(
855860
}
856861
}
857862
Command::ClosePopup => {
863+
if let Some(PopupState::Search { ref mut mode, .. }) = ui.popup {
864+
if let Some(InputMode::Insert) = mode {
865+
*mode = Some(InputMode::Normal);
866+
return Ok(true);
867+
}
868+
} else if config::get_config().app_config.modal_search
869+
&& ui.popup.is_none()
870+
&& ui.history.len() > 1
871+
{
872+
ui.history.pop();
873+
}
874+
858875
ui.popup = None;
859876
}
860877
_ => return Ok(false),

spotify_player/src/event/page.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,23 +198,40 @@ fn handle_key_sequence_for_search_page(
198198
state: &SharedState,
199199
ui: &mut UIStateGuard,
200200
) -> Result<bool> {
201-
let (focus_state, current_query, line_input) = match ui.current_page_mut() {
201+
let (focus_state, current_query, line_input, mode) = match ui.current_page_mut() {
202202
PageState::Search {
203203
state,
204204
line_input,
205205
current_query,
206-
} => (state.focus, current_query, line_input),
206+
mode,
207+
} => (&mut state.focus, current_query, line_input, mode),
207208
_ => anyhow::bail!("expect a search page"),
208209
};
209210

210211
// handle user's input
211212
if let SearchFocusState::Input = focus_state {
212213
if key_sequence.keys.len() == 1 {
213214
return match &key_sequence.keys[0] {
215+
Key::None(crossterm::event::KeyCode::Esc) => {
216+
if *mode == Some(InputMode::Insert) && !line_input.is_empty() {
217+
*current_query = line_input.get_text();
218+
client_pub.send(ClientRequest::Search(line_input.get_text()))?;
219+
*mode = Some(InputMode::Normal);
220+
ui.current_page_mut().next();
221+
} else if ui.history.len() > 1 {
222+
ui.history.pop();
223+
}
224+
return Ok(true);
225+
}
214226
Key::None(crossterm::event::KeyCode::Enter) => {
215227
if !line_input.is_empty() {
216228
*current_query = line_input.get_text();
217229
client_pub.send(ClientRequest::Search(line_input.get_text()))?;
230+
231+
if let Some(InputMode::Insert) = mode {
232+
*mode = Some(InputMode::Normal);
233+
ui.current_page_mut().next();
234+
}
218235
}
219236
Ok(true)
220237
}

spotify_player/src/event/popup.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,49 @@ fn handle_key_sequence_for_search_popup(
354354
state: &SharedState,
355355
ui: &mut UIStateGuard,
356356
) -> Result<bool> {
357+
let conf = config::get_config();
358+
357359
// handle user's input that updates the search query
358-
let Some(PopupState::Search { ref mut query }) = &mut ui.popup else {
360+
let Some(PopupState::Search {
361+
query,
362+
ref mut mode,
363+
}) = &mut ui.popup
364+
else {
359365
return Ok(false);
360366
};
367+
368+
match conf
369+
.keymap_config
370+
.find_command_from_key_sequence(key_sequence)
371+
{
372+
Some(Command::Search) => {
373+
if let Some(InputMode::Normal) = mode {
374+
*mode = Some(InputMode::Insert);
375+
return Ok(true);
376+
}
377+
}
378+
Some(Command::FocusNextWindow | Command::FocusPreviousWindow) => {
379+
if let Some(mode) = mode.as_mut() {
380+
mode.toggle()
381+
}
382+
383+
return Ok(true);
384+
}
385+
Some(Command::ChooseSelected) => {
386+
if let Some(InputMode::Insert) = mode {
387+
*mode = Some(InputMode::Normal);
388+
}
389+
}
390+
_ => {}
391+
}
392+
361393
if key_sequence.keys.len() == 1 {
362394
if let Key::None(c) = key_sequence.keys[0] {
363395
match c {
364-
crossterm::event::KeyCode::Char(c) => {
365-
query.push(c);
396+
crossterm::event::KeyCode::Char(ch)
397+
if matches!(mode, None | Some(InputMode::Insert)) =>
398+
{
399+
query.push(ch);
366400
ui.current_page_mut().select(0);
367401
return Ok(true);
368402
}

spotify_player/src/state/ui/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ impl UIState {
5959
self.current_page_mut().select(0);
6060
self.popup = Some(PopupState::Search {
6161
query: String::new(),
62+
mode: if config::get_config().app_config.modal_search {
63+
Some(InputMode::Insert)
64+
} else {
65+
None
66+
},
6267
});
6368
}
6469

@@ -91,7 +96,7 @@ impl UIState {
9196
/// Get a list of items possibly filtered by a search query if exists a search popup
9297
pub fn search_filtered_items<'a, T: std::fmt::Display>(&self, items: &'a [T]) -> Vec<&'a T> {
9398
match self.popup {
94-
Some(PopupState::Search { ref query }) => filtered_items_from_query(query, items),
99+
Some(PopupState::Search { ref query, .. }) => filtered_items_from_query(query, items),
95100
_ => items.iter().collect::<Vec<_>>(),
96101
}
97102
}

spotify_player/src/state/ui/page.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
state::model::{Category, ContextId},
2+
state::{InputMode, model::{Category, ContextId}},
33
ui::single_line_input::LineInput,
44
};
55
use ratatui::widgets::{ListState, TableState};
@@ -18,6 +18,7 @@ pub enum PageState {
1818
line_input: LineInput,
1919
current_query: String,
2020
state: SearchPageUIState,
21+
mode: Option<InputMode>,
2122
},
2223
Lyrics {
2324
track_uri: String,

spotify_player/src/state/ui/popup.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,27 @@ pub enum PlaylistCreateCurrentField {
1111
Desc,
1212
}
1313

14+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
15+
pub enum InputMode {
16+
#[default]
17+
Insert,
18+
Normal,
19+
}
20+
21+
impl InputMode {
22+
pub fn toggle(&mut self) {
23+
*self = match self {
24+
InputMode::Insert => InputMode::Normal,
25+
InputMode::Normal => InputMode::Insert,
26+
};
27+
}
28+
}
29+
1430
#[derive(Debug)]
1531
pub enum PopupState {
1632
Search {
1733
query: String,
34+
mode: Option<InputMode>,
1835
},
1936
UserPlaylistList(PlaylistPopupAction, ListState),
2037
UserFollowedArtistList(ListState),

spotify_player/src/ui/page.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub fn render_search_page(
5353
state,
5454
current_query,
5555
line_input,
56+
..
5657
} => (state.focus, current_query, line_input),
5758
_ => return,
5859
};

spotify_player/src/ui/popup.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use super::{
55
Paragraph, PlaylistCreateCurrentField, PlaylistPopupAction, PopupState, Rect, Row, SharedState,
66
Table, UIStateGuard,
77
};
8+
use crate::state::InputMode;
89

910
const SHORTCUT_TABLE_N_COLUMNS: usize = 3;
1011
const SHORTCUT_TABLE_CONSTRAINS: [Constraint; SHORTCUT_TABLE_N_COLUMNS] =
@@ -62,14 +63,21 @@ pub fn render_popup(
6263
);
6364
(chunks[0], true)
6465
}
65-
PopupState::Search { query } => {
66+
PopupState::Search { query, mode } => {
6667
let chunks =
6768
Layout::vertical([Constraint::Fill(0), Constraint::Length(3)]).split(rect);
6869

6970
let rect =
7071
construct_and_render_block("Search", &ui.theme, Borders::ALL, frame, chunks[1]);
7172

72-
frame.render_widget(Paragraph::new(format!("/{query}")), rect);
73+
match mode {
74+
Some(InputMode::Insert) | None => {
75+
frame.render_widget(Paragraph::new(format!("/{query}")), rect);
76+
}
77+
Some(InputMode::Normal) => {
78+
frame.render_widget(Paragraph::new(query.to_string()), rect);
79+
}
80+
}
7381
(chunks[0], true)
7482
}
7583
PopupState::ActionList(item, _) => {

spotify_player/src/ui/single_line_input.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,23 @@ impl LineInput {
9898

9999
let text_style = Style::default();
100100
let cursor_style = Style::default().add_modifier(Modifier::REVERSED);
101-
let formatted_line = Line::from(vec![
101+
let mut text_parts = vec![
102102
Span::styled(before_cursor, text_style),
103103
Span::styled(cursor, cursor_style),
104104
Span::styled(after_cursor, text_style),
105-
]);
105+
];
106+
107+
if is_active && crate::config::get_config().app_config.modal_search {
108+
text_parts.splice(
109+
0..0,
110+
[Span::styled(
111+
"/",
112+
Style::default().fg(ratatui::style::Color::DarkGray),
113+
)],
114+
);
115+
}
116+
117+
let formatted_line = Line::from(text_parts);
106118

107119
Paragraph::new(formatted_line)
108120
}

0 commit comments

Comments
 (0)