From 04e5d2ad28af2c0b561ed4443eabddc4ee70d1f2 Mon Sep 17 00:00:00 2001 From: krolxon Date: Fri, 26 Apr 2024 14:32:58 +0530 Subject: [PATCH] alot of edge cases removed, ui improvments --- Cargo.lock | 7 ++++ Cargo.toml | 1 + README.md | 15 +++++++-- src/app.rs | 21 ++---------- src/browser.rs | 43 ++++++++++++++++-------- src/cli.rs | 4 +-- src/connection.rs | 66 ++++++++++++++++++++++++++---------- src/handler.rs | 44 +++++++++++++++++++++--- src/lib.rs | 3 ++ src/list.rs | 18 ++++++---- src/main.rs | 58 +++++++++++++++++++------------- src/song.rs | 67 +++++++++++++++++++++++++++++++++++++ src/ui.rs | 85 +++++++++++++++++++++++++++++++++++------------ 13 files changed, 320 insertions(+), 112 deletions(-) create mode 100755 src/song.rs diff --git a/Cargo.lock b/Cargo.lock index 79b2484..fdabdc5 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "indoc" version = "2.0.5" @@ -386,6 +392,7 @@ version = "0.1.0" dependencies = [ "clap", "crossterm", + "humantime", "mpd", "ratatui", "rust-fuzzy-search", diff --git a/Cargo.toml b/Cargo.toml index ef3cf49..89621b4 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ simple-dmenu = "0.1.0" ratatui = "0.26.2" crossterm = "0.27.0" rust-fuzzy-search = "0.1.1" +humantime = "2.1.0" diff --git a/README.md b/README.md index 61e9a1e..ae1e7f0 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ A MPD client in Rust ### Keys -- `q` OR 'Ctr+C' to quit +- `q` OR `Ctr+C` to quit - `p` to toggle pause - `+` to increase volume - `-` to decrease volume @@ -16,9 +16,18 @@ A MPD client in Rust - `3` to go to playlists view - `Enter` to add song/playlist to current playlist - `a` to append the song to current playing queue -- `f` to go forward -- `b` to go backwords +- `f` to go forwards +- `b` to go backwards - `>` to play next song from queue - `<` to play previous song from queue - `U` to update the MPD database +- `r` to toggle repeat +- `z` to toggle random +### TODO +- [x] fix performance issues +- [ ] improvements on queue control +- [ ] add to playlists, playlists view +- [ ] search for songs +- [ ] metadata based tree view +- [ ] Humantime format diff --git a/src/app.rs b/src/app.rs index be7c358..b43cd7a 100755 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,6 @@ pub struct App { /// check if app is running pub running: bool, pub conn: Connection, - pub song_list: ContentList, pub queue_list: ContentList, pub pl_list: ContentList, pub selected_tab: SelectedTab, @@ -28,16 +27,6 @@ pub enum SelectedTab { Playlists, } -impl SelectedTab { - fn as_usize(&self) { - match self { - SelectedTab::Queue => 0, - SelectedTab::Playlists => 1, - SelectedTab::DirectoryBrowser => 2, - }; - } -} - impl App { pub fn builder(addrs: &str) -> AppResult { let mut conn = Connection::new(addrs).unwrap(); @@ -47,14 +36,10 @@ impl App { Self::get_queue(&mut conn, &mut queue_list.list); - let mut song_list = ContentList::new(); - song_list.list = conn.songs_filenames.clone(); - let browser = FileBrowser::new(); Ok(Self { running: true, conn, - song_list, queue_list, pl_list, selected_tab: SelectedTab::DirectoryBrowser, @@ -63,11 +48,9 @@ impl App { } pub fn tick(&mut self) { - self.conn.update_state(); - self.conn.update_progress(); - self.conn.update_volume(); + self.conn.update_status(); self.update_queue(); - self.browser.update_directory(&mut self.conn).unwrap(); + // self.browser.update_directory(&mut self.conn).unwrap(); } pub fn quit(&mut self) { diff --git a/src/browser.rs b/src/browser.rs index 0efec1b..a8c62fe 100755 --- a/src/browser.rs +++ b/src/browser.rs @@ -1,3 +1,5 @@ +use mpd::Query; + use crate::{app::AppResult, connection::Connection}; #[derive(Debug)] @@ -59,32 +61,45 @@ impl FileBrowser { if t == "directory" { if path != "." { self.prev_path = self.path.clone(); - self.path = path.to_string(); + self.path = self.prev_path.clone() + "/" + path; self.update_directory(conn)?; self.prev_selected = self.selected; self.selected = 0; + // println!("self.path: {}", self.path); + // println!("self.prev_pat: {}", self.prev_path); } } else { - let list = conn - .songs_filenames - .iter() - .map(|f| f.as_str()) - .collect::>(); - let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list) - .get(0) - .unwrap() - .clone(); + // let list = conn + // .songs_filenames + // .iter() + // .map(|f| f.as_str()) + // .collect::>(); + // let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list) + // .get(0) + // .unwrap() + // .clone(); - let song = conn.get_song_with_only_filename(filename); - conn.push(&song)?; + for filename in conn.songs_filenames.clone().iter() { + if filename.contains(path) { + let song = conn.get_song_with_only_filename(filename); + conn.push(&song)?; + } + } } Ok(()) } pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> { - self.path = self.prev_path.clone(); + if self.prev_path != "." { + let r = self.path.rfind("/").unwrap(); + self.path = self.path.as_str()[..r].to_string(); + self.update_directory(conn)?; + } else { + self.path = self.prev_path.clone(); + self.update_directory(conn)?; + } + self.selected = self.prev_selected; - self.update_directory(conn)?; Ok(()) } } diff --git a/src/cli.rs b/src/cli.rs index 131f746..2992952 100755 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,10 +6,10 @@ use clap::{Parser, Subcommand}; pub struct Args { /// No TUI #[arg(short= 'n', default_value="false")] - pub no_tui: bool, + pub tui: bool, #[command(subcommand)] - pub command: Command, + pub command: Option, } #[derive(Debug, Subcommand)] diff --git a/src/connection.rs b/src/connection.rs index b77da1b..b84d429 100755 --- a/src/connection.rs +++ b/src/connection.rs @@ -15,6 +15,8 @@ pub struct Connection { pub elapsed: Duration, pub total_duration: Duration, pub volume: u8, + pub repeat: bool, + pub random: bool, } impl Connection { @@ -27,8 +29,11 @@ impl Connection { .into_iter() .map(|x| x.file) .collect(); - let (elapsed, total) = conn.status().unwrap().time.unwrap_or_default(); - let volume: u8 = conn.status().unwrap_or_default().volume as u8; + let status = conn.status().unwrap(); + let (elapsed, total) = status.time.unwrap_or_default(); + let volume: u8 = status.volume as u8; + let repeat = status.repeat; + let random = status.random; Ok(Self { conn, @@ -37,6 +42,8 @@ impl Connection { elapsed, total_duration: total, volume, + repeat, + random, }) } @@ -64,25 +71,33 @@ impl Connection { Ok(()) } - pub fn update_state(&mut self) -> String { - match self.conn.status().unwrap().state { + /// Update status + pub fn update_status(&mut self) { + let status = self.conn.status().unwrap(); + + // Playback State + match status.state { State::Stop => self.state = "Stopped".to_string(), State::Play => self.state = "Playing".to_string(), State::Pause => self.state = "Paused".to_string(), } - self.state.clone() - } - pub fn update_progress(&mut self) { - let (elapsed, total) = self.conn.status().unwrap().time.unwrap_or_default(); + // Progress + let (elapsed, total) = status.time.unwrap_or_default(); self.elapsed = elapsed; self.total_duration = total; + + // Volume + self.volume = status.volume as u8; + + // Repeat mode + self.repeat = status.repeat; + + // Random mode + self.random = status.random; } - pub fn update_volume(&mut self) { - self.volume = self.conn.status().unwrap_or_default().volume as u8; - } - + /// Get progress ratio of current playing song pub fn get_progress_ratio(&self) -> f64 { let total = self.total_duration.as_secs_f64(); if total == 0.0 { @@ -143,11 +158,6 @@ impl Connection { } } - /// get current playing song - pub fn get_current_song(&mut self) -> Option { - self.conn.currentsong().unwrap().unwrap_or_default().title - } - /// Print status to stdout pub fn status(&mut self) { let current_song = self.conn.currentsong(); @@ -170,7 +180,7 @@ impl Connection { let song = self.conn.currentsong()?.unwrap_or_default(); if let Some(s) = song.title { if let Some(a) = song.artist { - return Ok(Some(format!("\"{}\" By {}", a, s))); + return Ok(Some(format!("\"{}\" By {}", s, a))); } else { return Ok(Some(s)); } @@ -190,7 +200,26 @@ impl Connection { self.conn.toggle_pause().unwrap(); } + /// Toggle Repeat mode + pub fn toggle_repeat(&mut self) { + if self.conn.status().unwrap().repeat { + self.conn.repeat(false).unwrap(); + } else { + self.conn.repeat(true).unwrap(); + } + } + + /// Toggle random mode + pub fn toggle_random(&mut self) { + if self.conn.status().unwrap().random { + self.conn.random(false).unwrap(); + } else { + self.conn.random(true).unwrap(); + } + } + // Volume controls + /// Increase Volume pub fn inc_volume(&mut self, v: i8) { let cur = self.conn.status().unwrap().volume; if cur + v <= 100 { @@ -198,6 +227,7 @@ impl Connection { } } + /// Decrease volume pub fn dec_volume(&mut self, v: i8) { let cur = self.conn.status().unwrap().volume; if cur - v >= 0 { diff --git a/src/handler.rs b/src/handler.rs index 15d5c49..ecc9e38 100755 --- a/src/handler.rs +++ b/src/handler.rs @@ -13,6 +13,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.quit(); } else { app.conn.conn.clear()?; + app.conn.update_status(); } } @@ -47,6 +48,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; } } + app.conn.update_status(); } KeyCode::Char('h') => match app.selected_tab { @@ -68,6 +70,18 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 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()?; @@ -75,9 +89,23 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { // 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 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)?; } @@ -127,16 +155,22 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { // Volume controls KeyCode::Char('=') => { app.conn.inc_volume(2); - app.conn.update_volume(); + app.conn.update_status(); } KeyCode::Char('-') => { app.conn.dec_volume(2); - app.conn.update_volume(); + 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(); } diff --git a/src/lib.rs b/src/lib.rs index 71a2e93..df79071 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,9 @@ pub mod tui; /// Content list pub mod list; +/// Song +pub mod song; + /// File Browser pub mod browser; diff --git a/src/list.rs b/src/list.rs index 452907d..631c40d 100755 --- a/src/list.rs +++ b/src/list.rs @@ -18,18 +18,24 @@ impl ContentList { // self.index += 1; // } - if self.index == self.list.len() - 1 { - self.index = 0; - } else { - self.index += 1; + let len = self.list.len(); + if len != 0 { + if self.index == self.list.len() - 1 { + self.index = 0; + } else { + self.index += 1; + } } } /// Go to previous item in list pub fn prev(&mut self) { if self.index == 0 { - self.index = self.list.len() - 1; - } else { + let len = self.list.len(); + if len != 0 { + self.index = len - 1; + } + } else { self.index -= 1; } } diff --git a/src/main.rs b/src/main.rs index 4d849f1..060d947 100755 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use rmptui::connection::Connection; use rmptui::event::Event; use rmptui::event::EventHandler; use rmptui::handler; +use rmptui::song::RSong; use rmptui::tui; use std::io; @@ -23,41 +24,52 @@ pub type Result = core::result::Result; pub type Error = Box; fn main() -> AppResult<()> { - // let args = Args::parse(); - - // if args.no_tui { - // handle_tui()?; - // } else { - // match args.command { - // Command::Volume { vol } => { - // conn.set_volume(vol); - // } - // Command::Dmenu => conn.play_dmenu().unwrap(), - // Command::Fzf => conn.play_fzf().unwrap(), - // Command::Status => conn.status(), - // Command::Pause => conn.pause(), - // Command::Toggle => conn.toggle_pause(), - // }; - // } - - let backend = CrosstermBackend::new(io::stderr()); - let terminal = Terminal::new(backend)?; + let args = Args::parse(); let mut app = App::builder("127.0.0.1:6600")?; - let events = EventHandler::new(250); + + if !args.tui { + handle_tui(&mut app)?; + } else { + match args.command { + Some(Command::Dmenu) => app.conn.play_dmenu()?, + Some(Command::Fzf) => app.conn.play_fzf().unwrap(), + Some(Command::Status) => app.conn.status(), + Some(Command::Pause) => app.conn.pause(), + Some(Command::Toggle) => app.conn.toggle_pause(), + _ => { + let mut vec: Vec = Vec::new(); + for filename in app.conn.songs_filenames { + let song = RSong::new(&mut app.conn.conn, filename); + vec.push(song); + } + println!("{:#?}", vec); + + } + } + } + Ok(()) +} + +pub fn handle_tui(app: &mut App) -> AppResult<()> { + let backend = CrosstermBackend::new(io::stderr()); + let terminal = Terminal::new(backend)?; + let events = EventHandler::new(1000); let mut tui = tui::Tui::new(terminal, events); tui.init()?; + // update the directory + app.browser.update_directory(&mut app.conn).unwrap(); + while app.running { - tui.draw(&mut app)?; + tui.draw(app)?; match tui.events.next()? { Event::Tick => app.tick(), - Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?, + Event::Key(key_event) => handler::handle_key_events(key_event, app)?, Event::Mouse(_) => {} Event::Resize(_, _) => {} } } - Ok(()) } diff --git a/src/song.rs b/src/song.rs new file mode 100755 index 0000000..f1a10eb --- /dev/null +++ b/src/song.rs @@ -0,0 +1,67 @@ +use mpd::{Client, Song}; + +#[derive(Debug)] +#[derive(Clone )] +pub struct RSong { + pub file: String, + pub artist: Option, + pub title: Option, + pub duration: Option, + pub last_mod: Option, + pub name: Option, + pub place: Option, + pub range: Option, + pub tags: Vec<(String, String)>, +} + +impl RSong { + pub fn new(c: &mut Client, filename: String) -> Self { + let mut s = RSong { + file: filename.clone(), + artist: None, + title: None, + duration: None, + last_mod: None, + name: None, + place: None, + range: None, + tags: vec![], + }; + + // Dummy song + + let song = Song { + file: filename.clone(), + artist: None, + title: None, + duration: None, + last_mod: None, + name: None, + place: None, + range: None, + tags: vec![("".to_string(), "".to_string())], + }; + + for (k, v) in (c.readcomments(song).unwrap()).flatten() { + if k.to_lowercase().contains("artist") { + s.artist = Some(v); + } else if k.to_lowercase().contains("title") { + s.title = Some(v); + } else if k.to_lowercase().contains("duration") { + s.duration = Some(v.parse::().unwrap()); + } else if k.to_lowercase().contains("lastmod") { + s.last_mod = Some(v); + } else if k.to_lowercase().contains("name") { + s.name = Some(v); + } else if k.to_lowercase().contains("place") { + s.place = Some(v); + } else if k.to_lowercase().contains("range") { + s.range = Some(v); + } else { + s.tags.push((k, v)); + } + } + + s + } +} diff --git a/src/ui.rs b/src/ui.rs index 2f1bca3..265a0d7 100755 --- a/src/ui.rs +++ b/src/ui.rs @@ -14,7 +14,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { // Layout let layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(93), Constraint::Percentage(7)]) + .constraints(vec![Constraint::Percentage(93), Constraint::Min(3)]) .split(frame.size()); match app.selected_tab { @@ -52,7 +52,12 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { ) .borders(Borders::ALL), ) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_style( + Style::new() + .fg(Color::Cyan) + .bg(Color::Black) + .add_modifier(Modifier::REVERSED), + ) .highlight_symbol(">>") .repeat_highlight_symbol(true) .scroll_padding(20); @@ -72,7 +77,12 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { ); let list = List::new(app.queue_list.list.clone()) .block(title.borders(Borders::ALL)) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_style( + Style::new() + .fg(Color::Cyan) + .bg(Color::Black) + .add_modifier(Modifier::REVERSED), + ) .highlight_symbol(">>") .repeat_highlight_symbol(true); @@ -93,7 +103,12 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { let list = List::new(app.pl_list.list.clone()) .block(title.borders(Borders::ALL)) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_style( + Style::new() + .fg(Color::Cyan) + .bg(Color::Black) + .add_modifier(Modifier::REVERSED), + ) .highlight_symbol(">>") .repeat_highlight_symbol(true); @@ -103,39 +118,65 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { /// Draws Progress Bar fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { + // Get the current playing song let song = app .conn .now_playing() .unwrap() .unwrap_or_else(|| "No Title Found".to_string()); - let state = &app.conn.state; - // let (elapsed, total) = app.conn.conn.status().unwrap().time.unwrap_or_default(); + // Get the current playing state + let mut state: String = String::new(); + if !app.queue_list.list.is_empty() { + state = app.conn.state.clone(); + state.push(':'); + } + // Get the current modes + let mut modes_bottom: String = String::new(); + // we do this to check if at least one mode is enabled so we can push "[]" + if app.conn.repeat | app.conn.random { + modes_bottom.push('r'); + } + + if !modes_bottom.is_empty() { + modes_bottom.clear(); + modes_bottom.push('['); + if app.conn.repeat { + modes_bottom.push('r'); + } + if app.conn.random { + modes_bottom.push('z'); + } + 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) + ) + } else { + "".to_string() + }; + + // Define the title let title = Block::default() - .title(Title::from(format!("{}: ", state).red().bold())) + .title(Title::from(format!("{}", state).red().bold())) .title(Title::from(song.green().bold())) - .title( - Title::from( - format!( - "{}/{}", - app.conn.elapsed.as_secs(), - app.conn.total_duration.as_secs() - ) - .cyan() - .bold(), - ) - .alignment(Alignment::Right), - ) + .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right)) + .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom)) .borders(Borders::ALL); - // .title(Title::from(app.conn.conn.status().unwrap_or_default().volume.to_string().yellow())).title_alignment(Alignment::Right); let progress_bar = LineGauge::default() .block(title.borders(Borders::ALL)) .gauge_style( Style::default() - .fg(Color::LightBlue) - .bg(Color::Gray) + .fg(Color::Blue) + .bg(Color::Black) .add_modifier(Modifier::BOLD), ) .line_set(symbols::line::THICK)