From 65ba37c09af93fc13da29e3dfcfe4ed671c1f81d Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 10 Sep 2026 13:06:04 -0600 Subject: [PATCH] fix: parse the log level from the unscrolled log text draw_logs read the level from the log line's Display output, which drops the first offset characters. Once a log line is scrolled, the field at index 1 is no longer the level: past the timestamp it yields the logger name and colours the row wrong, and past the final delimiter the index is out of bounds and panics, taking the TUI down. Parse log.text instead, matching what draw_logs_popup already does. The displayed text is unchanged. --- src/ui/lidarr_ui/system/mod.rs | 2 +- src/ui/lidarr_ui/system/system_ui_tests.rs | 77 +++++++++++++++++++++ src/ui/radarr_ui/system/mod.rs | 2 +- src/ui/radarr_ui/system/system_ui_tests.rs | 77 +++++++++++++++++++++ src/ui/readarr_ui/system/mod.rs | 2 +- src/ui/readarr_ui/system/system_ui_tests.rs | 69 ++++++++++++++++++ src/ui/sonarr_ui/system/mod.rs | 2 +- src/ui/sonarr_ui/system/system_ui_tests.rs | 77 +++++++++++++++++++++ 8 files changed, 304 insertions(+), 4 deletions(-) diff --git a/src/ui/lidarr_ui/system/mod.rs b/src/ui/lidarr_ui/system/mod.rs index e34362d9..56da00c9 100644 --- a/src/ui/lidarr_ui/system/mod.rs +++ b/src/ui/lidarr_ui/system/mod.rs @@ -165,7 +165,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let logs_box = SelectableList::new(&mut app.data.lidarr_data.logs, |log| { let log_line = log.to_string(); - let level = log_line.split('|').collect::>()[1].to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) diff --git a/src/ui/lidarr_ui/system/system_ui_tests.rs b/src/ui/lidarr_ui/system/system_ui_tests.rs index 58d9fcf7..b6fd236d 100644 --- a/src/ui/lidarr_ui/system/system_ui_tests.rs +++ b/src/ui/lidarr_ui/system/system_ui_tests.rs @@ -25,6 +25,83 @@ mod tests { }); } + mod test_log_level_styling { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use ratatui::style::{Modifier, Style}; + + use crate::models::HorizontallyScrollableText; + use crate::ui::styles::failure_style; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, create_test_terminal}; + + use super::*; + + #[test] + fn test_system_ui_styles_a_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(log_lines_vec()); + app.data.lidarr_data.logs.items[1] + .offset + .store(30, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "CommandExecutor|log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn test_system_ui_styles_a_fully_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(log_lines_vec()); + app.data.lidarr_data.logs.items[1] + .offset + .store(46, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + fn rendered_row_style(app: &mut App<'_>, needle: &str) -> Style { + let (width, height) = TerminalSize::Large.to_cartesian(); + let mut terminal = create_test_terminal(width, height); + + terminal + .draw(|f| { + SystemUi::draw(f, app, f.area()); + }) + .unwrap(); + + let buffer = terminal.backend().buffer(); + + for y in 0..height { + let row = (0..width) + .map(|x| buffer.cell((x, y)).expect("a rendered cell").symbol()) + .collect::(); + + if let Some(byte_index) = row.find(needle) { + let column = row[..byte_index].chars().count() as u16; + + return buffer.cell((column, y)).expect("a rendered cell").style(); + } + } + + panic!("no rendered row contained {needle}"); + } + + fn log_lines_vec() -> Vec { + vec![ + "2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|log line alpha".into(), + "2025-12-16 16:41:30 UTC|FATAL|CommandExecutor|log line gamma".into(), + ] + } + } + mod snapshot_tests { use crate::models::stateful_list::StatefulList; use crate::ui::ui_test_utils::test_utils::TerminalSize; diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 8bec01e2..270286fa 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -172,7 +172,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let logs_box = SelectableList::new(&mut app.data.radarr_data.logs, |log| { let log_line = log.to_string(); - let level = log_line.split('|').collect::>()[1].to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) diff --git a/src/ui/radarr_ui/system/system_ui_tests.rs b/src/ui/radarr_ui/system/system_ui_tests.rs index 31bd1610..fec72e7a 100644 --- a/src/ui/radarr_ui/system/system_ui_tests.rs +++ b/src/ui/radarr_ui/system/system_ui_tests.rs @@ -25,6 +25,83 @@ mod tests { }); } + mod test_log_level_styling { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use ratatui::style::{Modifier, Style}; + + use crate::models::HorizontallyScrollableText; + use crate::ui::styles::failure_style; + use crate::ui::ui_test_utils::test_utils::create_test_terminal; + + use super::*; + + #[test] + fn test_system_ui_styles_a_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::System.into()); + app.data.radarr_data.logs.set_items(log_lines_vec()); + app.data.radarr_data.logs.items[1] + .offset + .store(30, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "CommandExecutor|log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn test_system_ui_styles_a_fully_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::System.into()); + app.data.radarr_data.logs.set_items(log_lines_vec()); + app.data.radarr_data.logs.items[1] + .offset + .store(46, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + fn rendered_row_style(app: &mut App<'_>, needle: &str) -> Style { + let (width, height) = TerminalSize::Large.to_cartesian(); + let mut terminal = create_test_terminal(width, height); + + terminal + .draw(|f| { + SystemUi::draw(f, app, f.area()); + }) + .unwrap(); + + let buffer = terminal.backend().buffer(); + + for y in 0..height { + let row = (0..width) + .map(|x| buffer.cell((x, y)).expect("a rendered cell").symbol()) + .collect::(); + + if let Some(byte_index) = row.find(needle) { + let column = row[..byte_index].chars().count() as u16; + + return buffer.cell((column, y)).expect("a rendered cell").style(); + } + } + + panic!("no rendered row contained {needle}"); + } + + fn log_lines_vec() -> Vec { + vec![ + "2025-12-15 16:14:45 UTC|INFO|DownloadDecisionMaker|log line alpha".into(), + "2025-12-16 16:41:30 UTC|FATAL|CommandExecutor|log line gamma".into(), + ] + } + } + mod snapshot_tests { use super::*; use crate::models::stateful_list::StatefulList; diff --git a/src/ui/readarr_ui/system/mod.rs b/src/ui/readarr_ui/system/mod.rs index 2c4f3059..d353adae 100644 --- a/src/ui/readarr_ui/system/mod.rs +++ b/src/ui/readarr_ui/system/mod.rs @@ -161,7 +161,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let logs_box = SelectableList::new(&mut app.data.readarr_data.logs, |log| { let log_line = log.to_string(); - let level = extract_log_level(&log_line); + let level = extract_log_level(&log.text); style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) diff --git a/src/ui/readarr_ui/system/system_ui_tests.rs b/src/ui/readarr_ui/system/system_ui_tests.rs index 4716ac80..8aa54a35 100644 --- a/src/ui/readarr_ui/system/system_ui_tests.rs +++ b/src/ui/readarr_ui/system/system_ui_tests.rs @@ -63,6 +63,75 @@ mod tests { } } + mod test_log_level_styling { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use ratatui::style::{Modifier, Style}; + + use crate::ui::styles::failure_style; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, create_test_terminal}; + + use super::*; + + #[test] + fn test_system_ui_styles_a_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::System.into()); + app.data.readarr_data.logs.set_items(log_lines_vec()); + app.data.readarr_data.logs.items[2] + .offset + .store(30, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "CommandExecutor|log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn test_system_ui_styles_a_fully_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::System.into()); + app.data.readarr_data.logs.set_items(log_lines_vec()); + app.data.readarr_data.logs.items[2] + .offset + .store(46, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + fn rendered_row_style(app: &mut App<'_>, needle: &str) -> Style { + let (width, height) = TerminalSize::Large.to_cartesian(); + let mut terminal = create_test_terminal(width, height); + + terminal + .draw(|f| { + SystemUi::draw(f, app, f.area()); + }) + .unwrap(); + + let buffer = terminal.backend().buffer(); + + for y in 0..height { + let row = (0..width) + .map(|x| buffer.cell((x, y)).expect("a rendered cell").symbol()) + .collect::(); + + if let Some(byte_index) = row.find(needle) { + let column = row[..byte_index].chars().count() as u16; + + return buffer.cell((column, y)).expect("a rendered cell").style(); + } + } + + panic!("no rendered row contained {needle}"); + } + } + mod snapshot_tests { use crate::models::stateful_list::StatefulList; use crate::ui::ui_test_utils::test_utils::TerminalSize; diff --git a/src/ui/sonarr_ui/system/mod.rs b/src/ui/sonarr_ui/system/mod.rs index 233acd7f..5e51a301 100644 --- a/src/ui/sonarr_ui/system/mod.rs +++ b/src/ui/sonarr_ui/system/mod.rs @@ -165,7 +165,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let logs_box = SelectableList::new(&mut app.data.sonarr_data.logs, |log| { let log_line = log.to_string(); - let level = log_line.split('|').collect::>()[1].to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) diff --git a/src/ui/sonarr_ui/system/system_ui_tests.rs b/src/ui/sonarr_ui/system/system_ui_tests.rs index 39b258c1..97d2d6e0 100644 --- a/src/ui/sonarr_ui/system/system_ui_tests.rs +++ b/src/ui/sonarr_ui/system/system_ui_tests.rs @@ -25,6 +25,83 @@ mod tests { }); } + mod test_log_level_styling { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use ratatui::style::{Modifier, Style}; + + use crate::models::HorizontallyScrollableText; + use crate::ui::styles::failure_style; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, create_test_terminal}; + + use super::*; + + #[test] + fn test_system_ui_styles_a_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(log_lines_vec()); + app.data.sonarr_data.logs.items[1] + .offset + .store(30, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "CommandExecutor|log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn test_system_ui_styles_a_fully_scrolled_log_using_its_unscrolled_level() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(log_lines_vec()); + app.data.sonarr_data.logs.items[1] + .offset + .store(46, Ordering::SeqCst); + + let style = rendered_row_style(&mut app, "log line gamma"); + + assert_eq!(style.fg, failure_style().fg); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + fn rendered_row_style(app: &mut App<'_>, needle: &str) -> Style { + let (width, height) = TerminalSize::Large.to_cartesian(); + let mut terminal = create_test_terminal(width, height); + + terminal + .draw(|f| { + SystemUi::draw(f, app, f.area()); + }) + .unwrap(); + + let buffer = terminal.backend().buffer(); + + for y in 0..height { + let row = (0..width) + .map(|x| buffer.cell((x, y)).expect("a rendered cell").symbol()) + .collect::(); + + if let Some(byte_index) = row.find(needle) { + let column = row[..byte_index].chars().count() as u16; + + return buffer.cell((column, y)).expect("a rendered cell").style(); + } + } + + panic!("no rendered row contained {needle}"); + } + + fn log_lines_vec() -> Vec { + vec![ + "2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|log line alpha".into(), + "2025-12-16 16:41:30 UTC|FATAL|CommandExecutor|log line gamma".into(), + ] + } + } + mod snapshot_tests { use crate::models::stateful_list::StatefulList; use crate::ui::ui_test_utils::test_utils::TerminalSize;