rmptui04e5d2a
alot of edge cases removed, ui improvments
+319 −111
13 files changed
- M Cargo.lock +7 −0
- M Cargo.toml +1 −0
- M README.md +12 −3
- M src/app.rs +2 −19
- M src/browser.rs +29 −14
- M src/cli.rs +2 −2
- M src/connection.rs +47 −17
- M src/handler.rs +39 −5
- M src/lib.rs +3 −0
- M src/list.rs +12 −6
- M src/main.rs +35 −23
- A src/song.rs +67 −0
- M src/ui.rs +63 −22
| @@ -225,6 +225,12 @@ version = "0.5.0" | |||
| 225 | 225 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 226 | 226 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 227 | 227 | ||
| 228 | + | [[package]] | |
| 229 | + | name = "humantime" | |
| 230 | + | version = "2.1.0" | |
| 231 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 232 | + | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" | |
| 233 | + | ||
| 228 | 234 | [[package]] | |
| 229 | 235 | name = "indoc" | |
| 230 | 236 | version = "2.0.5" | |
| @@ -386,6 +392,7 @@ version = "0.1.0" | |||
| 386 | 392 | dependencies = [ | |
| 387 | 393 | "clap", | |
| 388 | 394 | "crossterm", | |
| 395 | + | "humantime", | |
| 389 | 396 | "mpd", | |
| 390 | 397 | "ratatui", | |
| 391 | 398 | "rust-fuzzy-search", | |
| @@ -13,3 +13,4 @@ simple-dmenu = "0.1.0" | |||
| 13 | 13 | ratatui = "0.26.2" | |
| 14 | 14 | crossterm = "0.27.0" | |
| 15 | 15 | rust-fuzzy-search = "0.1.1" | |
| 16 | + | humantime = "2.1.0" | |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | A MPD client in Rust | |
| 2 | 2 | ||
| 3 | 3 | ### Keys | |
| 4 | − | - `q` OR 'Ctr+C' to quit | |
| 4 | + | - `q` OR `Ctr+C` to quit | |
| 5 | 5 | - `p` to toggle pause | |
| 6 | 6 | - `+` to increase volume | |
| 7 | 7 | - `-` to decrease volume | |
| @@ -16,9 +16,18 @@ A MPD client in Rust | |||
| 16 | 16 | - `3` to go to playlists view | |
| 17 | 17 | - `Enter` to add song/playlist to current playlist | |
| 18 | 18 | - `a` to append the song to current playing queue | |
| 19 | − | - `f` to go forward | |
| 20 | − | - `b` to go backwords | |
| 19 | + | - `f` to go forwards | |
| 20 | + | - `b` to go backwards | |
| 21 | 21 | - `>` to play next song from queue | |
| 22 | 22 | - `<` to play previous song from queue | |
| 23 | 23 | - `U` to update the MPD database | |
| 24 | + | - `r` to toggle repeat | |
| 25 | + | - `z` to toggle random | |
| 24 | 26 | ||
| 27 | + | ### TODO | |
| 28 | + | - [x] fix performance issues | |
| 29 | + | - [ ] improvements on queue control | |
| 30 | + | - [ ] add to playlists, playlists view | |
| 31 | + | - [ ] search for songs | |
| 32 | + | - [ ] metadata based tree view | |
| 33 | + | - [ ] Humantime format | |
| @@ -14,7 +14,6 @@ pub struct App { | |||
| 14 | 14 | /// check if app is running | |
| 15 | 15 | pub running: bool, | |
| 16 | 16 | pub conn: Connection, | |
| 17 | − | pub song_list: ContentList<String>, | |
| 18 | 17 | pub queue_list: ContentList<String>, | |
| 19 | 18 | pub pl_list: ContentList<String>, | |
| 20 | 19 | pub selected_tab: SelectedTab, | |
| @@ -28,16 +27,6 @@ pub enum SelectedTab { | |||
| 28 | 27 | Playlists, | |
| 29 | 28 | } | |
| 30 | 29 | ||
| 31 | − | impl SelectedTab { | |
| 32 | − | fn as_usize(&self) { | |
| 33 | − | match self { | |
| 34 | − | SelectedTab::Queue => 0, | |
| 35 | − | SelectedTab::Playlists => 1, | |
| 36 | − | SelectedTab::DirectoryBrowser => 2, | |
| 37 | − | }; | |
| 38 | − | } | |
| 39 | − | } | |
| 40 | − | ||
| 41 | 30 | impl App { | |
| 42 | 31 | pub fn builder(addrs: &str) -> AppResult<Self> { | |
| 43 | 32 | let mut conn = Connection::new(addrs).unwrap(); | |
| @@ -47,14 +36,10 @@ impl App { | |||
| 47 | 36 | ||
| 48 | 37 | Self::get_queue(&mut conn, &mut queue_list.list); | |
| 49 | 38 | ||
| 50 | − | let mut song_list = ContentList::new(); | |
| 51 | − | song_list.list = conn.songs_filenames.clone(); | |
| 52 | − | ||
| 53 | 39 | let browser = FileBrowser::new(); | |
| 54 | 40 | Ok(Self { | |
| 55 | 41 | running: true, | |
| 56 | 42 | conn, | |
| 57 | − | song_list, | |
| 58 | 43 | queue_list, | |
| 59 | 44 | pl_list, | |
| 60 | 45 | selected_tab: SelectedTab::DirectoryBrowser, | |
| @@ -63,11 +48,9 @@ impl App { | |||
| 63 | 48 | } | |
| 64 | 49 | ||
| 65 | 50 | pub fn tick(&mut self) { | |
| 66 | − | self.conn.update_state(); | |
| 67 | − | self.conn.update_progress(); | |
| 68 | − | self.conn.update_volume(); | |
| 51 | + | self.conn.update_status(); | |
| 69 | 52 | self.update_queue(); | |
| 70 | − | self.browser.update_directory(&mut self.conn).unwrap(); | |
| 53 | + | // self.browser.update_directory(&mut self.conn).unwrap(); | |
| 71 | 54 | } | |
| 72 | 55 | ||
| 73 | 56 | pub fn quit(&mut self) { | |
| @@ -1,3 +1,5 @@ | |||
| 1 | + | use mpd::Query; | |
| 2 | + | ||
| 1 | 3 | use crate::{app::AppResult, connection::Connection}; | |
| 2 | 4 | ||
| 3 | 5 | #[derive(Debug)] | |
| @@ -59,32 +61,45 @@ impl FileBrowser { | |||
| 59 | 61 | if t == "directory" { | |
| 60 | 62 | if path != "." { | |
| 61 | 63 | self.prev_path = self.path.clone(); | |
| 62 | − | self.path = path.to_string(); | |
| 64 | + | self.path = self.prev_path.clone() + "/" + path; | |
| 63 | 65 | self.update_directory(conn)?; | |
| 64 | 66 | self.prev_selected = self.selected; | |
| 65 | 67 | self.selected = 0; | |
| 68 | + | // println!("self.path: {}", self.path); | |
| 69 | + | // println!("self.prev_pat: {}", self.prev_path); | |
| 66 | 70 | } | |
| 67 | 71 | } else { | |
| 68 | − | let list = conn | |
| 69 | − | .songs_filenames | |
| 70 | − | .iter() | |
| 71 | − | .map(|f| f.as_str()) | |
| 72 | − | .collect::<Vec<&str>>(); | |
| 73 | − | let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list) | |
| 74 | − | .get(0) | |
| 75 | − | .unwrap() | |
| 76 | − | .clone(); | |
| 72 | + | // let list = conn | |
| 73 | + | // .songs_filenames | |
| 74 | + | // .iter() | |
| 75 | + | // .map(|f| f.as_str()) | |
| 76 | + | // .collect::<Vec<&str>>(); | |
| 77 | + | // let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list) | |
| 78 | + | // .get(0) | |
| 79 | + | // .unwrap() | |
| 80 | + | // .clone(); | |
| 77 | 81 | ||
| 78 | − | let song = conn.get_song_with_only_filename(filename); | |
| 79 | − | conn.push(&song)?; | |
| 82 | + | for filename in conn.songs_filenames.clone().iter() { | |
| 83 | + | if filename.contains(path) { | |
| 84 | + | let song = conn.get_song_with_only_filename(filename); | |
| 85 | + | conn.push(&song)?; | |
| 86 | + | } | |
| 87 | + | } | |
| 80 | 88 | } | |
| 81 | 89 | Ok(()) | |
| 82 | 90 | } | |
| 83 | 91 | ||
| 84 | 92 | pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> { | |
| 85 | − | self.path = self.prev_path.clone(); | |
| 93 | + | if self.prev_path != "." { | |
| 94 | + | let r = self.path.rfind("/").unwrap(); | |
| 95 | + | self.path = self.path.as_str()[..r].to_string(); | |
| 96 | + | self.update_directory(conn)?; | |
| 97 | + | } else { | |
| 98 | + | self.path = self.prev_path.clone(); | |
| 99 | + | self.update_directory(conn)?; | |
| 100 | + | } | |
| 101 | + | ||
| 86 | 102 | self.selected = self.prev_selected; | |
| 87 | − | self.update_directory(conn)?; | |
| 88 | 103 | Ok(()) | |
| 89 | 104 | } | |
| 90 | 105 | } | |
| @@ -6,10 +6,10 @@ use clap::{Parser, Subcommand}; | |||
| 6 | 6 | pub struct Args { | |
| 7 | 7 | /// No TUI | |
| 8 | 8 | #[arg(short= 'n', default_value="false")] | |
| 9 | − | pub no_tui: bool, | |
| 9 | + | pub tui: bool, | |
| 10 | 10 | ||
| 11 | 11 | #[command(subcommand)] | |
| 12 | − | pub command: Command, | |
| 12 | + | pub command: Option<Command>, | |
| 13 | 13 | } | |
| 14 | 14 | ||
| 15 | 15 | #[derive(Debug, Subcommand)] | |
| @@ -15,6 +15,8 @@ pub struct Connection { | |||
| 15 | 15 | pub elapsed: Duration, | |
| 16 | 16 | pub total_duration: Duration, | |
| 17 | 17 | pub volume: u8, | |
| 18 | + | pub repeat: bool, | |
| 19 | + | pub random: bool, | |
| 18 | 20 | } | |
| 19 | 21 | ||
| 20 | 22 | impl Connection { | |
| @@ -27,8 +29,11 @@ impl Connection { | |||
| 27 | 29 | .into_iter() | |
| 28 | 30 | .map(|x| x.file) | |
| 29 | 31 | .collect(); | |
| 30 | − | let (elapsed, total) = conn.status().unwrap().time.unwrap_or_default(); | |
| 31 | − | let volume: u8 = conn.status().unwrap_or_default().volume as u8; | |
| 32 | + | let status = conn.status().unwrap(); | |
| 33 | + | let (elapsed, total) = status.time.unwrap_or_default(); | |
| 34 | + | let volume: u8 = status.volume as u8; | |
| 35 | + | let repeat = status.repeat; | |
| 36 | + | let random = status.random; | |
| 32 | 37 | ||
| 33 | 38 | Ok(Self { | |
| 34 | 39 | conn, | |
| @@ -37,6 +42,8 @@ impl Connection { | |||
| 37 | 42 | elapsed, | |
| 38 | 43 | total_duration: total, | |
| 39 | 44 | volume, | |
| 45 | + | repeat, | |
| 46 | + | random, | |
| 40 | 47 | }) | |
| 41 | 48 | } | |
| 42 | 49 | ||
| @@ -64,25 +71,33 @@ impl Connection { | |||
| 64 | 71 | Ok(()) | |
| 65 | 72 | } | |
| 66 | 73 | ||
| 67 | − | pub fn update_state(&mut self) -> String { | |
| 68 | − | match self.conn.status().unwrap().state { | |
| 74 | + | /// Update status | |
| 75 | + | pub fn update_status(&mut self) { | |
| 76 | + | let status = self.conn.status().unwrap(); | |
| 77 | + | ||
| 78 | + | // Playback State | |
| 79 | + | match status.state { | |
| 69 | 80 | State::Stop => self.state = "Stopped".to_string(), | |
| 70 | 81 | State::Play => self.state = "Playing".to_string(), | |
| 71 | 82 | State::Pause => self.state = "Paused".to_string(), | |
| 72 | 83 | } | |
| 73 | − | self.state.clone() | |
| 74 | − | } | |
| 75 | 84 | ||
| 76 | − | pub fn update_progress(&mut self) { | |
| 77 | − | let (elapsed, total) = self.conn.status().unwrap().time.unwrap_or_default(); | |
| 85 | + | // Progress | |
| 86 | + | let (elapsed, total) = status.time.unwrap_or_default(); | |
| 78 | 87 | self.elapsed = elapsed; | |
| 79 | 88 | self.total_duration = total; | |
| 80 | − | } | |
| 81 | 89 | ||
| 82 | − | pub fn update_volume(&mut self) { | |
| 83 | − | self.volume = self.conn.status().unwrap_or_default().volume as u8; | |
| 90 | + | // Volume | |
| 91 | + | self.volume = status.volume as u8; | |
| 92 | + | ||
| 93 | + | // Repeat mode | |
| 94 | + | self.repeat = status.repeat; | |
| 95 | + | ||
| 96 | + | // Random mode | |
| 97 | + | self.random = status.random; | |
| 84 | 98 | } | |
| 85 | 99 | ||
| 100 | + | /// Get progress ratio of current playing song | |
| 86 | 101 | pub fn get_progress_ratio(&self) -> f64 { | |
| 87 | 102 | let total = self.total_duration.as_secs_f64(); | |
| 88 | 103 | if total == 0.0 { | |
| @@ -143,11 +158,6 @@ impl Connection { | |||
| 143 | 158 | } | |
| 144 | 159 | } | |
| 145 | 160 | ||
| 146 | − | /// get current playing song | |
| 147 | − | pub fn get_current_song(&mut self) -> Option<String> { | |
| 148 | − | self.conn.currentsong().unwrap().unwrap_or_default().title | |
| 149 | − | } | |
| 150 | − | ||
| 151 | 161 | /// Print status to stdout | |
| 152 | 162 | pub fn status(&mut self) { | |
| 153 | 163 | let current_song = self.conn.currentsong(); | |
| @@ -170,7 +180,7 @@ impl Connection { | |||
| 170 | 180 | let song = self.conn.currentsong()?.unwrap_or_default(); | |
| 171 | 181 | if let Some(s) = song.title { | |
| 172 | 182 | if let Some(a) = song.artist { | |
| 173 | − | return Ok(Some(format!("\"{}\" By {}", a, s))); | |
| 183 | + | return Ok(Some(format!("\"{}\" By {}", s, a))); | |
| 174 | 184 | } else { | |
| 175 | 185 | return Ok(Some(s)); | |
| 176 | 186 | } | |
| @@ -190,7 +200,26 @@ impl Connection { | |||
| 190 | 200 | self.conn.toggle_pause().unwrap(); | |
| 191 | 201 | } | |
| 192 | 202 | ||
| 203 | + | /// Toggle Repeat mode | |
| 204 | + | pub fn toggle_repeat(&mut self) { | |
| 205 | + | if self.conn.status().unwrap().repeat { | |
| 206 | + | self.conn.repeat(false).unwrap(); | |
| 207 | + | } else { | |
| 208 | + | self.conn.repeat(true).unwrap(); | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | /// Toggle random mode | |
| 213 | + | pub fn toggle_random(&mut self) { | |
| 214 | + | if self.conn.status().unwrap().random { | |
| 215 | + | self.conn.random(false).unwrap(); | |
| 216 | + | } else { | |
| 217 | + | self.conn.random(true).unwrap(); | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | ||
| 193 | 221 | // Volume controls | |
| 222 | + | /// Increase Volume | |
| 194 | 223 | pub fn inc_volume(&mut self, v: i8) { | |
| 195 | 224 | let cur = self.conn.status().unwrap().volume; | |
| 196 | 225 | if cur + v <= 100 { | |
| @@ -198,6 +227,7 @@ impl Connection { | |||
| 198 | 227 | } | |
| 199 | 228 | } | |
| 200 | 229 | ||
| 230 | + | /// Decrease volume | |
| 201 | 231 | pub fn dec_volume(&mut self, v: i8) { | |
| 202 | 232 | let cur = self.conn.status().unwrap().volume; | |
| 203 | 233 | if cur - v >= 0 { | |
| @@ -13,6 +13,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 13 | 13 | app.quit(); | |
| 14 | 14 | } else { | |
| 15 | 15 | app.conn.conn.clear()?; | |
| 16 | + | app.conn.update_status(); | |
| 16 | 17 | } | |
| 17 | 18 | } | |
| 18 | 19 | ||
| @@ -47,6 +48,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 47 | 48 | .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; | |
| 48 | 49 | } | |
| 49 | 50 | } | |
| 51 | + | app.conn.update_status(); | |
| 50 | 52 | } | |
| 51 | 53 | ||
| 52 | 54 | KeyCode::Char('h') => match app.selected_tab { | |
| @@ -68,6 +70,18 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 68 | 70 | app.conn.pause(); | |
| 69 | 71 | } | |
| 70 | 72 | ||
| 73 | + | // Toggle rpeat | |
| 74 | + | KeyCode::Char('r') => { | |
| 75 | + | app.conn.toggle_repeat(); | |
| 76 | + | app.conn.update_status(); | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Toggle random | |
| 80 | + | KeyCode::Char('z') => { | |
| 81 | + | app.conn.toggle_random(); | |
| 82 | + | app.conn.update_status(); | |
| 83 | + | } | |
| 84 | + | ||
| 71 | 85 | // Dmenu prompt | |
| 72 | 86 | KeyCode::Char('D') => { | |
| 73 | 87 | app.conn.play_dmenu()?; | |
| @@ -75,9 +89,23 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 75 | 89 | ||
| 76 | 90 | // add to queue | |
| 77 | 91 | KeyCode::Char('a') => { | |
| 78 | − | let song = app.conn.get_song_with_only_filename( | |
| 79 | − | app.conn.songs_filenames.get(app.song_list.index).unwrap(), | |
| 80 | − | ); | |
| 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 | + | ||
| 81 | 109 | app.conn.conn.push(&song)?; | |
| 82 | 110 | } | |
| 83 | 111 | ||
| @@ -127,16 +155,22 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 127 | 155 | // Volume controls | |
| 128 | 156 | KeyCode::Char('=') => { | |
| 129 | 157 | app.conn.inc_volume(2); | |
| 130 | − | app.conn.update_volume(); | |
| 158 | + | app.conn.update_status(); | |
| 131 | 159 | } | |
| 132 | 160 | ||
| 133 | 161 | KeyCode::Char('-') => { | |
| 134 | 162 | app.conn.dec_volume(2); | |
| 135 | − | app.conn.update_volume(); | |
| 163 | + | app.conn.update_status(); | |
| 136 | 164 | } | |
| 137 | 165 | ||
| 138 | 166 | // Delete highlighted song from the queue | |
| 139 | 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 | + | } | |
| 172 | + | } | |
| 173 | + | ||
| 140 | 174 | app.conn.conn.delete(app.queue_list.index as u32)?; | |
| 141 | 175 | app.update_queue(); | |
| 142 | 176 | } | |
| @@ -13,6 +13,9 @@ pub mod tui; | |||
| 13 | 13 | /// Content list | |
| 14 | 14 | pub mod list; | |
| 15 | 15 | ||
| 16 | + | /// Song | |
| 17 | + | pub mod song; | |
| 18 | + | ||
| 16 | 19 | /// File Browser | |
| 17 | 20 | pub mod browser; | |
| 18 | 21 | ||
| @@ -18,18 +18,24 @@ impl<T> ContentList<T> { | |||
| 18 | 18 | // self.index += 1; | |
| 19 | 19 | // } | |
| 20 | 20 | ||
| 21 | − | if self.index == self.list.len() - 1 { | |
| 22 | − | self.index = 0; | |
| 23 | − | } else { | |
| 24 | − | self.index += 1; | |
| 21 | + | let len = self.list.len(); | |
| 22 | + | if len != 0 { | |
| 23 | + | if self.index == self.list.len() - 1 { | |
| 24 | + | self.index = 0; | |
| 25 | + | } else { | |
| 26 | + | self.index += 1; | |
| 27 | + | } | |
| 25 | 28 | } | |
| 26 | 29 | } | |
| 27 | 30 | ||
| 28 | 31 | /// Go to previous item in list | |
| 29 | 32 | pub fn prev(&mut self) { | |
| 30 | 33 | if self.index == 0 { | |
| 31 | − | self.index = self.list.len() - 1; | |
| 32 | − | } else { | |
| 34 | + | let len = self.list.len(); | |
| 35 | + | if len != 0 { | |
| 36 | + | self.index = len - 1; | |
| 37 | + | } | |
| 38 | + | } else { | |
| 33 | 39 | self.index -= 1; | |
| 34 | 40 | } | |
| 35 | 41 | } | |
| @@ -9,6 +9,7 @@ use rmptui::connection::Connection; | |||
| 9 | 9 | use rmptui::event::Event; | |
| 10 | 10 | use rmptui::event::EventHandler; | |
| 11 | 11 | use rmptui::handler; | |
| 12 | + | use rmptui::song::RSong; | |
| 12 | 13 | use rmptui::tui; | |
| 13 | 14 | use std::io; | |
| 14 | 15 | ||
| @@ -23,41 +24,52 @@ pub type Result<T> = core::result::Result<T, Error>; | |||
| 23 | 24 | pub type Error = Box<dyn std::error::Error>; | |
| 24 | 25 | ||
| 25 | 26 | fn main() -> AppResult<()> { | |
| 26 | − | // let args = Args::parse(); | |
| 27 | − | ||
| 28 | − | // if args.no_tui { | |
| 29 | − | // handle_tui()?; | |
| 30 | − | // } else { | |
| 31 | − | // match args.command { | |
| 32 | − | // Command::Volume { vol } => { | |
| 33 | − | // conn.set_volume(vol); | |
| 34 | − | // } | |
| 35 | − | // Command::Dmenu => conn.play_dmenu().unwrap(), | |
| 36 | − | // Command::Fzf => conn.play_fzf().unwrap(), | |
| 37 | − | // Command::Status => conn.status(), | |
| 38 | − | // Command::Pause => conn.pause(), | |
| 39 | − | // Command::Toggle => conn.toggle_pause(), | |
| 40 | − | // }; | |
| 41 | − | // } | |
| 42 | − | ||
| 43 | − | let backend = CrosstermBackend::new(io::stderr()); | |
| 44 | − | let terminal = Terminal::new(backend)?; | |
| 27 | + | let args = Args::parse(); | |
| 45 | 28 | let mut app = App::builder("127.0.0.1:6600")?; | |
| 46 | − | let events = EventHandler::new(250); | |
| 29 | + | ||
| 30 | + | if !args.tui { | |
| 31 | + | handle_tui(&mut app)?; | |
| 32 | + | } else { | |
| 33 | + | match args.command { | |
| 34 | + | Some(Command::Dmenu) => app.conn.play_dmenu()?, | |
| 35 | + | Some(Command::Fzf) => app.conn.play_fzf().unwrap(), | |
| 36 | + | Some(Command::Status) => app.conn.status(), | |
| 37 | + | Some(Command::Pause) => app.conn.pause(), | |
| 38 | + | Some(Command::Toggle) => app.conn.toggle_pause(), | |
| 39 | + | _ => { | |
| 40 | + | let mut vec: Vec<RSong> = Vec::new(); | |
| 41 | + | for filename in app.conn.songs_filenames { | |
| 42 | + | let song = RSong::new(&mut app.conn.conn, filename); | |
| 43 | + | vec.push(song); | |
| 44 | + | } | |
| 45 | + | println!("{:#?}", vec); | |
| 46 | + | ||
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | Ok(()) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | pub fn handle_tui(app: &mut App) -> AppResult<()> { | |
| 54 | + | let backend = CrosstermBackend::new(io::stderr()); | |
| 55 | + | let terminal = Terminal::new(backend)?; | |
| 56 | + | let events = EventHandler::new(1000); | |
| 47 | 57 | ||
| 48 | 58 | let mut tui = tui::Tui::new(terminal, events); | |
| 49 | 59 | tui.init()?; | |
| 50 | 60 | ||
| 61 | + | // update the directory | |
| 62 | + | app.browser.update_directory(&mut app.conn).unwrap(); | |
| 63 | + | ||
| 51 | 64 | while app.running { | |
| 52 | − | tui.draw(&mut app)?; | |
| 65 | + | tui.draw(app)?; | |
| 53 | 66 | match tui.events.next()? { | |
| 54 | 67 | Event::Tick => app.tick(), | |
| 55 | − | Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?, | |
| 68 | + | Event::Key(key_event) => handler::handle_key_events(key_event, app)?, | |
| 56 | 69 | Event::Mouse(_) => {} | |
| 57 | 70 | Event::Resize(_, _) => {} | |
| 58 | 71 | } | |
| 59 | 72 | } | |
| 60 | 73 | ||
| 61 | − | ||
| 62 | 74 | Ok(()) | |
| 63 | 75 | } | |
| @@ -0,0 +1,67 @@ | |||
| 1 | + | use mpd::{Client, Song}; | |
| 2 | + | ||
| 3 | + | #[derive(Debug)] | |
| 4 | + | #[derive(Clone )] | |
| 5 | + | pub struct RSong { | |
| 6 | + | pub file: String, | |
| 7 | + | pub artist: Option<String>, | |
| 8 | + | pub title: Option<String>, | |
| 9 | + | pub duration: Option<u32>, | |
| 10 | + | pub last_mod: Option<String>, | |
| 11 | + | pub name: Option<String>, | |
| 12 | + | pub place: Option<String>, | |
| 13 | + | pub range: Option<String>, | |
| 14 | + | pub tags: Vec<(String, String)>, | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | impl RSong { | |
| 18 | + | pub fn new(c: &mut Client, filename: String) -> Self { | |
| 19 | + | let mut s = RSong { | |
| 20 | + | file: filename.clone(), | |
| 21 | + | artist: None, | |
| 22 | + | title: None, | |
| 23 | + | duration: None, | |
| 24 | + | last_mod: None, | |
| 25 | + | name: None, | |
| 26 | + | place: None, | |
| 27 | + | range: None, | |
| 28 | + | tags: vec![], | |
| 29 | + | }; | |
| 30 | + | ||
| 31 | + | // Dummy song | |
| 32 | + | ||
| 33 | + | let song = Song { | |
| 34 | + | file: filename.clone(), | |
| 35 | + | artist: None, | |
| 36 | + | title: None, | |
| 37 | + | duration: None, | |
| 38 | + | last_mod: None, | |
| 39 | + | name: None, | |
| 40 | + | place: None, | |
| 41 | + | range: None, | |
| 42 | + | tags: vec![("".to_string(), "".to_string())], | |
| 43 | + | }; | |
| 44 | + | ||
| 45 | + | for (k, v) in (c.readcomments(song).unwrap()).flatten() { | |
| 46 | + | if k.to_lowercase().contains("artist") { | |
| 47 | + | s.artist = Some(v); | |
| 48 | + | } else if k.to_lowercase().contains("title") { | |
| 49 | + | s.title = Some(v); | |
| 50 | + | } else if k.to_lowercase().contains("duration") { | |
| 51 | + | s.duration = Some(v.parse::<u32>().unwrap()); | |
| 52 | + | } else if k.to_lowercase().contains("lastmod") { | |
| 53 | + | s.last_mod = Some(v); | |
| 54 | + | } else if k.to_lowercase().contains("name") { | |
| 55 | + | s.name = Some(v); | |
| 56 | + | } else if k.to_lowercase().contains("place") { | |
| 57 | + | s.place = Some(v); | |
| 58 | + | } else if k.to_lowercase().contains("range") { | |
| 59 | + | s.range = Some(v); | |
| 60 | + | } else { | |
| 61 | + | s.tags.push((k, v)); | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | s | |
| 66 | + | } | |
| 67 | + | } | |
| @@ -14,7 +14,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 14 | 14 | // Layout | |
| 15 | 15 | let layout = Layout::default() | |
| 16 | 16 | .direction(Direction::Vertical) | |
| 17 | − | .constraints(vec![Constraint::Percentage(93), Constraint::Percentage(7)]) | |
| 17 | + | .constraints(vec![Constraint::Percentage(93), Constraint::Min(3)]) | |
| 18 | 18 | .split(frame.size()); | |
| 19 | 19 | ||
| 20 | 20 | match app.selected_tab { | |
| @@ -52,7 +52,12 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 52 | 52 | ) | |
| 53 | 53 | .borders(Borders::ALL), | |
| 54 | 54 | ) | |
| 55 | − | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| 55 | + | .highlight_style( | |
| 56 | + | Style::new() | |
| 57 | + | .fg(Color::Cyan) | |
| 58 | + | .bg(Color::Black) | |
| 59 | + | .add_modifier(Modifier::REVERSED), | |
| 60 | + | ) | |
| 56 | 61 | .highlight_symbol(">>") | |
| 57 | 62 | .repeat_highlight_symbol(true) | |
| 58 | 63 | .scroll_padding(20); | |
| @@ -72,7 +77,12 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 72 | 77 | ); | |
| 73 | 78 | let list = List::new(app.queue_list.list.clone()) | |
| 74 | 79 | .block(title.borders(Borders::ALL)) | |
| 75 | − | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| 80 | + | .highlight_style( | |
| 81 | + | Style::new() | |
| 82 | + | .fg(Color::Cyan) | |
| 83 | + | .bg(Color::Black) | |
| 84 | + | .add_modifier(Modifier::REVERSED), | |
| 85 | + | ) | |
| 76 | 86 | .highlight_symbol(">>") | |
| 77 | 87 | .repeat_highlight_symbol(true); | |
| 78 | 88 | ||
| @@ -93,7 +103,12 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 93 | 103 | ||
| 94 | 104 | let list = List::new(app.pl_list.list.clone()) | |
| 95 | 105 | .block(title.borders(Borders::ALL)) | |
| 96 | − | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| 106 | + | .highlight_style( | |
| 107 | + | Style::new() | |
| 108 | + | .fg(Color::Cyan) | |
| 109 | + | .bg(Color::Black) | |
| 110 | + | .add_modifier(Modifier::REVERSED), | |
| 111 | + | ) | |
| 97 | 112 | .highlight_symbol(">>") | |
| 98 | 113 | .repeat_highlight_symbol(true); | |
| 99 | 114 | ||
| @@ -103,39 +118,65 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 103 | 118 | ||
| 104 | 119 | /// Draws Progress Bar | |
| 105 | 120 | fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 121 | + | // Get the current playing song | |
| 106 | 122 | let song = app | |
| 107 | 123 | .conn | |
| 108 | 124 | .now_playing() | |
| 109 | 125 | .unwrap() | |
| 110 | 126 | .unwrap_or_else(|| "No Title Found".to_string()); | |
| 111 | 127 | ||
| 112 | − | let state = &app.conn.state; | |
| 113 | − | // let (elapsed, total) = app.conn.conn.status().unwrap().time.unwrap_or_default(); | |
| 128 | + | // Get the current playing state | |
| 129 | + | let mut state: String = String::new(); | |
| 130 | + | if !app.queue_list.list.is_empty() { | |
| 131 | + | state = app.conn.state.clone(); | |
| 132 | + | state.push(':'); | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // Get the current modes | |
| 136 | + | let mut modes_bottom: String = String::new(); | |
| 137 | + | // we do this to check if at least one mode is enabled so we can push "[]" | |
| 138 | + | if app.conn.repeat | app.conn.random { | |
| 139 | + | modes_bottom.push('r'); | |
| 140 | + | } | |
| 114 | 141 | ||
| 142 | + | if !modes_bottom.is_empty() { | |
| 143 | + | modes_bottom.clear(); | |
| 144 | + | modes_bottom.push('['); | |
| 145 | + | if app.conn.repeat { | |
| 146 | + | modes_bottom.push('r'); | |
| 147 | + | } | |
| 148 | + | if app.conn.random { | |
| 149 | + | modes_bottom.push('z'); | |
| 150 | + | } | |
| 151 | + | modes_bottom.push(']'); | |
| 152 | + | }; | |
| 153 | + | ||
| 154 | + | ||
| 155 | + | // get the duration | |
| 156 | + | 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 | + | ) | |
| 162 | + | } else { | |
| 163 | + | "".to_string() | |
| 164 | + | }; | |
| 165 | + | ||
| 166 | + | // Define the title | |
| 115 | 167 | let title = Block::default() | |
| 116 | − | .title(Title::from(format!("{}: ", state).red().bold())) | |
| 168 | + | .title(Title::from(format!("{}", state).red().bold())) | |
| 117 | 169 | .title(Title::from(song.green().bold())) | |
| 118 | − | .title( | |
| 119 | − | Title::from( | |
| 120 | − | format!( | |
| 121 | − | "{}/{}", | |
| 122 | − | app.conn.elapsed.as_secs(), | |
| 123 | − | app.conn.total_duration.as_secs() | |
| 124 | − | ) | |
| 125 | − | .cyan() | |
| 126 | − | .bold(), | |
| 127 | − | ) | |
| 128 | − | .alignment(Alignment::Right), | |
| 129 | − | ) | |
| 170 | + | .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right)) | |
| 171 | + | .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom)) | |
| 130 | 172 | .borders(Borders::ALL); | |
| 131 | 173 | ||
| 132 | − | // .title(Title::from(app.conn.conn.status().unwrap_or_default().volume.to_string().yellow())).title_alignment(Alignment::Right); | |
| 133 | 174 | let progress_bar = LineGauge::default() | |
| 134 | 175 | .block(title.borders(Borders::ALL)) | |
| 135 | 176 | .gauge_style( | |
| 136 | 177 | Style::default() | |
| 137 | − | .fg(Color::LightBlue) | |
| 138 | − | .bg(Color::Gray) | |
| 178 | + | .fg(Color::Blue) | |
| 179 | + | .bg(Color::Black) | |
| 139 | 180 | .add_modifier(Modifier::BOLD), | |
| 140 | 181 | ) | |
| 141 | 182 | .line_set(symbols::line::THICK) | |