Merge pull request #63 from JMcCumberIO/fix/tui-idle-cpu-and-error-banners
Check / stable / fmt (push) Failing after 30s
Check / beta / clippy (push) Failing after 1m17s
Check / stable / clippy (push) Failing after 1m21s
Check / nightly / doc (push) Successful in 1m7s
Check / 1.95.0 / check (push) Successful in 1m9s
Test Suite / ubuntu / beta (push) Successful in 1m51s
Test Suite / ubuntu / stable (push) Successful in 1m52s
Test Suite / ubuntu / stable / coverage (push) Failing after 2m27s
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled

fix: reduce TUI idle CPU and improve server error banners
This commit is contained in:
Alex Clarke
2026-07-06 08:58:06 -06:00
committed by GitHub
32 changed files with 146 additions and 63 deletions
+11 -12
View File
@@ -5,6 +5,7 @@ mod tests {
use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value; use serde_json::Value;
use serial_test::serial; use serial_test::serial;
use std::time::{Duration, Instant};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars}; use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
@@ -80,9 +81,9 @@ mod tests {
assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.server_tabs.tabs, expected_tab_routes); assert_eq!(app.server_tabs.tabs, expected_tab_routes);
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 64); assert_eq!(app.scroll_interval, Duration::from_millis(100));
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.ui_scroll_tick_count, 0); assert!(!app.should_text_scroll);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
@@ -101,7 +102,7 @@ mod tests {
assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.error, HorizontallyScrollableText::default());
assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.index, 0);
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 64); assert_eq!(app.scroll_interval, Duration::from_millis(100));
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
@@ -247,22 +248,20 @@ mod tests {
#[test] #[test]
fn test_on_ui_scroll_tick() { fn test_on_ui_scroll_tick() {
let mut app = App { let mut app = App {
ticks_until_scroll: 1, scroll_interval: Duration::ZERO,
..App::default() ..App::default()
}; };
assert_eq!(app.ui_scroll_tick_count, 0); app.on_ui_scroll_tick();
assert_eq!(app.tick_count, 0); assert!(app.should_text_scroll);
app.on_ui_scroll_tick(); app.on_ui_scroll_tick();
assert!(app.should_text_scroll);
assert_eq!(app.ui_scroll_tick_count, 1); app.scroll_interval = Duration::from_secs(60);
assert_eq!(app.tick_count, 0); app.last_scroll = Instant::now();
app.on_ui_scroll_tick(); app.on_ui_scroll_tick();
assert!(!app.should_text_scroll);
assert_eq!(app.ui_scroll_tick_count, 0);
assert_eq!(app.tick_count, 0);
} }
#[tokio::test] #[tokio::test]
+11 -7
View File
@@ -7,6 +7,7 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{Duration, Instant};
use std::{fs, process}; use std::{fs, process};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -41,9 +42,10 @@ pub struct App<'a> {
pub error: HorizontallyScrollableText, pub error: HorizontallyScrollableText,
pub notification: Option<Notification>, pub notification: Option<Notification>,
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub scroll_interval: Duration,
pub last_scroll: Instant,
pub tick_count: u64, pub tick_count: u64,
pub ui_scroll_tick_count: u64, pub should_text_scroll: bool,
pub is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
@@ -171,10 +173,11 @@ impl App<'_> {
} }
pub fn on_ui_scroll_tick(&mut self) { pub fn on_ui_scroll_tick(&mut self) {
if self.ui_scroll_tick_count == self.ticks_until_scroll { if self.last_scroll.elapsed() >= self.scroll_interval {
self.ui_scroll_tick_count = 0; self.should_text_scroll = true;
self.last_scroll = Instant::now();
} else { } else {
self.ui_scroll_tick_count += 1; self.should_text_scroll = false;
} }
} }
@@ -260,9 +263,10 @@ impl Default for App<'_> {
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(Vec::new()), server_tabs: TabState::new(Vec::new()),
tick_until_poll: 400, tick_until_poll: 400,
ticks_until_scroll: 64, scroll_interval: Duration::from_millis(100),
last_scroll: Instant::now(),
tick_count: 0, tick_count: 0,
ui_scroll_tick_count: 0, should_text_scroll: false,
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
+14 -2
View File
@@ -18,10 +18,12 @@ pub struct Events {
rx: Receiver<InputEvent<Key>>, rx: Receiver<InputEvent<Key>>,
} }
const DEFAULT_TICK_RATE_MS: u64 = 50;
impl Events { impl Events {
pub fn new() -> Self { pub fn new() -> Self {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let tick_rate: Duration = Duration::from_millis(50); let tick_rate: Duration = Duration::from_millis(DEFAULT_TICK_RATE_MS);
thread::spawn(move || { thread::spawn(move || {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
@@ -51,9 +53,19 @@ impl Events {
} }
pub fn next(&self) -> Result<Option<InputEvent<Key>>> { pub fn next(&self) -> Result<Option<InputEvent<Key>>> {
match self.rx.try_recv() { match self.rx.recv() {
Ok(event) => Ok(Some(event)), Ok(event) => Ok(Some(event)),
_ => Ok(None), _ => Ok(None),
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_to_original_tick_rate() {
assert_eq!(DEFAULT_TICK_RATE_MS, 50);
}
}
+9 -3
View File
@@ -277,12 +277,15 @@ async fn start_ui(
let input_events = Events::new(); let input_events = Events::new();
loop { loop {
{
let mut app = app.lock().await; let mut app = app.lock().await;
terminal.draw(|f| ui(f, &mut app))?; terminal.draw(|f| ui(f, &mut app))?;
}
match input_events.next()? { match input_events.next()? {
Some(InputEvent::KeyEvent(key)) => { Some(InputEvent::KeyEvent(key)) => {
let mut app = app.lock().await;
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;
} }
@@ -290,8 +293,11 @@ async fn start_ui(
handlers::handle_events(key, &mut app); handlers::handle_events(key, &mut app);
} }
Some(InputEvent::Tick) => app.on_tick().await, Some(InputEvent::Tick) => {
_ => {} let mut app = app.lock().await;
app.on_tick().await;
}
None => break,
} }
} }
+22 -5
View File
@@ -1,5 +1,5 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::{Arc, OnceLock};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
@@ -9,6 +9,7 @@ use regex::Regex;
use reqwest::{Client, RequestBuilder}; use reqwest::{Client, RequestBuilder};
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::Value;
use sonarr_network::SonarrEvent; use sonarr_network::SonarrEvent;
use strum_macros::Display; use strum_macros::Display;
use tokio::select; use tokio::select;
@@ -32,6 +33,8 @@ mod network_tests;
#[cfg(test)] #[cfg(test)]
pub mod servarr_test_utils; pub mod servarr_test_utils;
static WHITESPACE_RE: OnceLock<Regex> = OnceLock::new();
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
#[async_trait] #[async_trait]
pub trait NetworkTrait { pub trait NetworkTrait {
@@ -142,11 +145,8 @@ impl<'a, 'b> Network<'a, 'b> {
} }
} else { } else {
let status = response.status(); let status = response.status();
let whitespace_regex = Regex::new(r"\s+")?;
let response_body = response.text().await.unwrap_or_default(); let response_body = response.text().await.unwrap_or_default();
let error_body = whitespace_regex let error_body = format_error_body(&response_body);
.replace_all(&response_body.replace('\n', " "), " ")
.to_string();
error!("Request failed. Received {status} response code with body: {response_body}"); error!("Request failed. Received {status} response code with body: {response_body}");
self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}")); self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}"));
@@ -279,6 +279,23 @@ impl<'a, 'b> Network<'a, 'b> {
} }
} }
fn format_error_body(response_body: &str) -> String {
let re = WHITESPACE_RE.get_or_init(|| Regex::new(r"\s+").unwrap());
let body = serde_json::from_str::<Value>(response_body)
.ok()
.and_then(|value| {
value
.get("message")
.or_else(|| value.get("errorMessage"))
.or_else(|| value.get("error"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.unwrap_or_else(|| response_body.replace('\n', " "));
re.replace_all(&body, " ").to_string()
}
#[derive(Clone, Copy, Debug, Display, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Display, PartialEq, Eq)]
pub enum RequestMethod { pub enum RequestMethod {
Get, Get,
+45
View File
@@ -353,6 +353,51 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_handle_request_non_success_code_uses_json_message_for_error_body() {
let mut server = Server::new_async().await;
let async_server = server
.mock("GET", "/test")
.match_header("X-Api-Key", "test1234")
.with_status(500)
.with_body(
json!({
"message": "database is locked\ndatabase is locked",
"description": "at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker"
})
.to_string(),
)
.create_async()
.await;
let app_arc = Arc::new(Mutex::new(App::test_default()));
let mut network = test_network(&app_arc);
let resp = network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
method: RequestMethod::Get,
body: None,
api_token: "test1234".to_owned(),
ignore_status_code: false,
custom_headers: HeaderMap::new(),
},
|response, mut app| app.error = HorizontallyScrollableText::from(response.value),
)
.await;
async_server.assert_async().await;
assert_str_eq!(
app_arc.lock().await.error.text,
"Request failed. Received 500 Internal Server Error response code with body: database is locked database is locked"
);
assert!(resp.is_err());
assert_str_eq!(
resp.unwrap_err().to_string(),
"Request failed. Received 500 Internal Server Error response code with body: database is locked database is locked"
);
}
#[rstest] #[rstest]
#[tokio::test] #[tokio::test]
async fn test_call_api( async fn test_call_api(
+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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
} }
+1 -1
View File
@@ -60,7 +60,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, 50), get_width_from_percentage(area, 50),
current_selection == *history_item, current_selection == *history_item,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -46,7 +46,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
let pass_fail = if result.is_valid { "+" } else { "x" }; let pass_fail = if result.is_valid { "+" } else { "x" };
let row = Row::new(vec![ let row = Row::new(vec![
+1 -1
View File
@@ -112,7 +112,7 @@ fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
artist.artist_name.scroll_left_or_reset( artist.artist_name.scroll_left_or_reset(
get_width_from_percentage(area, 27), get_width_from_percentage(area, 27),
*artist == current_selection, *artist == current_selection,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
+2 -2
View File
@@ -250,7 +250,7 @@ fn draw_album_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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -345,7 +345,7 @@ fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
get_width_from_percentage(area, 35), get_width_from_percentage(area, 35),
current_selection == *release current_selection == *release
&& active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, && active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -271,7 +271,7 @@ fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
album.title.scroll_left_or_reset( album.title.scroll_left_or_reset(
get_width_from_percentage(area, 33), get_width_from_percentage(area, 33),
*album == current_selection, *album == current_selection,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
let monitored = if album.monitored { "🏷" } else { "" }; let monitored = if album.monitored { "🏷" } else { "" };
let album_type = album.album_type.clone().unwrap_or_default(); let album_type = album.album_type.clone().unwrap_or_default();
@@ -376,7 +376,7 @@ fn draw_artist_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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -490,7 +490,7 @@ fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
get_width_from_percentage(area, 35), get_width_from_percentage(area, 35),
current_selection == *release current_selection == *release
&& active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, && active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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
@@ -96,7 +96,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
artist.artist_name.scroll_left_or_reset( artist.artist_name.scroll_left_or_reset(
get_width_from_percentage(area, 25), get_width_from_percentage(area, 25),
*artist == current_selection, *artist == current_selection,
app.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
let monitored = if artist.monitored { "🏷" } else { "" }; let monitored = if artist.monitored { "🏷" } else { "" };
let artist_type = artist.artist_type.clone().unwrap_or_default(); let artist_type = artist.artist_type.clone().unwrap_or_default();
+1 -1
View File
@@ -158,7 +158,7 @@ fn draw_track_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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
+1 -1
View File
@@ -139,7 +139,7 @@ fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
app app
.error .error
.scroll_left_or_reset(area.width as usize, true, app.ui_scroll_tick_count == 0); .scroll_left_or_reset(area.width as usize, true, app.should_text_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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
} }
+1 -1
View File
@@ -61,7 +61,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -47,7 +47,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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
@@ -141,7 +141,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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
@@ -250,7 +250,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -402,7 +402,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
} }
+1 -1
View File
@@ -61,7 +61,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -46,7 +46,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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
@@ -121,7 +121,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -279,7 +279,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -414,7 +414,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
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();
@@ -266,7 +266,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![
@@ -377,7 +377,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
let size = convert_to_gb(*size); let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" }; let rejected_str = if *rejected { "" } else { "" };
@@ -314,7 +314,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.ui_scroll_tick_count == 0, app.should_text_scroll,
); );
Row::new(vec![ Row::new(vec![