event more grouping prep

This commit is contained in:
Robin Appelman 2025-08-16 13:04:45 +02:00
commit 9e66967141
11 changed files with 149 additions and 145 deletions

View file

@ -1,7 +1,5 @@
use crate::grouping::{group_lines_by, FilterGrouping, LogGrouping};
use crate::logfile::{LogFile, LogLine, LogLineNumber}; use crate::logfile::{LogFile, LogLine, LogLineNumber};
use crate::logs::ParsedLogs; use crate::logs::ParsedLogs;
use crate::matcher::MatchResult;
use crate::timegraph::{SparkLine, TimeGraph}; use crate::timegraph::{SparkLine, TimeGraph};
use regex::{escape, Regex, RegexBuilder}; use regex::{escape, Regex, RegexBuilder};
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
@ -12,16 +10,11 @@ use time::OffsetDateTime;
pub struct App<'logs> { pub struct App<'logs> {
pub lines: &'logs ParsedLogs<'logs>, pub lines: &'logs ParsedLogs<'logs>,
pub matches: &'logs [LogGrouping<'logs, MatchResult>],
pub log_file: &'logs LogFile, pub log_file: &'logs LogFile,
pub unmatched_count: usize, pub unmatched_count: usize,
} }
impl<'logs> App<'logs> { impl<'logs> App<'logs> {
pub fn match_lines(&self) -> usize {
self.matches.len() + 1
}
pub fn get_source_line(&self, index: LogLineNumber) -> Option<&'logs str> { pub fn get_source_line(&self, index: LogLineNumber) -> Option<&'logs str> {
self.log_file.nth(index) self.log_file.nth(index)
} }
@ -49,6 +42,7 @@ impl<'logs> App<'logs> {
} }
} }
#[derive(Clone)]
pub struct LineSet<'logs> { pub struct LineSet<'logs> {
pub lines: Vec<&'logs LogLine<'logs>>, pub lines: Vec<&'logs LogLine<'logs>>,
pub histogram: OnceCell<TimeGraph>, pub histogram: OnceCell<TimeGraph>,
@ -99,11 +93,6 @@ impl<'logs> LineSet<'logs> {
pub fn iter<'a>(&'a self) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> { pub fn iter<'a>(&'a self) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> {
self.lines.iter().copied() self.lines.iter().copied()
} }
pub fn group_by<G: FilterGrouping>(&self) -> Vec<LogGrouping<G::Result>> {
let _grouped = group_lines_by(self.lines.iter().copied(), G::filter);
todo!()
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]

View file

@ -2,6 +2,7 @@ mod unique;
use crate::app::{Filter, LineSet}; use crate::app::{Filter, LineSet};
use crate::logfile::LogLine; use crate::logfile::LogLine;
use crate::matcher::MatchResult;
use crate::timegraph::{SparkLine, TimeGraph}; use crate::timegraph::{SparkLine, TimeGraph};
use ratatui::layout::{Alignment, Constraint}; use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow; use std::borrow::Cow;
@ -11,49 +12,39 @@ use std::ops::RangeInclusive;
use time::OffsetDateTime; use time::OffsetDateTime;
pub use unique::*; pub use unique::*;
pub trait Grouping { pub struct GroupingUi {
const HEADER: &'static [(&'static str, Alignment)]; pub header: &'static [(&'static str, Alignment)],
pub widths: &'static [Constraint],
const WIDTHS: &'static [Constraint];
}
pub trait FilterGrouping: Grouping {
type Result: Grouping;
type Identifier: Ord;
fn filter(line: &LogLine) -> Self::Identifier;
} }
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub trait GroupingResult { pub trait GroupingResult<'a> {
type Grouping: Grouping;
type Next: FilterGrouping;
fn height(&self) -> usize { fn height(&self) -> usize {
1 1
} }
fn matches(&self, filter: &Filter) -> bool; fn matches(&self, filter: &Filter) -> bool;
fn render(&self) -> impl Iterator<Item = Cow<str>>; fn render(&self) -> impl Iterator<Item = Cow<'a, str>>;
} }
pub struct LogGrouping<'logs, G> { #[derive(Clone)]
pub struct LogGrouping<'logs> {
pub name: Option<&'static str>, pub name: Option<&'static str>,
pub result: Option<G>, pub result: Option<Groupings>,
pub count: usize, pub count: usize,
pub lines: LineSet<'logs>, pub lines: LineSet<'logs>,
pub by_identifier: OnceCell<Vec<LineSet<'logs>>>, pub by_identifier: OnceCell<Vec<LineSet<'logs>>>,
} }
impl<'logs, G: GroupingResult> LogGrouping<'logs, G> { impl<'logs> LogGrouping<'logs> {
pub fn new(result: G, lines: Vec<&'logs LogLine<'logs>>) -> Self { pub fn new(result: impl Into<Groupings>, lines: Vec<&'logs LogLine<'logs>>) -> Self {
let count = lines.len(); let count = lines.len();
let lines = LineSet::new(lines); let lines = LineSet::new(lines);
LogGrouping { LogGrouping {
name: None, name: None,
result: Some(result), result: Some(result.into()),
count, count,
lines, lines,
by_identifier: OnceCell::new(), by_identifier: OnceCell::new(),
@ -122,3 +113,28 @@ where
list.reverse(); list.reverse();
list list
} }
#[derive(PartialEq, Clone)]
pub enum Groupings {
Match(MatchResult),
}
impl From<MatchResult> for Groupings {
fn from(value: MatchResult) -> Self {
Groupings::Match(value)
}
}
impl<'a> GroupingResult<'a> for Groupings {
fn matches(&self, filter: &Filter) -> bool {
match self {
Groupings::Match(r) => r.matches(filter),
}
}
fn render(&self) -> impl Iterator<Item = Cow<'a, str>> {
match self {
Groupings::Match(r) => r.render(),
}
}
}

View file

@ -1,42 +1,29 @@
use crate::app::Filter; use crate::app::Filter;
use crate::grouping::{FilterGrouping, Grouping, GroupingResult}; use crate::grouping::{GroupingResult, GroupingUi};
use crate::logfile::LogLine; use crate::logfile::LogLine;
use ratatui::layout::{Alignment, Constraint}; use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow; use std::borrow::Cow;
pub struct UniqueLog; pub struct UniqueLog;
impl Grouping for UniqueLog {
const HEADER: &'static [(&'static str, Alignment)] = &[
("Level", Alignment::Left),
("App", Alignment::Left),
("Message", Alignment::Left),
];
const WIDTHS: &'static [Constraint] = &[
Constraint::Min(10),
Constraint::Min(20),
Constraint::Percentage(100),
];
}
impl FilterGrouping for UniqueLog {
type Result = UniqueLog;
type Identifier = u64;
fn filter(line: &LogLine) -> Self::Identifier {
line.identity()
}
}
pub struct UniqueGrouping<'a> { pub struct UniqueGrouping<'a> {
pub line: &'a LogLine<'a>, pub line: &'a LogLine<'a>,
} }
impl<'a> GroupingResult for UniqueGrouping<'a> { pub const UNIQUE_GROUPING_UI: GroupingUi = GroupingUi {
type Grouping = UniqueLog; header: &[
type Next = UniqueLog; ("Level", Alignment::Left),
("App", Alignment::Left),
("Message", Alignment::Left),
],
widths: &[
Constraint::Min(10),
Constraint::Min(20),
Constraint::Percentage(100),
],
};
impl<'a> GroupingResult<'a> for UniqueGrouping<'a> {
fn matches(&self, filter: &Filter) -> bool { fn matches(&self, filter: &Filter) -> bool {
if filter.is_empty() { if filter.is_empty() {
return true; return true;
@ -46,7 +33,7 @@ impl<'a> GroupingResult for UniqueGrouping<'a> {
.all(|filter_part| filter_part.is_match(&self.line.message)) .all(|filter_part| filter_part.is_match(&self.line.message))
} }
fn render(&self) -> impl Iterator<Item = Cow<str>> { fn render(&self) -> impl Iterator<Item = Cow<'a, str>> {
[ [
Cow::from(self.line.level.as_str()), Cow::from(self.line.level.as_str()),
Cow::from(self.line.app.as_ref()), Cow::from(self.line.app.as_ref()),

View file

@ -152,8 +152,8 @@ fn main() -> MainResult {
matched_lines.sort_by_key(|(_, lines)| lines.len()); matched_lines.sort_by_key(|(_, lines)| lines.len());
matched_lines.reverse(); matched_lines.reverse();
let all = LogGrouping::<MatchResult>::named("All", parsed_log.find_lines(|_| true).collect()); let all = LogGrouping::named("All", parsed_log.find_lines(|_| true).collect());
let unmatched = LogGrouping::<MatchResult>::named("Unmatched", unmatched_lines); let unmatched = LogGrouping::named("Unmatched", unmatched_lines);
let mut matches: Vec<_> = matched_lines let mut matches: Vec<_> = matched_lines
.into_par_iter() .into_par_iter()
@ -169,7 +169,6 @@ fn main() -> MainResult {
let app = App { let app = App {
lines: &parsed_log, lines: &parsed_log,
matches: &matches,
log_file: &log_file, log_file: &log_file,
unmatched_count, unmatched_count,
}; };
@ -180,7 +179,7 @@ fn main() -> MainResult {
drop(progress); drop(progress);
run_ui(app)?; run_ui(app, matches)?;
Ok(()) Ok(())
} }

View file

@ -1,5 +1,5 @@
use crate::app::Filter; use crate::app::Filter;
use crate::grouping::{Grouping, GroupingResult, UniqueLog}; use crate::grouping::{GroupingResult, GroupingUi};
use crate::logfile::logline::{Exception, LogLine}; use crate::logfile::logline::{Exception, LogLine};
use crate::logfile::LineNumber; use crate::logfile::LineNumber;
use itertools::Either; use itertools::Either;
@ -211,24 +211,20 @@ impl From<Vec<LoggingStatementWithPathPrefix>> for MatchResult {
} }
} }
impl Grouping for LoggingStatementWithPathPrefix { pub const MATCH_GROUPING_UI: GroupingUi = GroupingUi {
const HEADER: &'static [(&'static str, Alignment)] = &[ header: &[
("Statement", Alignment::Left), ("Statement", Alignment::Left),
("File", Alignment::Left), ("File", Alignment::Left),
("Line", Alignment::Right), ("Line", Alignment::Right),
]; ],
widths: &[
const WIDTHS: &'static [Constraint] = &[
Constraint::Percentage(70), Constraint::Percentage(70),
Constraint::Percentage(30), Constraint::Percentage(30),
Constraint::Min(6), Constraint::Min(6),
]; ],
} };
impl GroupingResult for MatchResult {
type Grouping = LoggingStatementWithPathPrefix;
type Next = UniqueLog;
impl<'a> GroupingResult<'a> for MatchResult {
fn height(&self) -> usize { fn height(&self) -> usize {
self.count() self.count()
} }
@ -256,7 +252,7 @@ impl GroupingResult for MatchResult {
}) })
} }
fn render(&self) -> impl Iterator<Item = Cow<str>> { fn render(&self) -> impl Iterator<Item = Cow<'a, str>> {
let mut message = String::new(); let mut message = String::new();
let mut paths = String::new(); let mut paths = String::new();
let mut lines = String::new(); let mut lines = String::new();

View file

@ -2,6 +2,7 @@ use hdrhistogram::Histogram;
use ratatui::text::Text; use ratatui::text::Text;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::max; use std::cmp::max;
use std::fmt::{Display, Formatter};
use time::OffsetDateTime; use time::OffsetDateTime;
#[derive(Clone)] #[derive(Clone)]
@ -60,10 +61,18 @@ impl TimeGraph {
} }
// the biggest sparkline char is 3 bytes // the biggest sparkline char is 3 bytes
#[derive(Clone, Copy)]
pub struct SparkLine { pub struct SparkLine {
bytes: [u8; 10 * 3], bytes: [u8; 10 * 3],
} }
impl SparkLine {
pub fn as_str(&self) -> &str {
// SAFETY: we only put bytes into the buffer from encode_utf8
unsafe { str::from_utf8_unchecked(&self.bytes).trim_end_matches(char::from(0)) }
}
}
impl From<[char; 10]> for SparkLine { impl From<[char; 10]> for SparkLine {
fn from(value: [char; 10]) -> Self { fn from(value: [char; 10]) -> Self {
let mut buff = [0; 10 * 3]; let mut buff = [0; 10 * 3];
@ -78,17 +87,19 @@ impl From<[char; 10]> for SparkLine {
impl<'a> From<&'a SparkLine> for Text<'a> { impl<'a> From<&'a SparkLine> for Text<'a> {
fn from(value: &'a SparkLine) -> Self { fn from(value: &'a SparkLine) -> Self {
// SAFETY: we only put bytes into the buffer from encode_utf8 Text::raw(value.as_str())
let str = unsafe { str::from_utf8_unchecked(&value.bytes).trim_end_matches(char::from(0)) };
Text::raw(str)
} }
} }
impl<'a> From<&'a SparkLine> for Cow<'a, str> { impl<'a> From<&'a SparkLine> for Cow<'a, str> {
fn from(value: &'a SparkLine) -> Self { fn from(value: &'a SparkLine) -> Self {
// SAFETY: we only put bytes into the buffer from encode_utf8 value.as_str().into()
let str = unsafe { str::from_utf8_unchecked(&value.bytes).trim_end_matches(char::from(0)) }; }
str.into() }
impl Display for SparkLine {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
} }
} }

View file

@ -1,5 +1,5 @@
use crate::app::Filter; use crate::app::Filter;
use crate::grouping::{Grouping, GroupingResult, LogGrouping}; use crate::grouping::{GroupingResult, GroupingUi, LogGrouping};
use crate::ui::style::TABLE_HEADER_STYLE; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable; use crate::ui::table::ScrollbarTable;
use ratatui::prelude::*; use ratatui::prelude::*;
@ -8,12 +8,14 @@ use std::borrow::Cow;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use time::OffsetDateTime; use time::OffsetDateTime;
pub fn grouping_list<'a, G: GroupingResult>( pub fn grouping_list<'a>(
items: &'a [LogGrouping<'a, G>], items: &[LogGrouping<'a>],
ui: &GroupingUi,
time_range: RangeInclusive<OffsetDateTime>, time_range: RangeInclusive<OffsetDateTime>,
filter: &Filter, filter: &Filter,
) -> ScrollbarTable<'a> { ) -> ScrollbarTable<'a> {
let header = G::Grouping::HEADER let header = ui
.header
.iter() .iter()
.copied() .copied()
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]); .chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
@ -25,12 +27,15 @@ pub fn grouping_list<'a, G: GroupingResult>(
.style(TABLE_HEADER_STYLE) .style(TABLE_HEADER_STYLE)
.height(1); .height(1);
ScrollbarTable::new( let items: Vec<Row<'a>> = items
items
.iter() .iter()
.filter(|result| result.matches(filter)) .filter(|result| result.matches(filter))
.map(|result| grouped_row(result, time_range.clone())), .map(|result| grouped_row(result, time_range.clone()))
G::Grouping::WIDTHS .collect();
ScrollbarTable::new(
items,
ui.widths
.iter() .iter()
.copied() .copied()
.chain([Constraint::Length(10), Constraint::Min(10)]), .chain([Constraint::Length(10), Constraint::Min(10)]),
@ -38,14 +43,14 @@ pub fn grouping_list<'a, G: GroupingResult>(
.header(header) .header(header)
} }
fn grouped_row<'a, G: GroupingResult>( fn grouped_row<'a>(
grouping: &'a LogGrouping<'a, G>, grouping: &LogGrouping<'a>,
time_range: RangeInclusive<OffsetDateTime>, time_range: RangeInclusive<OffsetDateTime>,
) -> Row<'a> { ) -> Row<'a> {
if let Some(match_result) = &grouping.result { if let Some(match_result) = &grouping.result {
let grouping_columns = match_result.render(); let grouping_columns = match_result.render();
let columns = grouping_columns.chain([ let columns = grouping_columns.chain([
Cow::from(grouping.sparkline(time_range)), Cow::from(grouping.sparkline(time_range).to_string()),
Cow::from(grouping.count().to_string()), Cow::from(grouping.count().to_string()),
]); ]);
Row::new(columns).height(match_result.height() as u16) Row::new(columns).height(match_result.height() as u16)
@ -54,7 +59,7 @@ fn grouped_row<'a, G: GroupingResult>(
Text::from(grouping.name.unwrap_or_default()), Text::from(grouping.name.unwrap_or_default()),
Text::from(""), Text::from(""),
Text::from(""), Text::from(""),
Text::from(grouping.sparkline(time_range)), Text::from(grouping.sparkline(time_range).to_string()),
Text::from(grouping.count().to_string()), Text::from(grouping.count().to_string()),
]) ])
} }

View file

@ -1,4 +1,3 @@
use crate::app::App;
use crate::ui::find_hit_row; use crate::ui::find_hit_row;
use crate::ui::state::{Mode, UiPage, UiState}; use crate::ui::state::{Mode, UiPage, UiState};
use ratatui::crossterm::event; use ratatui::crossterm::event;
@ -30,7 +29,7 @@ pub enum PopMode {
Word, Word,
} }
pub fn handle_events(page: UiPage, ui_state: &UiState, app: &App) -> io::Result<Option<UiEvent>> { pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEvent>> {
if event::poll(Duration::from_millis(50))? { if event::poll(Duration::from_millis(50))? {
match event::read()? { match event::read()? {
Event::Key(key) if key.kind == event::KeyEventKind::Press => { Event::Key(key) if key.kind == event::KeyEventKind::Press => {
@ -77,7 +76,7 @@ pub fn handle_events(page: UiPage, ui_state: &UiState, app: &App) -> io::Result<
MouseEventKind::ScrollUp => Some(UiEvent::Scroll(-1)), MouseEventKind::ScrollUp => Some(UiEvent::Scroll(-1)),
MouseEventKind::ScrollDown => Some(UiEvent::Scroll(1)), MouseEventKind::ScrollDown => Some(UiEvent::Scroll(1)),
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
find_hit_row(mouse.row, ui_state, app).map(UiEvent::Enter) find_hit_row(mouse.row, ui_state).map(UiEvent::Enter)
} }
_ => None, _ => None,
}) })

View file

@ -1,6 +1,6 @@
use crate::app::App; use crate::app::App;
use crate::error::UiError; use crate::error::UiError;
use crate::matcher::MatchResult; use crate::grouping::LogGrouping;
use crate::ui::by_identifier::logs_by_identifier; use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list; use crate::ui::error_list::error_list;
use crate::ui::footer::footer; use crate::ui::footer::footer;
@ -38,12 +38,12 @@ mod state;
pub mod style; pub mod style;
mod table; mod table;
pub fn run_ui<'a>(app: App<'a>) -> Result<(), UiError> { pub fn run_ui<'a>(app: App<'a>, matches: Vec<LogGrouping<'a>>) -> Result<(), UiError> {
init_panic_hook(); init_panic_hook();
let mut terminal = init_tui()?; let mut terminal = init_tui()?;
terminal.clear().ok(); terminal.clear().ok();
let mut ui_state: UiState = UiState::new(&app); let mut ui_state: UiState = UiState::new(matches);
let mut update = true; let mut update = true;
while !matches!(ui_state, UiState::Quit) { while !matches!(ui_state, UiState::Quit) {
@ -51,7 +51,7 @@ pub fn run_ui<'a>(app: App<'a>) -> Result<(), UiError> {
terminal.draw(|frame| ui(frame, &app, &mut ui_state))?; terminal.draw(|frame| ui(frame, &app, &mut ui_state))?;
} }
update = false; update = false;
if let Some(event) = handle_events(ui_state.page(), &ui_state, &app)? { if let Some(event) = handle_events(ui_state.page(), &ui_state)? {
(update, ui_state) = ui_state.process(event, &app); (update, ui_state) = ui_state.process(event, &app);
} }
} }
@ -84,9 +84,9 @@ pub fn restore_tui() -> io::Result<()> {
Ok(()) Ok(())
} }
fn find_hit_row(row: u16, ui_state: &UiState, app: &App) -> Option<usize> { fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> {
if let Some(table_row) = row.checked_sub(ui_state.content_offset()) { if let Some(table_row) = row.checked_sub(ui_state.content_offset()) {
let selected = ui_state.index_for_row(table_row as usize, app); let selected = ui_state.index_for_row(table_row as usize);
if selected < ui_state.row_count() { if selected < ui_state.row_count() {
Some(selected) Some(selected)
} else { } else {
@ -112,16 +112,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state { match state {
UiState::Quit => {} UiState::Quit => {}
UiState::GroupList(GroupListState { UiState::GroupList(GroupListState {
items,
table_state, table_state,
filter, filter,
ui,
.. ..
}) => { }) => {
let selected = table_state.selected(); let selected = table_state.selected();
let histogram = app.matches[selected].histogram(app.time_range()); let histogram = items[selected].histogram(app.time_range());
frame.render_widget(UiHistogram::new(histogram), layout[0]); frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget( frame.render_stateful_widget(
grouping_list::<MatchResult>(app.matches, app.time_range(), filter), grouping_list(items, ui, app.time_range(), filter),
layout[1], layout[1],
table_state, table_state,
); );

View file

@ -1,8 +1,8 @@
use crate::app::{App, Filter, EMPTY_FILTER}; use crate::app::{App, Filter, EMPTY_FILTER};
use crate::error::ParseError; use crate::error::ParseError;
use crate::grouping::LogGrouping; use crate::grouping::{GroupingUi, LogGrouping};
use crate::logfile::logline::{FullLogLine, LogLine}; use crate::logfile::logline::{FullLogLine, LogLine};
use crate::matcher::MatchResult; use crate::matcher::MATCH_GROUPING_UI;
use crate::ui::footer::FooterParams; use crate::ui::footer::FooterParams;
use crate::ui::input::{PopMode, UiEvent}; use crate::ui::input::{PopMode, UiEvent};
use crate::ui::table::ScrollbarTableState; use crate::ui::table::ScrollbarTableState;
@ -10,13 +10,12 @@ use crate::ui::UI_HEADER_SIZE;
use crate::{copy_osc, parse_line_full}; use crate::{copy_osc, parse_line_full};
use derive_more::From; use derive_more::From;
use ratatui::widgets::TableState; use ratatui::widgets::TableState;
use std::borrow::Cow;
use std::iter::once; use std::iter::once;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone, From, PartialEq)] #[derive(From, PartialEq)]
pub enum UiState<'a> { pub enum UiState<'a> {
GroupList(GroupListState), GroupList(GroupListState<'a>),
Group(GroupState<'a>), Group(GroupState<'a>),
ByIdentifier(LogsByIdentifierState<'a>), ByIdentifier(LogsByIdentifierState<'a>),
Log(LogState<'a>), Log(LogState<'a>),
@ -31,27 +30,30 @@ pub enum Mode {
FilterInput, FilterInput,
} }
#[derive(Clone)] pub struct GroupListState<'a> {
pub struct GroupListState { pub items: Vec<LogGrouping<'a>>,
pub ui: GroupingUi,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub filter: Filter, pub filter: Filter,
mode: Mode, mode: Mode,
} }
impl GroupListState { impl<'a> GroupListState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected() self.table_state.selected()
} }
fn enter<'a>(self, selected: usize, app: &App<'a>) -> UiState<'a> { fn enter(self, selected: usize) -> UiState<'a> {
// todo remove clones?
let result = if self.filter.is_empty() { let result = if self.filter.is_empty() {
&app.matches[selected - 1] self.items[selected].clone()
} else { } else {
app.matches self.items
.iter() .iter()
.filter(|log_match| log_match.matches(&self.filter)) .filter(|log_match| log_match.matches(&self.filter))
.nth(selected - 1) .nth(selected)
.unwrap_or(app.matches.last().unwrap()) .unwrap_or(self.items.last().unwrap())
.clone()
}; };
let table_state = ScrollbarTableState::new(result.by_identifier().len() + 1); let table_state = ScrollbarTableState::new(result.by_identifier().len() + 1);
@ -65,15 +67,14 @@ impl GroupListState {
} }
} }
impl PartialEq for GroupListState { impl PartialEq for GroupListState<'_> {
fn eq(&self, _other: &Self) -> bool { fn eq(&self, _other: &Self) -> bool {
true true
} }
} }
#[derive(Clone)]
pub struct GroupState<'a> { pub struct GroupState<'a> {
pub result: &'a LogGrouping<'a, MatchResult>, pub result: LogGrouping<'a>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
pub filter: Filter, pub filter: Filter,
@ -89,10 +90,12 @@ impl<'a> GroupState<'a> {
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
// todo: remove clones
let selected_line = if selected == 0 { let selected_line = if selected == 0 {
&self.result.lines self.result.lines.clone()
} else if self.filter.is_empty() { } else if self.filter.is_empty() {
&self.result.by_identifier()[selected - 1] self.result.by_identifier()[selected - 1].clone()
} else { } else {
self.result self.result
.by_identifier() .by_identifier()
@ -100,6 +103,7 @@ impl<'a> GroupState<'a> {
.filter(|grouped| grouped.matches(&self.filter)) .filter(|grouped| grouped.matches(&self.filter))
.nth(selected - 1) .nth(selected - 1)
.expect("filtered select out of bounds") .expect("filtered select out of bounds")
.clone()
}; };
let lines = selected_line.lines.as_slice(); let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
@ -126,9 +130,8 @@ pub enum GroupedLogGrouping {
Request, Request,
} }
#[derive(Clone)]
pub struct LogsByIdentifierState<'a> { pub struct LogsByIdentifierState<'a> {
pub lines: Cow<'a, [&'a LogLine<'a>]>, pub lines: Vec<&'a LogLine<'a>>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
pub filter: Filter, pub filter: Filter,
@ -190,7 +193,7 @@ impl<'a> LogsByIdentifierState<'a> {
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::ByIdentifier(LogsByIdentifierState { UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(), lines,
mode: Mode::Normal, mode: Mode::Normal,
filter: Filter::default(), filter: Filter::default(),
table_state, table_state,
@ -206,7 +209,6 @@ impl PartialEq for LogsByIdentifierState<'_> {
} }
} }
#[derive(Clone)]
pub struct ErrorLinesState<'a> { pub struct ErrorLinesState<'a> {
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
@ -218,7 +220,6 @@ impl PartialEq for ErrorLinesState<'_> {
} }
} }
#[derive(Clone)]
pub struct LogState<'a> { pub struct LogState<'a> {
pub log: &'a LogLine<'a>, pub log: &'a LogLine<'a>,
pub full_line: Box<FullLogLine>, pub full_line: Box<FullLogLine>,
@ -232,7 +233,7 @@ impl<'a> LogState<'a> {
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::ByIdentifier(LogsByIdentifierState { UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(), lines,
mode: Mode::Normal, mode: Mode::Normal,
filter: Filter::default(), filter: Filter::default(),
table_state, table_state,
@ -249,11 +250,14 @@ impl PartialEq for LogState<'_> {
} }
impl<'a> UiState<'a> { impl<'a> UiState<'a> {
pub fn new(app: &App) -> Self { pub fn new(matches: Vec<LogGrouping<'a>>) -> Self {
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
let lines = matches.len();
UiState::GroupList(GroupListState { UiState::GroupList(GroupListState {
table_state: ScrollbarTableState::new(app.match_lines()), items: matches,
ui: MATCH_GROUPING_UI,
table_state: ScrollbarTableState::new(lines),
filter: Filter::default(), filter: Filter::default(),
mode: Mode::Normal, mode: Mode::Normal,
}) })
@ -344,12 +348,11 @@ impl<'a> UiState<'a> {
} }
} }
pub fn index_for_row(&self, row: usize, app: &App) -> usize { pub fn index_for_row(&self, row: usize) -> usize {
match self { match self {
UiState::GroupList(GroupListState { filter, .. }) => { UiState::GroupList(GroupListState { filter, items, .. }) => {
let mut total_height = 0; let mut total_height = 0;
let match_row_counts = app let match_row_counts = items
.matches
.iter() .iter()
.filter(|m| m.matches(filter)) .filter(|m| m.matches(filter))
.map(|m| m.row_count()); .map(|m| m.row_count());
@ -365,9 +368,9 @@ impl<'a> UiState<'a> {
total_height += row_count; total_height += row_count;
} }
if total_height > row { if total_height > row {
app.matches.len() + 1 items.len() + 1
} else { } else {
app.matches.len() + 2 items.len() + 2
} }
} }
_ => row + self.scroll_offset(), _ => row + self.scroll_offset(),
@ -423,11 +426,9 @@ impl<'a> UiState<'a> {
} }
(UiState::GroupList(state), UiEvent::Select) => { (UiState::GroupList(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
(true, state.enter(selected, app)) (true, state.enter(selected))
}
(UiState::GroupList(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app))
} }
(UiState::GroupList(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::GroupList(state), UiEvent::Errors) => { (UiState::GroupList(state), UiEvent::Errors) => {
let table_state = ScrollbarTableState::new(app.error_count()); let table_state = ScrollbarTableState::new(app.error_count());
( (
@ -553,7 +554,7 @@ pub enum UiPage {
Error, Error,
} }
#[derive(Clone, PartialEq)] #[derive(PartialEq)]
pub struct ErrorState<'a> { pub struct ErrorState<'a> {
pub error: Arc<ParseError>, pub error: Arc<ParseError>,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,

View file

@ -8,7 +8,7 @@ use ratatui::widgets::{
pub struct ScrollbarTable<'a> { pub struct ScrollbarTable<'a> {
table: Table<'a>, table: Table<'a>,
scrollbar: Scrollbar<'a>, scrollbar: Scrollbar<'static>,
} }
impl<'a> ScrollbarTable<'a> { impl<'a> ScrollbarTable<'a> {
@ -19,7 +19,6 @@ impl<'a> ScrollbarTable<'a> {
C: IntoIterator, C: IntoIterator,
C::Item: Into<Constraint>, C::Item: Into<Constraint>,
{ {
let rows: Vec<_> = rows.into_iter().collect();
ScrollbarTable { ScrollbarTable {
table: Table::new(rows, widths) table: Table::new(rows, widths)
.block(Block::new().borders(Borders::RIGHT)) .block(Block::new().borders(Borders::RIGHT))