Krolyxon
Commit 91a7aea

rmptui91a7aea

add search functionality

Author
krolxon
Date
2024-04-26 16:54 +0530
Parent
04e5d2a
Commit
91a7aeab42bead688909640f746fd673ee6630a5

+367 −177

6 files changed

  1. M README.md +2 −2
  2. M src/app.rs +75 −2
  3. M src/browser.rs +0 −2
  4. M src/handler.rs +229 −158
  5. M src/tui.rs +2 −6
  6. M src/ui.rs +59 −7
M README.md View
@@ -28,6 +28,6 @@ A MPD client in Rust
2828 - [x] fix performance issues
2929 - [ ] improvements on queue control
3030 - [ ] add to playlists, playlists view
31−- [ ] search for songs
31+- [x] search for songs
3232 - [ ] metadata based tree view
33−- [ ] Humantime format
33+- [x] Humantime format
M src/app.rs View
@@ -1,8 +1,7 @@
1−use std::time::Duration;
2−
31 use crate::browser::FileBrowser;
42 use crate::connection::Connection;
53 use crate::list::ContentList;
4+use crate::ui::InputMode;
65 use mpd::Client;
76
87 // Application result type
@@ -18,6 +17,11 @@ pub struct App {
1817 pub pl_list: ContentList<String>,
1918 pub selected_tab: SelectedTab,
2019 pub browser: FileBrowser,
20+
21+ // Search
22+ pub inputmode: InputMode,
23+ pub search_input: String,
24+ pub cursor_position: usize,
2125 }
2226
2327 #[derive(Debug, PartialEq, Clone)]
@@ -44,6 +48,9 @@ impl App {
4448 pl_list,
4549 selected_tab: SelectedTab::DirectoryBrowser,
4650 browser,
51+ inputmode: InputMode::Normal,
52+ search_input: String::new(),
53+ cursor_position: 0,
4754 })
4855 }
4956
@@ -96,4 +103,70 @@ impl App {
96103 SelectedTab::Playlists => SelectedTab::DirectoryBrowser,
97104 };
98105 }
106+
107+ pub fn search_song(&mut self) -> AppResult<()> {
108+ let list = self
109+ .conn
110+ .songs_filenames
111+ .iter()
112+ .map(|f| f.as_str())
113+ .collect::<Vec<&str>>();
114+ let (filename, _) =
115+ rust_fuzzy_search::fuzzy_search_sorted(self.search_input.as_str(), &list)
116+ .get(0)
117+ .unwrap()
118+ .clone();
119+
120+ let song = self.conn.get_song_with_only_filename(filename);
121+ self.conn.push(&song)?;
122+
123+ Ok(())
124+ }
125+
126+ // Cursor movements
127+ pub fn move_cursor_left(&mut self) {
128+ let cursor_moved_left = self.cursor_position.saturating_sub(1);
129+ self.cursor_position = self.clamp_cursor(cursor_moved_left);
130+ }
131+
132+ pub fn move_cursor_right(&mut self) {
133+ let cursor_moved_right = self.cursor_position.saturating_add(1);
134+ self.cursor_position = self.clamp_cursor(cursor_moved_right);
135+ }
136+
137+ pub fn enter_char(&mut self, new_char: char) {
138+ self.search_input.insert(self.cursor_position, new_char);
139+
140+ self.move_cursor_right();
141+ }
142+
143+ pub fn delete_char(&mut self) {
144+ let is_not_cursor_leftmost = self.cursor_position != 0;
145+ if is_not_cursor_leftmost {
146+ // Method "remove" is not used on the saved text for deleting the selected char.
147+ // Reason: Using remove on String works on bytes instead of the chars.
148+ // Using remove would require special care because of char boundaries.
149+
150+ let current_index = self.cursor_position;
151+ let from_left_to_current_index = current_index - 1;
152+
153+ // Getting all characters before the selected character.
154+ let before_char_to_delete = self.search_input.chars().take(from_left_to_current_index);
155+ // Getting all characters after selected character.
156+ let after_char_to_delete = self.search_input.chars().skip(current_index);
157+
158+ // Put all characters together except the selected one.
159+ // By leaving the selected one out, it is forgotten and therefore deleted.
160+ self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
161+ self.move_cursor_left();
162+ }
163+ }
164+
165+ pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
166+ new_cursor_pos.clamp(0, self.search_input.len())
167+ }
168+
169+ pub fn reset_cursor(&mut self) {
170+ self.cursor_position = 0;
171+ }
99172 }
M src/browser.rs View
@@ -1,5 +1,3 @@
1−use mpd::Query;
2−
31 use crate::{app::AppResult, connection::Connection};
42
53 #[derive(Debug)]
M src/handler.rs View
@@ -1,203 +1,274 @@
11 use std::time::Duration;
22
3−use crate::app::{App, AppResult, SelectedTab};
3+use crate::{
4+ app::{App, AppResult, SelectedTab},
5+ ui::InputMode,
6+};
47 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5−use rust_fuzzy_search;
8+use rust_fuzzy_search::{self, fuzzy_search_sorted};
69 use simple_dmenu::dmenu;
710
811 pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
9− match key_event.code {
10− KeyCode::Char('q') | KeyCode::Esc => app.quit(),
11− KeyCode::Char('c') | KeyCode::Char('C') => {
12− if key_event.modifiers == KeyModifiers::CONTROL {
13− app.quit();
14− } else {
15− app.conn.conn.clear()?;
16− app.conn.update_status();
12+ if app.inputmode == InputMode::Editing {
13+ // Live update
14+ let list: Vec<&str> = app
15+ .browser
16+ .filetree
17+ .iter()
18+ .map(|(_, f)| f.as_str())
19+ .collect::<Vec<&str>>();
20+
21+ let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
22+ let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
23+
24+ for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
25+ if item.contains(res.get(0).unwrap()) {
26+ app.browser.selected = i;
1727 }
1828 }
1929
20− KeyCode::Char('j') | KeyCode::Down => match app.selected_tab {
21− SelectedTab::DirectoryBrowser => app.browser.next(),
22− SelectedTab::Queue => app.queue_list.next(),
23− SelectedTab::Playlists => app.pl_list.next(),
24− },
30+ match key_event.code {
31+ KeyCode::Esc => {
32+ app.inputmode = InputMode::Normal;
33+ }
34+ KeyCode::Char(to_insert) => {
35+ app.enter_char(to_insert);
36+ }
37+ KeyCode::Enter => {
38+ let list: Vec<&str> = app
39+ .browser
40+ .filetree
41+ .iter()
42+ .map(|(_, f)| f.as_str())
43+ .collect::<Vec<&str>>();
44+
45+ let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
46+ let (res, _) = res.get(0).unwrap();
47+
48+ for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
49+ if item.contains(res) {
50+ app.browser.selected = i;
51+ }
52+ }
2553
26− KeyCode::Char('k') | KeyCode::Up => match app.selected_tab {
27− SelectedTab::DirectoryBrowser => app.browser.prev(),
28− SelectedTab::Queue => app.queue_list.prev(),
29− SelectedTab::Playlists => app.pl_list.prev(),
30− },
54+ app.search_input.clear();
55+ app.inputmode = InputMode::Normal;
56+ app.reset_cursor();
57+ }
3158
32− KeyCode::Enter | KeyCode::Char('l') => {
33− // app.update_queue();
59+ KeyCode::Backspace => {
60+ app.delete_char();
61+ }
3462
35− match app.selected_tab {
36− SelectedTab::DirectoryBrowser => {
37− app.browser.handle_enter(&mut app.conn)?;
63+ KeyCode::Left => {
64+ app.move_cursor_left();
65+ }
66+
67+ KeyCode::Right => {
68+ app.move_cursor_right();
69+ }
70+
71+ _ => {}
72+ }
73+ } else {
74+ match key_event.code {
75+ KeyCode::Char('q') | KeyCode::Esc => app.quit(),
76+ KeyCode::Char('c') | KeyCode::Char('C') => {
77+ if key_event.modifiers == KeyModifiers::CONTROL {
78+ app.quit();
79+ } else {
80+ app.conn.conn.clear()?;
81+ app.conn.update_status();
3882 }
83+ }
3984
40− SelectedTab::Queue => {
41− let song = app.conn.get_song_with_only_filename(
42− app.queue_list.list.get(app.queue_list.index).unwrap(),
43− );
44− app.conn.push(&song)?;
85+ KeyCode::Char('j') | KeyCode::Down => match app.selected_tab {
86+ SelectedTab::DirectoryBrowser => app.browser.next(),
87+ SelectedTab::Queue => app.queue_list.next(),
88+ SelectedTab::Playlists => app.pl_list.next(),
89+ },
90+
91+ KeyCode::Char('k') | KeyCode::Up => match app.selected_tab {
92+ SelectedTab::DirectoryBrowser => app.browser.prev(),
93+ SelectedTab::Queue => app.queue_list.prev(),
94+ SelectedTab::Playlists => app.pl_list.prev(),
95+ },
96+
97+ KeyCode::Enter | KeyCode::Char('l') => {
98+ // app.update_queue();
99+
100+ match app.selected_tab {
101+ SelectedTab::DirectoryBrowser => {
102+ app.browser.handle_enter(&mut app.conn)?;
103+ }
104+
105+ SelectedTab::Queue => {
106+ let song = app.conn.get_song_with_only_filename(
107+ app.queue_list.list.get(app.queue_list.index).unwrap(),
108+ );
109+ app.conn.push(&song)?;
110+ }
111+ SelectedTab::Playlists => {
112+ app.conn
113+ .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
114+ }
45115 }
46− SelectedTab::Playlists => {
47− app.conn
48− .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
116+ app.conn.update_status();
117+ }
118+
119+ KeyCode::Char('h') => match app.selected_tab {
120+ SelectedTab::DirectoryBrowser => {
121+ app.browser.handle_go_back(&mut app.conn)?;
49122 }
123+ SelectedTab::Queue => {}
124+ SelectedTab::Playlists => {}
125+ },
126+
127+ // Playback controls
128+ // Toggle Pause
129+ KeyCode::Char('p') => {
130+ app.conn.toggle_pause();
50131 }
51− app.conn.update_status();
52− }
53132
54− KeyCode::Char('h') => match app.selected_tab {
55− SelectedTab::DirectoryBrowser => {
56− app.browser.handle_go_back(&mut app.conn)?;
133+ // Pause
134+ KeyCode::Char('s') => {
135+ app.conn.pause();
57136 }
58− SelectedTab::Queue => {}
59− SelectedTab::Playlists => {}
60− },
61137
62− // Playback controls
63− // Toggle Pause
64− KeyCode::Char('p') => {
65− app.conn.toggle_pause();
66− }
138+ // Toggle rpeat
139+ KeyCode::Char('r') => {
140+ app.conn.toggle_repeat();
141+ app.conn.update_status();
142+ }
67143
68− // Pause
69− KeyCode::Char('s') => {
70− app.conn.pause();
71− }
144+ // Toggle random
145+ KeyCode::Char('z') => {
146+ app.conn.toggle_random();
147+ app.conn.update_status();
148+ }
72149
73− // Toggle rpeat
74− KeyCode::Char('r') => {
75− app.conn.toggle_repeat();
76− app.conn.update_status();
77− }
150+ // Dmenu prompt
151+ KeyCode::Char('D') => {
152+ app.conn.play_dmenu()?;
153+ }
78154
79− // Toggle random
80− KeyCode::Char('z') => {
81− app.conn.toggle_random();
82− app.conn.update_status();
83− }
155+ // add to queue
156+ KeyCode::Char('a') => {
157+ // let song = app.conn.get_song_with_only_filename(
158+ // app.conn.songs_filenames.get(app.song_list.index).unwrap(),
159+ // );
160+
161+ let list = app
162+ .conn
163+ .songs_filenames
164+ .iter()
165+ .map(|f| f.as_str())
166+ .collect::<Vec<&str>>();
167+ let (filename, _) =
168+ rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list)
169+ .get(0)
170+ .unwrap()
171+ .clone();
172+
173+ let song = app.conn.get_song_with_only_filename(filename);
174+
175+ app.conn.conn.push(&song)?;
176+ }
84177
85− // Dmenu prompt
86− KeyCode::Char('D') => {
87− app.conn.play_dmenu()?;
88− }
178+ KeyCode::Right => {
179+ app.conn
180+ .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
181+ }
89182
90− // add to queue
91− KeyCode::Char('a') => {
92− // let song = app.conn.get_song_with_only_filename(
93− // app.conn.songs_filenames.get(app.song_list.index).unwrap(),
94− // );
95−
96− let list = app
97− .conn
98− .songs_filenames
99− .iter()
100− .map(|f| f.as_str())
101− .collect::<Vec<&str>>();
102− let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list)
103− .get(0)
104− .unwrap()
105− .clone();
106−
107− let song = app.conn.get_song_with_only_filename(filename);
108−
109− app.conn.conn.push(&song)?;
110− }
183+ KeyCode::Char('f') => {
184+ let place = app.conn.conn.status().unwrap().song.unwrap().pos;
185+ let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
186+ let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
187+ app.conn.conn.seek(place, pos)?;
188+ }
111189
112− KeyCode::Right => {
113− app.conn
114− .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
115− }
190+ KeyCode::Char('b') => {
191+ let place = app.conn.conn.status().unwrap().song.unwrap().pos;
192+ let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
193+ let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
194+ app.conn.conn.seek(place, pos)?;
195+ }
116196
117− KeyCode::Char('f') => {
118− let place = app.conn.conn.status().unwrap().song.unwrap().pos;
119− let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
120− let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
121− app.conn.conn.seek(place, pos)?;
122− }
197+ KeyCode::Tab => {
198+ app.cycle_tabls();
199+ }
123200
124− KeyCode::Char('b') => {
125− let place = app.conn.conn.status().unwrap().song.unwrap().pos;
126− let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
127− let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
128− app.conn.conn.seek(place, pos)?;
129− }
201+ KeyCode::Char('1') => {
202+ app.selected_tab = SelectedTab::DirectoryBrowser;
203+ }
130204
131− KeyCode::Tab => {
132− app.cycle_tabls();
133− }
205+ KeyCode::Char('2') => {
206+ app.selected_tab = SelectedTab::Queue;
207+ }
134208
135− KeyCode::Char('1') => {
136− app.selected_tab = SelectedTab::DirectoryBrowser;
137− }
209+ KeyCode::Char('3') => {
210+ app.selected_tab = SelectedTab::Playlists;
211+ }
138212
139− KeyCode::Char('2') => {
140− app.selected_tab = SelectedTab::Queue;
141− }
213+ KeyCode::Char('>') => {
214+ app.conn.conn.next()?;
215+ }
142216
143− KeyCode::Char('3') => {
144− app.selected_tab = SelectedTab::Playlists;
145− }
217+ KeyCode::Char('<') => {
218+ app.conn.conn.prev()?;
219+ }
146220
147− KeyCode::Char('>') => {
148− app.conn.conn.next()?;
149− }
221+ // Volume controls
222+ KeyCode::Char('=') => {
223+ app.conn.inc_volume(2);
224+ app.conn.update_status();
225+ }
150226
151− KeyCode::Char('<') => {
152− app.conn.conn.prev()?;
153− }
227+ KeyCode::Char('-') => {
228+ app.conn.dec_volume(2);
229+ app.conn.update_status();
230+ }
154231
155− // Volume controls
156− KeyCode::Char('=') => {
157− app.conn.inc_volume(2);
158− app.conn.update_status();
159− }
232+ // Delete highlighted song from the queue
233+ KeyCode::Char('d') => {
234+ if app.queue_list.index >= app.queue_list.list.len() - 1 {
235+ if app.queue_list.index != 0 {
236+ app.queue_list.index -= 1;
237+ }
238+ }
160239
161− KeyCode::Char('-') => {
162− app.conn.dec_volume(2);
163− app.conn.update_status();
164− }
240+ app.conn.conn.delete(app.queue_list.index as u32)?;
241+ app.update_queue();
242+ }
165243
166− // Delete highlighted song from the queue
167− KeyCode::Char('d') => {
168− if app.queue_list.index >= app.queue_list.list.len() - 1 {
169− if app.queue_list.index != 0 {
170− app.queue_list.index -= 1;
171− }
244+ KeyCode::Char('U') => {
245+ app.conn.conn.update()?;
172246 }
173247
174− app.conn.conn.delete(app.queue_list.index as u32)?;
175− app.update_queue();
176− }
248+ KeyCode::Char('L') => {
249+ let str = dmenu!(prompt "Search: ");
250+ let list = app
251+ .conn
252+ .songs_filenames
253+ .iter()
254+ .map(|f| f.as_str())
255+ .collect::<Vec<&str>>();
256+ let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&str, &list)
257+ .get(0)
258+ .unwrap()
259+ .clone();
260+
261+ let song = app.conn.get_song_with_only_filename(filename);
262+ app.conn.push(&song)?;
263+ }
177264
178− KeyCode::Char('U') => {
179− app.conn.conn.update()?;
180− }
265+ // Search for songs
266+ KeyCode::Char('/') => {
267+ app.inputmode = InputMode::toggle_editing_states(&app.inputmode);
268+ }
181269
182− KeyCode::Char('L') => {
183− let str = dmenu!(prompt "Search: ");
184− let list = app
185− .conn
186− .songs_filenames
187− .iter()
188− .map(|f| f.as_str())
189− .collect::<Vec<&str>>();
190− let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&str, &list)
191− .get(0)
192− .unwrap()
193− .clone();
194−
195− let song = app.conn.get_song_with_only_filename(filename);
196− app.conn.push(&song)?;
270+ _ => {}
197271 }
198−
199− _ => {}
200272 }
201−
202273 Ok(())
203274 }
M src/tui.rs View
@@ -1,11 +1,7 @@
1−use crate::connection::Connection;
1+use std::io;
22 use crate::ui;
33 use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
4−use crossterm::{
5− terminal::{self, *},
6−};
7−use ratatui::prelude::*;
8−use std::io::{self, stdout, Stdout};
4+use crossterm::terminal::{self, *};
95 use std::panic;
106
117 use crate::app::{App, AppResult};
M src/ui.rs View
@@ -4,6 +4,21 @@ use ratatui::{
44 widgets::{block::Title, *},
55 };
66
7+#[derive(Debug, PartialEq)]
8+pub enum InputMode {
9+ Editing,
10+ Normal,
11+}
12+
13+impl InputMode {
14+ pub fn toggle_editing_states(state: &InputMode) -> InputMode {
15+ match state {
16+ InputMode::Editing => return InputMode::Normal,
17+ InputMode::Normal => return InputMode::Editing,
18+ };
19+ }
20+}
21+
722 /// Renders the user interface widgets
823 pub fn render(app: &mut App, frame: &mut Frame) {
924 // This is where you add new widgets.
@@ -23,7 +38,14 @@ pub fn render(app: &mut App, frame: &mut Frame) {
2338 SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
2439 }
2540
26− draw_progress_bar(frame, app, layout[1]);
41+ match app.inputmode {
42+ InputMode::Normal => {
43+ draw_progress_bar(frame, app, layout[1]);
44+ }
45+ InputMode::Editing => {
46+ draw_search_bar(frame, app, layout[1]);
47+ }
48+ }
2749 }
2850
2951 /// Draws the file tree browser
@@ -116,6 +138,37 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
116138 frame.render_stateful_widget(list, size, &mut state);
117139 }
118140
141+// Draw search bar
142+fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
143+ match app.inputmode {
144+ InputMode::Normal =>
145+ // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
146+ {}
147+
148+ InputMode::Editing => {
149+ // Make the cursor visible and ask ratatui to put it at the specified coordinates after
150+ // rendering
151+ #[allow(clippy::cast_possible_truncation)]
152+ frame.set_cursor(
153+ // Draw the cursor at the current position in the input field.
154+ // This position is can be controlled via the left and right arrow key
155+ size.x + app.cursor_position as u16 + 2,
156+ // Move one line down, from the border to the input line
157+ size.y + 1,
158+ );
159+ }
160+ }
161+
162+ let input = Paragraph::new("/".to_string() + &app.search_input)
163+ .style(Style::default())
164+ .block(
165+ Block::default()
166+ .borders(Borders::ALL)
167+ .title("Search Forward: ".bold().green()),
168+ );
169+ frame.render_widget(input, size);
170+}
171+
119172 /// Draws Progress Bar
120173 fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
121174 // Get the current playing song
@@ -151,14 +204,13 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
151204 modes_bottom.push(']');
152205 };
153206
154−
155207 // get the duration
156208 let duration = if app.conn.total_duration.as_secs() != 0 {
157− format!(
158− "[{}/{}]",
159− humantime::format_duration(app.conn.elapsed),
160− humantime::format_duration(app.conn.total_duration)
161− )
209+ format!(
210+ "[{}/{}]",
211+ humantime::format_duration(app.conn.elapsed),
212+ humantime::format_duration(app.conn.total_duration)
213+ )
162214 } else {
163215 "".to_string()
164216 };