diff --git a/Cargo.lock b/Cargo.lock index 61b9ac5..4bda9d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1350,6 +1350,7 @@ dependencies = [ "ratatui", "rm-config", "serde", + "throbber-widgets-tui", "tokio", "tokio-util", "transmission-rpc", @@ -1650,6 +1651,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "throbber-widgets-tui" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431b847a60fc7b1df94a5b1bcb3cdd7027bb0013b431b08038505e8891c577a2" +dependencies = [ + "rand", + "ratatui", +] + [[package]] name = "tinystr" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index e508dfa..761fba5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ crossterm = { version = "0.27", features = ["event-stream"] } ratatui = { version = "0.26", features = ["serde"] } tui-input = "0.8" tui-tree-widget = "0.20" +throbber-widgets-tui = "0.5.0" # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index bf320bb..e6311ea 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -29,3 +29,5 @@ crossterm.workspace = true ratatui.workspace = true tui-input.workspace = true tui-tree-widget.workspace = true +throbber-widgets-tui.workspace = true + diff --git a/rm-main/src/action.rs b/rm-main/src/action.rs index 07a5d05..d3a8a76 100644 --- a/rm-main/src/action.rs +++ b/rm-main/src/action.rs @@ -8,6 +8,7 @@ pub(crate) enum Action { Quit, SoftQuit, Render, + Tick, Up, Down, Left, diff --git a/rm-main/src/app.rs b/rm-main/src/app.rs index 89f8620..15f1c6f 100644 --- a/rm-main/src/app.rs +++ b/rm-main/src/app.rs @@ -102,11 +102,17 @@ impl App { } async fn main_loop(&mut self, tui: &mut Tui) -> Result<()> { + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(250)); loop { let tui_event = tui.next(); let action = self.action_rx.recv(); + let tick_action = interval.tick(); tokio::select! { + _ = tick_action => { + self.ctx.action_tx.send(Action::Tick).unwrap(); + }, + event = tui_event => { if let Some(action) = event_to_action(self.mode, event.unwrap()) { if let Some(action) = self.update(action).await { diff --git a/rm-main/src/ui/components/mod.rs b/rm-main/src/ui/components/mod.rs index 39ee924..fb4eb91 100644 --- a/rm-main/src/ui/components/mod.rs +++ b/rm-main/src/ui/components/mod.rs @@ -12,4 +12,7 @@ pub trait Component { None } fn render(&mut self, _f: &mut Frame, _rect: Rect) {} + fn tick(&mut self) -> Option { + None + } } diff --git a/rm-main/src/ui/tabs/search.rs b/rm-main/src/ui/tabs/search.rs index b5a6f2b..96ff320 100644 --- a/rm-main/src/ui/tabs/search.rs +++ b/rm-main/src/ui/tabs/search.rs @@ -10,6 +10,7 @@ use ratatui::{ prelude::*, widgets::{Cell, Paragraph, Row, Table}, }; +use throbber_widgets_tui::ThrobberState; use tokio::sync::mpsc::{self, UnboundedSender}; use tui_input::Input; @@ -53,7 +54,10 @@ impl SearchTab { tokio::task::spawn(async move { let magnetease = Magnetease::new(); while let Some(search_phrase) = rx.recv().await { - search_result_info_clone.lock().unwrap().searching(); + search_result_info_clone + .lock() + .unwrap() + .searching(Arc::new(Mutex::new(ThrobberState::default()))); ctx_clone.send_action(Action::Render); let res = magnetease.search(&search_phrase).await; if res.is_empty() { @@ -193,11 +197,17 @@ impl Component for SearchTab { A::Home => self.scroll_to_home(), A::End => self.scroll_to_end(), A::Confirm => self.add_torrent(), + A::Tick => self.tick(), _ => None, } } + fn tick(&mut self) -> Option { + self.search_result_info.lock().unwrap().tick(); + Some(Action::Render) + } + fn render(&mut self, f: &mut Frame, rect: Rect) { let [top_line, rest, bottom_line] = Layout::vertical([ Constraint::Length(1), @@ -284,11 +294,11 @@ impl Component for SearchTab { } } -#[derive(Clone, Copy)] +#[derive(Clone)] enum SearchResultState { Nothing, NoResults, - Searching, + Searching(Arc>), Found(usize), } @@ -297,8 +307,8 @@ impl SearchResultState { Self::Nothing } - fn searching(&mut self) { - *self = Self::Searching; + fn searching(&mut self, state: Arc>) { + *self = Self::Searching(state); } fn not_found(&mut self) { @@ -314,8 +324,15 @@ impl Component for SearchResultState { fn render(&mut self, f: &mut Frame, rect: Rect) { match self { SearchResultState::Nothing => return, - SearchResultState::Searching => { - f.render_widget("󱗼 Searching...", rect); + SearchResultState::Searching(state) => { + let default_throbber = throbber_widgets_tui::Throbber::default() + .label("Searching...") + .style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)); + f.render_stateful_widget( + default_throbber.clone(), + rect, + &mut state.lock().unwrap(), + ); } SearchResultState::NoResults => { let mut line = Line::default(); @@ -333,4 +350,14 @@ impl Component for SearchResultState { } } } + + fn tick(&mut self) -> Option { + match self { + SearchResultState::Searching(state) => { + state.lock().unwrap().calc_next(); + Some(Action::Render) + } + _ => None, + } + } }