feat: Improved UI speed and responsiveness
Check / stable / fmt (push) Has been cancelled
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled

This commit is contained in:
2025-12-19 13:41:14 -07:00
parent 6a9fd0999c
commit 368f7505ff
21 changed files with 64 additions and 28 deletions
+22
View File
@@ -80,6 +80,7 @@ mod tests {
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.ui_scroll_tick_count, 0);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
@@ -240,6 +241,27 @@ mod tests {
assert_eq!(app.tick_count, 0); 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] #[tokio::test]
async fn test_on_tick_first_render() { async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500); let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
+10
View File
@@ -39,6 +39,7 @@ pub struct App<'a> {
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
pub tick_count: u64, pub tick_count: u64,
pub ui_scroll_tick_count: u64,
pub is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
@@ -145,6 +146,14 @@ impl App<'_> {
self.tick_count = 0; 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)] #[allow(dead_code)]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
@@ -227,6 +236,7 @@ impl Default for App<'_> {
tick_until_poll: 400, tick_until_poll: 400,
ticks_until_scroll: 4, ticks_until_scroll: 4,
tick_count: 0, tick_count: 0,
ui_scroll_tick_count: 0,
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
+6 -2
View File
@@ -1,3 +1,4 @@
use anyhow::Result;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::thread; use std::thread;
@@ -49,7 +50,10 @@ impl Events {
Events { rx } Events { rx }
} }
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> { pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
self.rx.recv() match self.rx.try_recv() {
Ok(event) => Ok(Some(event)),
_ => Ok(None),
}
} }
} }
+3 -2
View File
@@ -249,7 +249,7 @@ async fn start_ui(
terminal.draw(|f| ui(f, &mut app))?; terminal.draw(|f| ui(f, &mut app))?;
match input_events.next()? { match input_events.next()? {
InputEvent::KeyEvent(key) => { Some(InputEvent::KeyEvent(key)) => {
if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input { if key == Key::Char('q') && !app.ignore_special_keys_for_textbox_input {
break; break;
} }
@@ -257,7 +257,8 @@ async fn start_ui(
handlers::handle_events(key, &mut app); handlers::handle_events(key, &mut app);
} }
InputEvent::Tick => app.on_tick().await, Some(InputEvent::Tick) => app.on_tick().await,
_ => {}
} }
} }
+4 -5
View File
@@ -52,6 +52,7 @@ pub trait DrawUi {
} }
pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
app.on_ui_scroll_tick();
f.render_widget(background_block(), f.area()); f.render_widget(background_block(), f.area());
let [header_area, context_area, table_area] = if !app.error.text.is_empty() { let [header_area, context_area, table_area] = if !app.error.text.is_empty() {
let [header_area, error_area, context_area, table_area] = Layout::vertical([ 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() .failure()
.bold(); .bold();
app.error.scroll_left_or_reset( app
area.width as usize, .error
true, .scroll_left_or_reset(area.width as usize, true, app.ui_scroll_tick_count == 0);
app.tick_count.is_multiple_of(app.ticks_until_scroll),
);
let paragraph = Paragraph::new(Text::from(app.error.to_string().failure())) let paragraph = Paragraph::new(Text::from(app.error.to_string().failure()))
.block(block) .block(block)
+1 -1
View File
@@ -96,7 +96,7 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 20), get_width_from_percentage(area, 20),
current_selection == *blocklist_item, current_selection == *blocklist_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
let languages_string = languages let languages_string = languages
@@ -90,7 +90,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(table_area, 20), get_width_from_percentage(table_area, 20),
current_selection == *movie, 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 (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie let imdb_rating = movie
+1 -1
View File
@@ -70,7 +70,7 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
collection.title.scroll_left_or_reset( collection.title.scroll_left_or_reset(
get_width_from_percentage(area, 25), get_width_from_percentage(area, 25),
*collection == current_selection, *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 monitored = if collection.monitored { "🏷" } else { "" };
let search_on_add = if collection.search_on_add { let search_on_add = if collection.search_on_add {
+1 -1
View File
@@ -87,7 +87,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
output_path.as_ref().unwrap().scroll_left_or_reset( output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18), get_width_from_percentage(area, 18),
current_selection == *download_record, current_selection == *download_record,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
} }
@@ -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( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), get_width_from_percentage(area, 86),
*result == current_selection, *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 pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![ let row = Row::new(vec![
+1 -1
View File
@@ -139,7 +139,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*movie == current_selection, *movie == current_selection,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
+1 -1
View File
@@ -90,7 +90,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
movie.title.scroll_left_or_reset( movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*movie == current_selection, *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 monitored = if movie.monitored { "🏷" } else { "" };
let studio = movie.studio.clone().unwrap_or_default(); let studio = movie.studio.clone().unwrap_or_default();
+2 -2
View File
@@ -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( movie_history_item.source_title.scroll_left_or_reset(
get_width_from_percentage(area, 34), get_width_from_percentage(area, 34),
current_selection == *movie_history_item, current_selection == *movie_history_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ 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), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), && 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 size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
+1 -1
View File
@@ -88,7 +88,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
output_path.as_ref().unwrap().scroll_left_or_reset( output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18), get_width_from_percentage(area, 18),
current_selection == *download_record, current_selection == *download_record,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
} }
+1 -1
View File
@@ -69,7 +69,7 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -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( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), get_width_from_percentage(area, 86),
*result == current_selection, *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 pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![ let row = Row::new(vec![
+1 -1
View File
@@ -119,7 +119,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
series.title.scroll_left_or_reset( series.title.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*series == current_selection, *series == current_selection,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![
@@ -281,7 +281,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ 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), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, && 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 size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
+1 -1
View File
@@ -95,7 +95,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
series.title.scroll_left_or_reset( series.title.scroll_left_or_reset(
get_width_from_percentage(area, 23), get_width_from_percentage(area, 23),
*series == current_selection, *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 monitored = if series.monitored { "🏷" } else { "" };
let certification = series.certification.clone().unwrap_or_default(); let certification = series.certification.clone().unwrap_or_default();
@@ -271,7 +271,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ 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), get_width_from_percentage(area, 30),
current_selection == *release current_selection == *release
&& active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, && 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 size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -315,7 +315,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
source_title.scroll_left_or_reset( source_title.scroll_left_or_reset(
get_width_from_percentage(area, 40), get_width_from_percentage(area, 40),
current_selection == *history_item, current_selection == *history_item,
app.tick_count.is_multiple_of(app.ticks_until_scroll), app.ui_scroll_tick_count == 0,
); );
Row::new(vec![ Row::new(vec![