add search functionality

This commit is contained in:
krolxon 2024-04-26 16:54:58 +05:30
parent 04e5d2ad28
commit 91a7aeab42
6 changed files with 394 additions and 204 deletions

View File

@ -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

View File

@ -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<String>,
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::<Vec<&str>>();
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;
}
}

View File

@ -1,5 +1,3 @@
use mpd::Query;
use crate::{app::AppResult, connection::Connection};
#[derive(Debug)]

View File

@ -1,11 +1,76 @@
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<()> {
if app.inputmode == InputMode::Editing {
// Live update
let list: Vec<&str> = app
.browser
.filetree
.iter()
.map(|(_, f)| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
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::<Vec<&str>>();
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') => {
@ -99,7 +164,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
.iter()
.map(|f| f.as_str())
.collect::<Vec<&str>>();
let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list)
let (filename, _) =
rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list)
.get(0)
.unwrap()
.clone();
@ -196,8 +262,13 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
app.conn.push(&song)?;
}
_ => {}
// Search for songs
KeyCode::Char('/') => {
app.inputmode = InputMode::toggle_editing_states(&app.inputmode);
}
_ => {}
}
}
Ok(())
}

View File

@ -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};

View File

@ -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]),
}
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,7 +204,6 @@ 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!(