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.
This commit is contained in:
Dark-Alex-17 committed 2026-09-10 13:06:04 -06:00
1 parent c525fb9a4e
commit 65ba37c09a
8 files changed
+304 -4

No files matched your search

+1 -1
View File
@@ -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 logs_box = SelectableList::new(&mut app.data.lidarr_data.logs, |log| {
let log_line = log.to_string(); let log_line = log.to_string();
let level = log_line.split('|').collect::<Vec<&str>>()[1].to_string(); let level = log.text.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
@@ -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::<String>();
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<HorizontallyScrollableText> {
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 { mod snapshot_tests {
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::ui::ui_test_utils::test_utils::TerminalSize; use crate::ui::ui_test_utils::test_utils::TerminalSize;
+1 -1
View File
@@ -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 logs_box = SelectableList::new(&mut app.data.radarr_data.logs, |log| {
let log_line = log.to_string(); let log_line = log.to_string();
let level = log_line.split('|').collect::<Vec<&str>>()[1].to_string(); let level = log.text.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
@@ -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::<String>();
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<HorizontallyScrollableText> {
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 { mod snapshot_tests {
use super::*; use super::*;
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
+1 -1
View File
@@ -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 logs_box = SelectableList::new(&mut app.data.readarr_data.logs, |log| {
let log_line = log.to_string(); 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) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
@@ -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::<String>();
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 { mod snapshot_tests {
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::ui::ui_test_utils::test_utils::TerminalSize; use crate::ui::ui_test_utils::test_utils::TerminalSize;
+1 -1
View File
@@ -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 logs_box = SelectableList::new(&mut app.data.sonarr_data.logs, |log| {
let log_line = log.to_string(); let log_line = log.to_string();
let level = log_line.split('|').collect::<Vec<&str>>()[1].to_string(); let level = log.text.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
@@ -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::<String>();
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<HorizontallyScrollableText> {
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 { mod snapshot_tests {
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::ui::ui_test_utils::test_utils::TerminalSize; use crate::ui::ui_test_utils::test_utils::TerminalSize;