From 4ea1e9c47c8b85069592b2325d28c4bfc9bf5552 Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Thu, 10 Sep 2026 10:28:22 +0200 Subject: [PATCH] feat(tui): clear the whole queue from the transfers pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shift-C in the Downloads/Uploads pane now cancels every live download, forgets every row — queued, finished or failed — in the session too, and cancels uploads still streaming, so a shared queue empties for every window at once. The existing c keeps clearing only finished transfers. --- soulseek-rs/src/ui/main_tui/downloads.rs | 25 +++++ soulseek-rs/src/ui/main_tui/input.rs | 3 + soulseek-rs/src/ui/main_tui/mod.rs | 122 +++++++++++++++++++---- soulseek-rs/src/ui/main_tui/render.rs | 2 + 4 files changed, 135 insertions(+), 17 deletions(-) diff --git a/soulseek-rs/src/ui/main_tui/downloads.rs b/soulseek-rs/src/ui/main_tui/downloads.rs index 63b3239..8e650a0 100644 --- a/soulseek-rs/src/ui/main_tui/downloads.rs +++ b/soulseek-rs/src/ui/main_tui/downloads.rs @@ -89,6 +89,31 @@ impl MainTui { } } + /// Empty the whole transfer list: every live download is cancelled and + /// every row — live or finished — is forgotten by the session too, so a + /// shared queue empties for every window rather than only this one. + /// Uploads still streaming are cancelled; finished ones are the + /// session's history and refresh back from it regardless. + pub(super) fn clear_all_transfers(&mut self) { + for entry in &self.state.downloads { + let download = &entry.download; + if !download.is_finished() { + self.client + .cancel_download(&download.username, &download.filename); + } + self.client + .remove_download(&download.username, &download.filename); + } + for upload in &self.state.uploads { + if upload.status == UploadStatus::InProgress { + self.client + .cancel_upload(&upload.username, &upload.filename); + } + } + self.state.downloads.clear(); + self.state.downloads_table_state.select(None); + } + pub(super) fn toggle_selected_download_pause(&self) { let Some(index) = self.state.downloads_table_state.selected() else { return; diff --git a/soulseek-rs/src/ui/main_tui/input.rs b/soulseek-rs/src/ui/main_tui/input.rs index 0bb26e1..84bdc5b 100644 --- a/soulseek-rs/src/ui/main_tui/input.rs +++ b/soulseek-rs/src/ui/main_tui/input.rs @@ -569,6 +569,9 @@ impl MainTui { KeyCode::Char('c') => { self.clear_finished_downloads(); } + KeyCode::Char('C') => { + self.clear_all_transfers(); + } _ => {} } } diff --git a/soulseek-rs/src/ui/main_tui/mod.rs b/soulseek-rs/src/ui/main_tui/mod.rs index f3544f7..5b35068 100644 --- a/soulseek-rs/src/ui/main_tui/mod.rs +++ b/soulseek-rs/src/ui/main_tui/mod.rs @@ -295,8 +295,9 @@ mod tests { download_dir_set: std::sync::Mutex>, /// What `set_shared_directories` was last asked for, if anything. shares_set: std::sync::Mutex>>, - download_cancelled: std::sync::Mutex>, - upload_cancelled: std::sync::Mutex>, + download_cancelled: std::sync::Mutex>, + upload_cancelled: std::sync::Mutex>, + download_removed: std::sync::Mutex>, } fn queued(username: &str, filename: &str) -> soulseek_rs::types::Download { @@ -449,8 +450,12 @@ mod tests { fn remove_queued_download(&self, _u: &str, _f: &str) -> bool { false } - fn remove_download(&self, _u: &str, _f: &str) -> bool { - false + fn remove_download(&self, u: &str, f: &str) -> bool { + self.download_removed + .lock() + .expect("not poisoned") + .push((u.to_string(), f.to_string())); + true } fn uploads(&self) -> Vec { Vec::new() @@ -459,13 +464,17 @@ mod tests { Vec::new() } fn cancel_upload(&self, u: &str, f: &str) -> bool { - *self.upload_cancelled.lock().expect("not poisoned") = - Some((u.to_string(), f.to_string())); + self.upload_cancelled + .lock() + .expect("not poisoned") + .push((u.to_string(), f.to_string())); true } fn cancel_download(&self, u: &str, f: &str) -> bool { - *self.download_cancelled.lock().expect("not poisoned") = - Some((u.to_string(), f.to_string())); + self.download_cancelled + .lock() + .expect("not poisoned") + .push((u.to_string(), f.to_string())); true } fn set_upload_slots(&self, _slots: usize) {} @@ -614,11 +623,70 @@ mod tests { assert_eq!( *session.download_cancelled.lock().expect("not poisoned"), - Some(("bob".to_string(), "song.mp3".to_string())) + vec![("bob".to_string(), "song.mp3".to_string())] + ); + assert!( + session + .upload_cancelled + .lock() + .expect("not poisoned") + .is_empty(), + "the download row cancels no upload" + ); + } + + #[test] + fn shift_c_clears_the_whole_transfer_list() { + let session = Arc::new(TalkativeSession::default()); + let mut tui = attach(session.clone()); + tui.state.downloads.push(crate::models::DownloadEntry { + download: queued("bob", "waiting.mp3"), + receiver: None, + }); + tui.state.downloads.push(crate::models::DownloadEntry { + download: at_status( + "carol", + "live.mp3", + soulseek_rs::DownloadStatus::InProgress { + bytes_downloaded: 1, + total_bytes: 4096, + speed_bytes_per_sec: 1.0, + }, + ), + receiver: None, + }); + tui.state.downloads.push(crate::models::DownloadEntry { + download: completed("dave", "done.mp3"), + receiver: None, + }); + tui.state.uploads.push(uploading("alice", "served.mp3")); + tui.state.downloads_table_state.select(Some(0)); + tui.state.focused_pane = FocusedPane::Downloads; + + tui.handle_key_event(key('C')); + + assert!(tui.state.downloads.is_empty(), "every row goes"); + assert_eq!( + *session.download_cancelled.lock().expect("not poisoned"), + vec![ + ("bob".to_string(), "waiting.mp3".to_string()), + ("carol".to_string(), "live.mp3".to_string()), + ], + "live downloads are cancelled; the finished one is not" + ); + assert_eq!( + *session.download_removed.lock().expect("not poisoned"), + vec![ + ("bob".to_string(), "waiting.mp3".to_string()), + ("carol".to_string(), "live.mp3".to_string()), + ("dave".to_string(), "done.mp3".to_string()), + ], + "the session forgets every download, not just this window's rows" ); assert_eq!( *session.upload_cancelled.lock().expect("not poisoned"), - None + vec![("alice".to_string(), "served.mp3".to_string())], + "a streaming upload is cancelled too" ); } @@ -654,9 +722,13 @@ mod tests { tui.handle_key_event(key('x')); - assert_eq!( - *session.download_cancelled.lock().expect("not poisoned"), - None + assert!( + session + .download_cancelled + .lock() + .expect("not poisoned") + .is_empty(), + "a finished download is not cancelled" ); } @@ -676,11 +748,15 @@ mod tests { assert_eq!( *session.upload_cancelled.lock().expect("not poisoned"), - Some(("alice".to_string(), "served.mp3".to_string())) + vec![("alice".to_string(), "served.mp3".to_string())] ); - assert_eq!( - *session.download_cancelled.lock().expect("not poisoned"), - None + assert!( + session + .download_cancelled + .lock() + .expect("not poisoned") + .is_empty(), + "the upload row cancels no download" ); } @@ -2042,6 +2118,18 @@ mod tests { assert!(screen.contains("run the search again"), "{screen}"); } + #[test] + fn the_downloads_pane_lists_its_clear_everything_key() { + let mut tui = furnished_tui(); + tui.state.focused_pane = FocusedPane::Downloads; + let screen = screen_sized(&mut tui, 300, 40); + assert!(screen.contains("[c → clear finished]"), "{screen}"); + assert!(screen.contains("[C → clear all]"), "{screen}"); + press(&mut tui, KeyCode::Char('?')); + let screen = screen_of(&mut tui); + assert!(screen.contains("cancelling the live ones"), "{screen}"); + } + #[test] fn shift_w_brings_every_pane_back_and_leaves_zoom() { let mut tui = furnished_tui(); diff --git a/soulseek-rs/src/ui/main_tui/render.rs b/soulseek-rs/src/ui/main_tui/render.rs index c99ed49..fdd6aa7 100644 --- a/soulseek-rs/src/ui/main_tui/render.rs +++ b/soulseek-rs/src/ui/main_tui/render.rs @@ -89,6 +89,7 @@ const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[ ("r", "retry a failed one"), ("d", "delete a queued or finished one"), ("c", "clear every finished one"), + ("C", "clear every one, cancelling the live ones"), ("b", "browse that user"), ], ), @@ -702,6 +703,7 @@ impl MainTui { ("r", "retry failed"), ("d", "delete queued/done"), ("c", "clear finished"), + ("C", "clear all"), ("b", "browse user"), ("h/l", "scroll name"), ],