diff --git a/README.md b/README.md index ae1e7f0..cb7c65c 100755 --- a/README.md +++ b/README.md @@ -28,6 +28,6 @@ A MPD client in Rust - [x] fix performance issues - [ ] improvements on queue control - [ ] add to playlists, playlists view -- [ ] search for songs +- [x] search for songs - [ ] metadata based tree view -- [ ] Humantime format +- [x] Humantime format diff --git a/src/app.rs b/src/app.rs index b43cd7a..8b06be8 100755 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,7 @@ -use std::time::Duration; - use crate::browser::FileBrowser; use crate::connection::Connection; use crate::list::ContentList; +use crate::ui::InputMode; use mpd::Client; // Application result type @@ -18,6 +17,11 @@ pub struct App { pub pl_list: ContentList, pub selected_tab: SelectedTab, pub browser: FileBrowser, + + // Search + pub inputmode: InputMode, + pub search_input: String, + pub cursor_position: usize, } #[derive(Debug, PartialEq, Clone)] @@ -44,6 +48,9 @@ impl App { pl_list, selected_tab: SelectedTab::DirectoryBrowser, browser, + inputmode: InputMode::Normal, + search_input: String::new(), + cursor_position: 0, }) } @@ -96,4 +103,70 @@ impl App { SelectedTab::Playlists => SelectedTab::DirectoryBrowser, }; } + + pub fn search_song(&mut self) -> AppResult<()> { + let list = self + .conn + .songs_filenames + .iter() + .map(|f| f.as_str()) + .collect::>(); + let (filename, _) = + rust_fuzzy_search::fuzzy_search_sorted(self.search_input.as_str(), &list) + .get(0) + .unwrap() + .clone(); + + let song = self.conn.get_song_with_only_filename(filename); + self.conn.push(&song)?; + + Ok(()) + } + + // Cursor movements + pub fn move_cursor_left(&mut self) { + let cursor_moved_left = self.cursor_position.saturating_sub(1); + self.cursor_position = self.clamp_cursor(cursor_moved_left); + } + + pub fn move_cursor_right(&mut self) { + let cursor_moved_right = self.cursor_position.saturating_add(1); + self.cursor_position = self.clamp_cursor(cursor_moved_right); + } + + pub fn enter_char(&mut self, new_char: char) { + self.search_input.insert(self.cursor_position, new_char); + + self.move_cursor_right(); + } + + pub fn delete_char(&mut self) { + let is_not_cursor_leftmost = self.cursor_position != 0; + if is_not_cursor_leftmost { + // Method "remove" is not used on the saved text for deleting the selected char. + // Reason: Using remove on String works on bytes instead of the chars. + // Using remove would require special care because of char boundaries. + + let current_index = self.cursor_position; + let from_left_to_current_index = current_index - 1; + + // Getting all characters before the selected character. + let before_char_to_delete = self.search_input.chars().take(from_left_to_current_index); + // Getting all characters after selected character. + let after_char_to_delete = self.search_input.chars().skip(current_index); + + // Put all characters together except the selected one. + // By leaving the selected one out, it is forgotten and therefore deleted. + self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); + } + } + + pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { + new_cursor_pos.clamp(0, self.search_input.len()) + } + + pub fn reset_cursor(&mut self) { + self.cursor_position = 0; + } } diff --git a/src/browser.rs b/src/browser.rs index a8c62fe..88d1db1 100755 --- a/src/browser.rs +++ b/src/browser.rs @@ -1,5 +1,3 @@ -use mpd::Query; - use crate::{app::AppResult, connection::Connection}; #[derive(Debug)] diff --git a/src/handler.rs b/src/handler.rs index ecc9e38..21743ea 100755 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,203 +1,274 @@ use std::time::Duration; -use crate::app::{App, AppResult, SelectedTab}; +use crate::{ + app::{App, AppResult, SelectedTab}, + ui::InputMode, +}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use rust_fuzzy_search; +use rust_fuzzy_search::{self, fuzzy_search_sorted}; use simple_dmenu::dmenu; pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { - match key_event.code { - KeyCode::Char('q') | KeyCode::Esc => app.quit(), - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.quit(); - } else { - app.conn.conn.clear()?; + if app.inputmode == InputMode::Editing { + // Live update + let list: Vec<&str> = app + .browser + .filetree + .iter() + .map(|(_, f)| f.as_str()) + .collect::>(); + + let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list); + let res = res.iter().map(|(x, _)| *x).collect::>(); + + for (i, (_, item)) in app.browser.filetree.iter().enumerate() { + if item.contains(res.get(0).unwrap()) { + app.browser.selected = i; + } + } + + match key_event.code { + KeyCode::Esc => { + app.inputmode = InputMode::Normal; + } + KeyCode::Char(to_insert) => { + app.enter_char(to_insert); + } + KeyCode::Enter => { + let list: Vec<&str> = app + .browser + .filetree + .iter() + .map(|(_, f)| f.as_str()) + .collect::>(); + + let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list); + let (res, _) = res.get(0).unwrap(); + + for (i, (_, item)) in app.browser.filetree.iter().enumerate() { + if item.contains(res) { + app.browser.selected = i; + } + } + + app.search_input.clear(); + app.inputmode = InputMode::Normal; + app.reset_cursor(); + } + + KeyCode::Backspace => { + app.delete_char(); + } + + KeyCode::Left => { + app.move_cursor_left(); + } + + KeyCode::Right => { + app.move_cursor_right(); + } + + _ => {} + } + } else { + match key_event.code { + KeyCode::Char('q') | KeyCode::Esc => app.quit(), + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.quit(); + } else { + app.conn.conn.clear()?; + app.conn.update_status(); + } + } + + KeyCode::Char('j') | KeyCode::Down => match app.selected_tab { + SelectedTab::DirectoryBrowser => app.browser.next(), + SelectedTab::Queue => app.queue_list.next(), + SelectedTab::Playlists => app.pl_list.next(), + }, + + KeyCode::Char('k') | KeyCode::Up => match app.selected_tab { + SelectedTab::DirectoryBrowser => app.browser.prev(), + SelectedTab::Queue => app.queue_list.prev(), + SelectedTab::Playlists => app.pl_list.prev(), + }, + + KeyCode::Enter | KeyCode::Char('l') => { + // app.update_queue(); + + match app.selected_tab { + SelectedTab::DirectoryBrowser => { + app.browser.handle_enter(&mut app.conn)?; + } + + SelectedTab::Queue => { + let song = app.conn.get_song_with_only_filename( + app.queue_list.list.get(app.queue_list.index).unwrap(), + ); + app.conn.push(&song)?; + } + SelectedTab::Playlists => { + app.conn + .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; + } + } app.conn.update_status(); } - } - KeyCode::Char('j') | KeyCode::Down => match app.selected_tab { - SelectedTab::DirectoryBrowser => app.browser.next(), - SelectedTab::Queue => app.queue_list.next(), - SelectedTab::Playlists => app.pl_list.next(), - }, - - KeyCode::Char('k') | KeyCode::Up => match app.selected_tab { - SelectedTab::DirectoryBrowser => app.browser.prev(), - SelectedTab::Queue => app.queue_list.prev(), - SelectedTab::Playlists => app.pl_list.prev(), - }, - - KeyCode::Enter | KeyCode::Char('l') => { - // app.update_queue(); - - match app.selected_tab { + KeyCode::Char('h') => match app.selected_tab { SelectedTab::DirectoryBrowser => { - app.browser.handle_enter(&mut app.conn)?; + app.browser.handle_go_back(&mut app.conn)?; } + SelectedTab::Queue => {} + SelectedTab::Playlists => {} + }, - SelectedTab::Queue => { - let song = app.conn.get_song_with_only_filename( - app.queue_list.list.get(app.queue_list.index).unwrap(), - ); - app.conn.push(&song)?; - } - SelectedTab::Playlists => { - app.conn - .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; - } - } - app.conn.update_status(); - } - - KeyCode::Char('h') => match app.selected_tab { - SelectedTab::DirectoryBrowser => { - app.browser.handle_go_back(&mut app.conn)?; - } - SelectedTab::Queue => {} - SelectedTab::Playlists => {} - }, - - // Playback controls - // Toggle Pause - KeyCode::Char('p') => { - app.conn.toggle_pause(); - } - - // Pause - KeyCode::Char('s') => { - app.conn.pause(); - } - - // Toggle rpeat - KeyCode::Char('r') => { - app.conn.toggle_repeat(); - app.conn.update_status(); - } - - // Toggle random - KeyCode::Char('z') => { - app.conn.toggle_random(); - app.conn.update_status(); - } - - // Dmenu prompt - KeyCode::Char('D') => { - app.conn.play_dmenu()?; - } - - // add to queue - KeyCode::Char('a') => { - // let song = app.conn.get_song_with_only_filename( - // app.conn.songs_filenames.get(app.song_list.index).unwrap(), - // ); - - let list = app - .conn - .songs_filenames - .iter() - .map(|f| f.as_str()) - .collect::>(); - let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list) - .get(0) - .unwrap() - .clone(); - - let song = app.conn.get_song_with_only_filename(filename); - - app.conn.conn.push(&song)?; - } - - KeyCode::Right => { - app.conn - .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; - } - - KeyCode::Char('f') => { - let place = app.conn.conn.status().unwrap().song.unwrap().pos; - let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); - let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); - app.conn.conn.seek(place, pos)?; - } - - KeyCode::Char('b') => { - let place = app.conn.conn.status().unwrap().song.unwrap().pos; - let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); - let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); - app.conn.conn.seek(place, pos)?; - } - - KeyCode::Tab => { - app.cycle_tabls(); - } - - KeyCode::Char('1') => { - app.selected_tab = SelectedTab::DirectoryBrowser; - } - - KeyCode::Char('2') => { - app.selected_tab = SelectedTab::Queue; - } - - KeyCode::Char('3') => { - app.selected_tab = SelectedTab::Playlists; - } - - KeyCode::Char('>') => { - app.conn.conn.next()?; - } - - KeyCode::Char('<') => { - app.conn.conn.prev()?; - } - - // Volume controls - KeyCode::Char('=') => { - app.conn.inc_volume(2); - app.conn.update_status(); - } - - KeyCode::Char('-') => { - app.conn.dec_volume(2); - app.conn.update_status(); - } - - // Delete highlighted song from the queue - KeyCode::Char('d') => { - if app.queue_list.index >= app.queue_list.list.len() - 1 { - if app.queue_list.index != 0 { - app.queue_list.index -= 1; - } + // Playback controls + // Toggle Pause + KeyCode::Char('p') => { + app.conn.toggle_pause(); } - app.conn.conn.delete(app.queue_list.index as u32)?; - app.update_queue(); + // Pause + KeyCode::Char('s') => { + app.conn.pause(); + } + + // Toggle rpeat + KeyCode::Char('r') => { + app.conn.toggle_repeat(); + app.conn.update_status(); + } + + // Toggle random + KeyCode::Char('z') => { + app.conn.toggle_random(); + app.conn.update_status(); + } + + // Dmenu prompt + KeyCode::Char('D') => { + app.conn.play_dmenu()?; + } + + // add to queue + KeyCode::Char('a') => { + // let song = app.conn.get_song_with_only_filename( + // app.conn.songs_filenames.get(app.song_list.index).unwrap(), + // ); + + let list = app + .conn + .songs_filenames + .iter() + .map(|f| f.as_str()) + .collect::>(); + let (filename, _) = + rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list) + .get(0) + .unwrap() + .clone(); + + let song = app.conn.get_song_with_only_filename(filename); + + app.conn.conn.push(&song)?; + } + + KeyCode::Right => { + app.conn + .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; + } + + KeyCode::Char('f') => { + let place = app.conn.conn.status().unwrap().song.unwrap().pos; + let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); + let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); + app.conn.conn.seek(place, pos)?; + } + + KeyCode::Char('b') => { + let place = app.conn.conn.status().unwrap().song.unwrap().pos; + let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); + let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); + app.conn.conn.seek(place, pos)?; + } + + KeyCode::Tab => { + app.cycle_tabls(); + } + + KeyCode::Char('1') => { + app.selected_tab = SelectedTab::DirectoryBrowser; + } + + KeyCode::Char('2') => { + app.selected_tab = SelectedTab::Queue; + } + + KeyCode::Char('3') => { + app.selected_tab = SelectedTab::Playlists; + } + + KeyCode::Char('>') => { + app.conn.conn.next()?; + } + + KeyCode::Char('<') => { + app.conn.conn.prev()?; + } + + // Volume controls + KeyCode::Char('=') => { + app.conn.inc_volume(2); + app.conn.update_status(); + } + + KeyCode::Char('-') => { + app.conn.dec_volume(2); + app.conn.update_status(); + } + + // Delete highlighted song from the queue + KeyCode::Char('d') => { + if app.queue_list.index >= app.queue_list.list.len() - 1 { + if app.queue_list.index != 0 { + app.queue_list.index -= 1; + } + } + + app.conn.conn.delete(app.queue_list.index as u32)?; + app.update_queue(); + } + + KeyCode::Char('U') => { + app.conn.conn.update()?; + } + + KeyCode::Char('L') => { + let str = dmenu!(prompt "Search: "); + let list = app + .conn + .songs_filenames + .iter() + .map(|f| f.as_str()) + .collect::>(); + let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&str, &list) + .get(0) + .unwrap() + .clone(); + + let song = app.conn.get_song_with_only_filename(filename); + app.conn.push(&song)?; + } + + // Search for songs + KeyCode::Char('/') => { + app.inputmode = InputMode::toggle_editing_states(&app.inputmode); + } + + _ => {} } - - KeyCode::Char('U') => { - app.conn.conn.update()?; - } - - KeyCode::Char('L') => { - let str = dmenu!(prompt "Search: "); - let list = app - .conn - .songs_filenames - .iter() - .map(|f| f.as_str()) - .collect::>(); - let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&str, &list) - .get(0) - .unwrap() - .clone(); - - let song = app.conn.get_song_with_only_filename(filename); - app.conn.push(&song)?; - } - - _ => {} } - Ok(()) } diff --git a/src/tui.rs b/src/tui.rs index 5c94b3b..9ebd34f 100755 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,11 +1,7 @@ -use crate::connection::Connection; +use std::io; use crate::ui; use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::{ - terminal::{self, *}, -}; -use ratatui::prelude::*; -use std::io::{self, stdout, Stdout}; +use crossterm::terminal::{self, *}; use std::panic; use crate::app::{App, AppResult}; diff --git a/src/ui.rs b/src/ui.rs index 265a0d7..76a39ee 100755 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,6 +4,21 @@ use ratatui::{ widgets::{block::Title, *}, }; +#[derive(Debug, PartialEq)] +pub enum InputMode { + Editing, + Normal, +} + +impl InputMode { + pub fn toggle_editing_states(state: &InputMode) -> InputMode { + match state { + InputMode::Editing => return InputMode::Normal, + InputMode::Normal => return InputMode::Editing, + }; + } +} + /// Renders the user interface widgets pub fn render(app: &mut App, frame: &mut Frame) { // This is where you add new widgets. @@ -23,7 +38,14 @@ pub fn render(app: &mut App, frame: &mut Frame) { SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]), } - draw_progress_bar(frame, app, layout[1]); + match app.inputmode { + InputMode::Normal => { + draw_progress_bar(frame, app, layout[1]); + } + InputMode::Editing => { + draw_search_bar(frame, app, layout[1]); + } + } } /// Draws the file tree browser @@ -116,6 +138,37 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { frame.render_stateful_widget(list, size, &mut state); } +// Draw search bar +fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) { + match app.inputmode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask ratatui to put it at the specified coordinates after + // rendering + #[allow(clippy::cast_possible_truncation)] + frame.set_cursor( + // Draw the cursor at the current position in the input field. + // This position is can be controlled via the left and right arrow key + size.x + app.cursor_position as u16 + 2, + // Move one line down, from the border to the input line + size.y + 1, + ); + } + } + + let input = Paragraph::new("/".to_string() + &app.search_input) + .style(Style::default()) + .block( + Block::default() + .borders(Borders::ALL) + .title("Search Forward: ".bold().green()), + ); + frame.render_widget(input, size); +} + /// Draws Progress Bar fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { // Get the current playing song @@ -151,14 +204,13 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { modes_bottom.push(']'); }; - // get the duration let duration = if app.conn.total_duration.as_secs() != 0 { - format!( - "[{}/{}]", - humantime::format_duration(app.conn.elapsed), - humantime::format_duration(app.conn.total_duration) - ) + format!( + "[{}/{}]", + humantime::format_duration(app.conn.elapsed), + humantime::format_duration(app.conn.total_duration) + ) } else { "".to_string() };