From 368f7505ff45ed0e4a77f93b5c22e41fc8634e6f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 19 Dec 2025 13:41:14 -0700 Subject: [PATCH] feat: Improved UI speed and responsiveness --- src/app/app_tests.rs | 22 +++++++++++++++++++ src/app/mod.rs | 10 +++++++++ src/event/input_event.rs | 8 +++++-- src/main.rs | 5 +++-- src/ui/mod.rs | 9 ++++---- src/ui/radarr_ui/blocklist/mod.rs | 2 +- .../collections/collection_details_ui.rs | 2 +- src/ui/radarr_ui/collections/mod.rs | 2 +- src/ui/radarr_ui/downloads/mod.rs | 2 +- .../indexers/test_all_indexers_ui.rs | 2 +- src/ui/radarr_ui/library/add_movie_ui.rs | 2 +- src/ui/radarr_ui/library/mod.rs | 2 +- src/ui/radarr_ui/library/movie_details_ui.rs | 4 ++-- src/ui/sonarr_ui/downloads/mod.rs | 2 +- src/ui/sonarr_ui/history/mod.rs | 2 +- .../indexers/test_all_indexers_ui.rs | 2 +- src/ui/sonarr_ui/library/add_series_ui.rs | 2 +- .../sonarr_ui/library/episode_details_ui.rs | 4 ++-- src/ui/sonarr_ui/library/mod.rs | 2 +- src/ui/sonarr_ui/library/season_details_ui.rs | 4 ++-- src/ui/sonarr_ui/library/series_details_ui.rs | 2 +- 21 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index b9a8a85..5b85e37 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -80,6 +80,7 @@ mod tests { assert_eq!(app.tick_until_poll, 400); assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.tick_count, 0); + assert_eq!(app.ui_scroll_tick_count, 0); assert!(!app.is_loading); assert!(!app.is_routing); assert!(!app.should_refresh); @@ -240,6 +241,27 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[test] + fn test_on_ui_scroll_tick() { + let mut app = App { + ticks_until_scroll: 1, + ..App::default() + }; + + assert_eq!(app.ui_scroll_tick_count, 0); + assert_eq!(app.tick_count, 0); + + app.on_ui_scroll_tick(); + + assert_eq!(app.ui_scroll_tick_count, 1); + assert_eq!(app.tick_count, 0); + + app.on_ui_scroll_tick(); + + assert_eq!(app.ui_scroll_tick_count, 0); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_on_tick_first_render() { let (sync_network_tx, mut sync_network_rx) = mpsc::channel::(500); diff --git a/src/app/mod.rs b/src/app/mod.rs index efb05b8..a8de430 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -39,6 +39,7 @@ pub struct App<'a> { pub tick_until_poll: u64, pub ticks_until_scroll: u64, pub tick_count: u64, + pub ui_scroll_tick_count: u64, pub is_routing: bool, pub is_loading: bool, pub should_refresh: bool, @@ -145,6 +146,14 @@ impl App<'_> { self.tick_count = 0; } + pub fn on_ui_scroll_tick(&mut self) { + if self.ui_scroll_tick_count == self.ticks_until_scroll { + self.ui_scroll_tick_count = 0; + } else { + self.ui_scroll_tick_count += 1; + } + } + #[allow(dead_code)] pub fn reset(&mut self) { self.reset_tick_count(); @@ -227,6 +236,7 @@ impl Default for App<'_> { tick_until_poll: 400, ticks_until_scroll: 4, tick_count: 0, + ui_scroll_tick_count: 0, is_loading: false, is_routing: false, should_refresh: false, diff --git a/src/event/input_event.rs b/src/event/input_event.rs index cfded37..67e5897 100644 --- a/src/event/input_event.rs +++ b/src/event/input_event.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread; @@ -49,7 +50,10 @@ impl Events { Events { rx } } - pub fn next(&self) -> Result, mpsc::RecvError> { - self.rx.recv() + pub fn next(&self) -> Result>> { + match self.rx.try_recv() { + Ok(event) => Ok(Some(event)), + _ => Ok(None), + } } } diff --git a/src/main.rs b/src/main.rs index 02c8083..b611d22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -249,7 +249,7 @@ async fn start_ui( terminal.draw(|f| ui(f, &mut app))?; match input_events.next()? { - InputEvent::KeyEvent(key) => { + Some(InputEvent::KeyEvent(key)) => { if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input { break; } @@ -257,7 +257,8 @@ async fn start_ui( handlers::handle_events(key, &mut app); } - InputEvent::Tick => app.on_tick().await, + Some(InputEvent::Tick) => app.on_tick().await, + _ => {} } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 58f88dc..d974eee 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -52,6 +52,7 @@ pub trait DrawUi { } pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { + app.on_ui_scroll_tick(); f.render_widget(background_block(), f.area()); let [header_area, context_area, table_area] = if !app.error.text.is_empty() { let [header_area, error_area, context_area, table_area] = Layout::vertical([ @@ -124,11 +125,9 @@ fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .failure() .bold(); - app.error.scroll_left_or_reset( - area.width as usize, - true, - app.tick_count.is_multiple_of(app.ticks_until_scroll), - ); + app + .error + .scroll_left_or_reset(area.width as usize, true, app.ui_scroll_tick_count == 0); let paragraph = Paragraph::new(Text::from(app.error.to_string().failure())) .block(block) diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs index f4afeeb..4301221 100644 --- a/src/ui/radarr_ui/blocklist/mod.rs +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -96,7 +96,7 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 20), current_selection == *blocklist_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let languages_string = languages diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index f794e43..9567523 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -90,7 +90,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) movie.title.scroll_left_or_reset( get_width_from_percentage(table_area, 20), current_selection == *movie, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let (hours, minutes) = convert_runtime(movie.runtime); let imdb_rating = movie diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 8cb7539..63ed8a8 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -70,7 +70,7 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) collection.title.scroll_left_or_reset( get_width_from_percentage(area, 25), *collection == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let monitored = if collection.monitored { "🏷" } else { "" }; let search_on_add = if collection.search_on_add { diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index 30db8c2..2e52d7b 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -87,7 +87,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { output_path.as_ref().unwrap().scroll_left_or_reset( get_width_from_percentage(area, 18), current_selection == *download_record, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); } diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index c4ec021..734fc15 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -45,7 +45,7 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), *result == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let pass_fail = if result.is_valid { "✔" } else { "❌" }; let row = Row::new(vec![ diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index 65f27e4..f3ad937 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -139,7 +139,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *movie == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index 7260837..2ba46ae 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -90,7 +90,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *movie == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let monitored = if movie.monitored { "🏷" } else { "" }; let studio = movie.studio.clone().unwrap_or_default(); diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index bdc2566..7823d2b 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -246,7 +246,7 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie_history_item.source_title.scroll_left_or_reset( get_width_from_percentage(area, 34), current_selection == *movie_history_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -398,7 +398,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "⛔" } else { "" }; diff --git a/src/ui/sonarr_ui/downloads/mod.rs b/src/ui/sonarr_ui/downloads/mod.rs index 3064827..5706bd4 100644 --- a/src/ui/sonarr_ui/downloads/mod.rs +++ b/src/ui/sonarr_ui/downloads/mod.rs @@ -88,7 +88,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { output_path.as_ref().unwrap().scroll_left_or_reset( get_width_from_percentage(area, 18), current_selection == *download_record, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); } diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index 20747ea..05c00ef 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -69,7 +69,7 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs index f7256ec..8b0bf20 100644 --- a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs @@ -44,7 +44,7 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), *result == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let pass_fail = if result.is_valid { "✔" } else { "❌" }; let row = Row::new(vec![ diff --git a/src/ui/sonarr_ui/library/add_series_ui.rs b/src/ui/sonarr_ui/library/add_series_ui.rs index a59e282..6d09c02 100644 --- a/src/ui/sonarr_ui/library/add_series_ui.rs +++ b/src/ui/sonarr_ui/library/add_series_ui.rs @@ -119,7 +119,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { series.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *series == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index af07c2f..f5af16a 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -281,7 +281,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -431,7 +431,7 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "⛔" } else { "" }; diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs index cb7c6a9..19569ef 100644 --- a/src/ui/sonarr_ui/library/mod.rs +++ b/src/ui/sonarr_ui/library/mod.rs @@ -95,7 +95,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { series.title.scroll_left_or_reset( get_width_from_percentage(area, 23), *series == current_selection, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let monitored = if series.monitored { "🏷" } else { "" }; let certification = series.certification.clone().unwrap_or_default(); diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index aea91bd..b676c6f 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -271,7 +271,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -382,7 +382,7 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "⛔" } else { "" }; diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index b395ee2..92364ae 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -315,7 +315,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.tick_count.is_multiple_of(app.ticks_until_scroll), + app.ui_scroll_tick_count == 0, ); Row::new(vec![