more grouping prep

This commit is contained in:
Robin Appelman 2025-08-10 12:35:23 +02:00
commit 0fa90510cb
14 changed files with 251 additions and 185 deletions

View file

@ -1,4 +1,4 @@
use crate::grouping::LogGrouping;
use crate::grouping::{group_lines_by, FilterGrouping, LogGrouping};
use crate::logfile::{LogFile, LogLine, LogLineNumber};
use crate::logs::ParsedLogs;
use crate::matcher::MatchResult;
@ -12,8 +12,7 @@ use time::OffsetDateTime;
pub struct App<'logs> {
pub lines: &'logs ParsedLogs<'logs>,
pub matches: Vec<LogGrouping<'logs, MatchResult>>,
pub all: LogGrouping<'logs, MatchResult>,
pub matches: &'logs [LogGrouping<'logs, MatchResult>],
pub log_file: &'logs LogFile,
pub unmatched_count: usize,
}
@ -27,15 +26,15 @@ impl<'logs> App<'logs> {
self.log_file.nth(index)
}
pub fn lines_by_request<'a: 'logs>(
&'a self,
request_id: &'a str,
) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> {
pub fn lines_by_request<'s>(
&self,
request_id: &'s str,
) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'logs, 's> {
self.lines
.find_lines(move |line| line.request_id == request_id)
}
pub fn error_lines(&self) -> impl Iterator<Item = (&'logs str, &JsonError)> {
pub fn error_lines(&self) -> impl Iterator<Item = (&'logs str, &'logs JsonError)> + use<'logs> {
self.lines.errors().iter().map(|(line_number, error)| {
(self.log_file.nth(*line_number).unwrap_or_default(), error)
})
@ -96,6 +95,15 @@ impl<'logs> LineSet<'logs> {
.parts()
.all(|filter_part| filter_part.is_match(&line.message))
}
pub fn iter<'a>(&'a self) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> {
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)]

View file

@ -1,26 +1,37 @@
mod unique;
use crate::app::{Filter, LineSet};
use crate::logfile::LogLine;
use crate::timegraph::{SparkLine, TimeGraph};
use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::BTreeMap;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub use unique::*;
pub trait GroupedLine {
pub trait Grouping {
const HEADER: &'static [(&'static str, Alignment)];
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)]
pub trait GroupingResult {
type Lines: GroupedLine;
type Grouping: Grouping;
type Next: FilterGrouping;
fn len(&self) -> usize;
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a, Self>
where
<Self as GroupingResult>::Lines: 'a;
fn height(&self) -> usize {
1
}
fn matches(&self, filter: &Filter) -> bool;
@ -31,53 +42,47 @@ pub struct LogGrouping<'logs, G> {
pub name: Option<&'static str>,
pub result: Option<G>,
pub count: usize,
pub all: LineSet<'logs>,
pub grouped: Vec<LineSet<'logs>>,
pub lines: LineSet<'logs>,
pub by_identifier: OnceCell<Vec<LineSet<'logs>>>,
}
impl<'logs, G: GroupingResult> LogGrouping<'logs, G> {
pub fn new(result: G, lines: Vec<&'logs LogLine<'logs>>) -> Self {
let count = lines.len();
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity);
let all = LineSet::new(lines);
let lines = LineSet::new(lines);
LogGrouping {
name: None,
result: Some(result),
count,
grouped,
all,
lines,
by_identifier: OnceCell::new(),
}
}
pub fn named(name: &'static str, lines: Vec<&'logs LogLine<'logs>>) -> Self {
let count = lines.len();
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity);
let all = LineSet::new(lines);
let lines = LineSet::new(lines);
LogGrouping {
name: Some(name),
result: None,
count,
grouped,
all,
lines,
by_identifier: OnceCell::new(),
}
}
pub fn sparkline(&self, time_range: RangeInclusive<OffsetDateTime>) -> &SparkLine {
self.all.sparkline(time_range)
self.lines.sparkline(time_range)
}
pub fn histogram(&self, time_range: RangeInclusive<OffsetDateTime>) -> &TimeGraph {
self.all.histogram(time_range)
self.lines.histogram(time_range)
}
pub fn row_count(&self) -> usize {
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
}
pub fn iter(&self) -> impl Iterator<Item = &LineSet<'logs>> {
self.grouped.iter()
self.result.as_ref().map(|res| res.height()).unwrap_or(1)
}
pub fn matches(&self, filter: &Filter) -> bool {
@ -93,6 +98,11 @@ impl<'logs, G: GroupingResult> LogGrouping<'logs, G> {
pub fn count(&self) -> usize {
self.count
}
pub fn by_identifier(&self) -> &[LineSet<'logs>] {
self.by_identifier
.get_or_init(|| group_lines_by(self.lines.iter(), LogLine::identity))
}
}
pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>>

57
src/grouping/unique.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::app::Filter;
use crate::grouping::{FilterGrouping, Grouping, GroupingResult};
use crate::logfile::LogLine;
use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow;
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 line: &'a LogLine<'a>,
}
impl<'a> GroupingResult for UniqueGrouping<'a> {
type Grouping = UniqueLog;
type Next = UniqueLog;
fn matches(&self, filter: &Filter) -> bool {
if filter.is_empty() {
return true;
}
filter
.parts()
.all(|filter_part| filter_part.is_match(&self.line.message))
}
fn render(&self) -> impl Iterator<Item = Cow<str>> {
[
Cow::from(self.line.level.as_str()),
Cow::from(self.line.app.as_ref()),
self.line.display(),
]
.into_iter()
}
}

View file

@ -32,10 +32,10 @@ impl<'logfile> ParsedLogs<'logfile> {
&self.error_lines
}
pub fn find_lines<'a: 'logfile, F: Fn(&'logfile LogLine<'logfile>) -> bool + 'logfile>(
pub fn find_lines<'a: 'f, 'f, F: Fn(&'a LogLine<'logfile>) -> bool + 'f>(
&'a self,
filter: F,
) -> impl Iterator<Item = &'logfile LogLine<'logfile>> + use<'a, 'logfile, F> {
) -> impl Iterator<Item = &'a LogLine<'logfile>> + use<'a, 'logfile, F> {
self.parsed.iter().filter(move |line| filter(line))
}

View file

@ -160,6 +160,8 @@ fn main() -> MainResult {
.map(|(result, lines)| LogGrouping::new(result, lines))
.collect();
matches.insert(0, all); // todo: seems unoptimized
let unmatched_count = unmatched.count();
if unmatched_count > 0 {
matches.push(unmatched);
@ -167,8 +169,7 @@ fn main() -> MainResult {
let app = App {
lines: &parsed_log,
matches,
all,
matches: &matches,
log_file: &log_file,
unmatched_count,
};

View file

@ -1,5 +1,5 @@
use crate::app::Filter;
use crate::grouping::{GroupedLine, GroupingResult};
use crate::grouping::{Grouping, GroupingResult, UniqueLog};
use crate::logfile::logline::{Exception, LogLine};
use crate::logfile::LineNumber;
use itertools::Either;
@ -211,7 +211,7 @@ impl From<Vec<LoggingStatementWithPathPrefix>> for MatchResult {
}
}
impl GroupedLine for LoggingStatementWithPathPrefix {
impl Grouping for LoggingStatementWithPathPrefix {
const HEADER: &'static [(&'static str, Alignment)] = &[
("Statement", Alignment::Left),
("File", Alignment::Left),
@ -222,25 +222,17 @@ impl GroupedLine for LoggingStatementWithPathPrefix {
Constraint::Percentage(70),
Constraint::Percentage(30),
Constraint::Min(6),
Constraint::Length(10),
Constraint::Min(10),
];
}
impl GroupingResult for MatchResult {
type Lines = LoggingStatementWithPathPrefix;
type Grouping = LoggingStatementWithPathPrefix;
type Next = UniqueLog;
fn len(&self) -> usize {
fn height(&self) -> usize {
self.count()
}
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a>
where
<Self as GroupingResult>::Lines: 'a,
{
self.iter()
}
fn matches(&self, filter: &Filter) -> bool {
if filter.is_empty() {
return true;
@ -269,7 +261,7 @@ impl GroupingResult for MatchResult {
let mut paths = String::new();
let mut lines = String::new();
for statement in self.lines() {
for statement in self.iter() {
writeln!(&mut message, "{}", statement.message()).ok();
writeln!(&mut paths, "{}", statement.path()).ok();
writeln!(&mut lines, "{}", statement.line()).ok();

View file

@ -1,4 +1,4 @@
use crate::app::{App, Filter};
use crate::app::Filter;
use crate::logfile::logline::{format_time, LogLine};
use crate::ui::state::GroupedLogGrouping;
use crate::ui::style::TABLE_HEADER_STYLE;
@ -10,28 +10,25 @@ use ratatui::prelude::{StatefulWidget, Widget};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
pub struct GroupedLogs<'a> {
pub struct LogsByIdentifier<'a> {
lines: &'a [&'a LogLine<'a>],
app: &'a App<'a>,
filter: &'a Filter,
grouping: GroupedLogGrouping,
}
pub fn grouped_logs<'a>(
app: &'a App<'a>,
pub fn logs_by_identifier<'a>(
lines: &'a [&'a LogLine<'a>],
filter: &'a Filter,
grouping: GroupedLogGrouping,
) -> GroupedLogs<'a> {
GroupedLogs {
) -> LogsByIdentifier<'a> {
LogsByIdentifier {
lines,
app,
filter,
grouping,
}
}
impl StatefulWidget for GroupedLogs<'_> {
impl StatefulWidget for LogsByIdentifier<'_> {
type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
@ -45,7 +42,7 @@ impl StatefulWidget for GroupedLogs<'_> {
.copied()
.filter(|line| line.matches(self.filter))
.nth(state.selected())
.unwrap_or(self.app.lines.first());
.unwrap_or(self.lines.first().unwrap());
let par = match self.grouping {
GroupedLogGrouping::Message => Paragraph::new(format!(

View file

@ -5,7 +5,7 @@ use ratatui::layout::Constraint;
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
pub fn error_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
pub fn error_list<'a>(app: &App<'a>) -> ScrollbarTable<'a> {
let header = [Text::from("Error"), Text::from("Line")]
.into_iter()
.map(Cell::from)

View file

@ -11,7 +11,7 @@ pub enum FooterParams<'a> {
FilterInput { page: UiPage, filter: &'a Filter },
}
pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> {
pub fn footer<'a>(app: &App, params: FooterParams<'a>) -> Table<'a> {
let footer_style = Style::default()
.bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600);

View file

@ -1,21 +1,19 @@
use crate::app::Filter;
use crate::grouping::{GroupedLine, GroupingResult, LogGrouping};
use crate::grouping::{Grouping, GroupingResult, LogGrouping};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use ratatui::prelude::*;
use ratatui::widgets::{Cell, Row};
use std::borrow::Cow;
use std::iter::once;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub fn grouping_list<'a, G: GroupingResult>(
all: &'a LogGrouping<'a, G>,
items: &'a [LogGrouping<'a, G>],
time_range: RangeInclusive<OffsetDateTime>,
filter: &Filter,
) -> ScrollbarTable<'a> {
let header = G::Lines::HEADER
let header = G::Grouping::HEADER
.iter()
.copied()
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
@ -28,10 +26,14 @@ pub fn grouping_list<'a, G: GroupingResult>(
.height(1);
ScrollbarTable::new(
once(all)
.chain(items.iter().filter(|result| result.matches(filter)))
items
.iter()
.filter(|result| result.matches(filter))
.map(|result| grouped_row(result, time_range.clone())),
G::Lines::WIDTHS,
G::Grouping::WIDTHS
.iter()
.copied()
.chain([Constraint::Length(10), Constraint::Min(10)]),
)
.header(header)
}
@ -46,7 +48,7 @@ fn grouped_row<'a, G: GroupingResult>(
Cow::from(grouping.sparkline(time_range)),
Cow::from(grouping.count().to_string()),
]);
Row::new(columns).height(match_result.len() as u16)
Row::new(columns).height(match_result.height() as u16)
} else {
Row::new([
Text::from(grouping.name.unwrap_or_default()),

View file

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

View file

@ -1,16 +1,17 @@
use crate::app::App;
use crate::error::UiError;
use crate::matcher::MatchResult;
use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list;
use crate::ui::footer::footer;
use crate::ui::grouped_logs::grouped_logs;
use crate::ui::grouping_list::grouping_list;
use crate::ui::histogram::UiHistogram;
use crate::ui::input::handle_events;
use crate::ui::single_group::single_group;
use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines;
use crate::ui::state::{
ErrorLinesState, ErrorState, GroupedLogsState, LogState, MatchListState, MatchState, UiState,
ErrorLinesState, ErrorState, GroupListState, GroupState, LogState, LogsByIdentifierState,
UiState,
};
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use ratatui::crossterm::terminal::{
@ -25,24 +26,24 @@ use std::io;
use std::io::stdout;
use std::panic::{set_hook, take_hook};
mod by_identifier;
mod error_list;
mod footer;
mod grouped_logs;
mod grouping_list;
mod histogram;
mod input;
mod single_group;
mod single_log;
mod single_match;
mod state;
pub mod style;
mod table;
pub fn run_ui(app: App) -> Result<(), UiError> {
pub fn run_ui<'a>(app: App<'a>) -> Result<(), UiError> {
init_panic_hook();
let mut terminal = init_tui()?;
terminal.clear().ok();
let mut ui_state = UiState::new(&app);
let mut ui_state: UiState = UiState::new(&app);
let mut update = true;
while !matches!(ui_state, UiState::Quit) {
@ -50,7 +51,7 @@ pub fn run_ui(app: App) -> Result<(), UiError> {
terminal.draw(|frame| ui(frame, &app, &mut ui_state))?;
}
update = false;
if let Some(event) = handle_events(ui_state.page(), &ui_state)? {
if let Some(event) = handle_events(ui_state.page(), &ui_state, &app)? {
(update, ui_state) = ui_state.process(event, &app);
}
}
@ -83,9 +84,9 @@ pub fn restore_tui() -> io::Result<()> {
Ok(())
}
fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> {
fn find_hit_row(row: u16, ui_state: &UiState, app: &App) -> Option<usize> {
if let Some(table_row) = row.checked_sub(ui_state.content_offset()) {
let selected = ui_state.index_for_row(table_row as usize);
let selected = ui_state.index_for_row(table_row as usize, app);
if selected < ui_state.row_count() {
Some(selected)
} else {
@ -110,28 +111,23 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state {
UiState::Quit => {}
UiState::MatchList(MatchListState {
UiState::GroupList(GroupListState {
table_state,
filter,
..
}) => {
let selected = table_state.selected();
let histogram = if selected == 0 {
app.all.histogram(app.time_range())
} else {
let log_match = &app.matches[selected - 1];
log_match.histogram(app.time_range())
};
let histogram = app.matches[selected].histogram(app.time_range());
frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget(
grouping_list::<MatchResult>(&app.all, &app.matches, app.time_range(), filter),
grouping_list::<MatchResult>(app.matches, app.time_range(), filter),
layout[1],
table_state,
);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Match(MatchState {
UiState::Group(GroupState {
result,
table_state,
filter,
@ -139,9 +135,9 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
}) => {
let selected = table_state.selected();
let selected_group = if selected == 0 {
&result.all
&result.lines
} else {
&result.grouped[selected - 1]
&result.by_identifier()[selected - 1]
};
frame.render_widget(
@ -149,13 +145,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
layout[0],
);
frame.render_stateful_widget(
grouped_lines(app, result, filter),
single_group(
app.time_range(),
&result.lines,
result.by_identifier(),
filter,
),
layout[1],
table_state,
);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::GroupedLogs(GroupedLogsState {
UiState::ByIdentifier(LogsByIdentifierState {
lines,
table_state,
filter,
@ -163,7 +164,7 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
..
}) => {
frame.render_stateful_widget(
grouped_logs(app, lines, filter, *grouping),
logs_by_identifier(lines, filter, *grouping),
layout[0].union(layout[1]),
table_state,
);

View file

@ -1,6 +1,4 @@
use crate::app::{App, Filter, LineSet};
use crate::grouping::LogGrouping;
use crate::matcher::MatchResult;
use crate::app::{Filter, LineSet};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use ratatui::buffer::Buffer;
@ -12,32 +10,35 @@ use std::iter::once;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub fn grouped_lines<'a>(
app: &'a App<'a>,
log_match: &'a LogGrouping<'a, MatchResult>,
pub fn single_group<'a>(
time_range: RangeInclusive<OffsetDateTime>,
all: &'a LineSet<'a>,
unique: &'a [LineSet<'a>],
filter: &'a Filter,
) -> SingleMatchTable<'a> {
SingleMatchTable {
app,
log_match,
) -> SingleGroup<'a> {
SingleGroup {
time_range,
all,
unique,
filter,
}
}
pub struct SingleMatchTable<'a> {
app: &'a App<'a>,
log_match: &'a LogGrouping<'a, MatchResult>,
pub struct SingleGroup<'a> {
time_range: RangeInclusive<OffsetDateTime>,
all: &'a LineSet<'a>,
unique: &'a [LineSet<'a>],
filter: &'a Filter,
}
impl StatefulWidget for SingleMatchTable<'_> {
impl StatefulWidget for SingleGroup<'_> {
type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
let grouped = &self.log_match.grouped;
let grouped = self.unique;
let header = [
Text::from("Level"),
Text::from("App"),
@ -63,8 +64,8 @@ impl StatefulWidget for SingleMatchTable<'_> {
Text::from("All lines"),
Text::from(""),
Text::from(""),
Text::from(self.log_match.sparkline(self.app.time_range())),
Text::from(self.log_match.count().to_string()),
Text::from(self.all.sparkline(self.time_range.clone())),
Text::from(self.all.len().to_string()),
]))
.chain(
grouped
@ -73,7 +74,7 @@ impl StatefulWidget for SingleMatchTable<'_> {
.enumerate()
.map(|(i, group)| {
group_row(
self.app.time_range(),
self.time_range.clone(),
group,
i.abs_diff(state.selected()) < 100,
)

View file

@ -16,9 +16,9 @@ use std::sync::Arc;
#[derive(Clone, From, PartialEq)]
pub enum UiState<'a> {
MatchList(MatchListState<'a>),
Match(MatchState<'a>),
GroupedLogs(GroupedLogsState<'a>),
GroupList(GroupListState),
Group(GroupState<'a>),
ByIdentifier(LogsByIdentifierState<'a>),
Log(LogState<'a>),
Errors(ErrorLinesState<'a>),
Error(ErrorState<'a>),
@ -32,22 +32,19 @@ pub enum Mode {
}
#[derive(Clone)]
pub struct MatchListState<'a> {
app: &'a App<'a>,
pub struct GroupListState {
pub table_state: ScrollbarTableState,
pub filter: Filter,
mode: Mode,
}
impl<'a> MatchListState<'a> {
impl GroupListState {
fn selected(&self) -> usize {
self.table_state.selected()
}
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
let result = if selected == 0 {
&app.all
} else if self.filter.is_empty() {
fn enter<'a>(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let result = if self.filter.is_empty() {
&app.matches[selected - 1]
} else {
app.matches
@ -57,8 +54,8 @@ impl<'a> MatchListState<'a> {
.unwrap_or(app.matches.last().unwrap())
};
let table_state = ScrollbarTableState::new(result.grouped.len() + 1);
UiState::Match(MatchState {
let table_state = ScrollbarTableState::new(result.by_identifier().len() + 1);
UiState::Group(GroupState {
result,
table_state,
previous: Box::new(self.into()),
@ -68,14 +65,14 @@ impl<'a> MatchListState<'a> {
}
}
impl PartialEq for MatchListState<'_> {
impl PartialEq for GroupListState {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[derive(Clone)]
pub struct MatchState<'a> {
pub struct GroupState<'a> {
pub result: &'a LogGrouping<'a, MatchResult>,
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
@ -83,7 +80,7 @@ pub struct MatchState<'a> {
mode: Mode,
}
impl<'a> MatchState<'a> {
impl<'a> GroupState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
@ -93,12 +90,12 @@ impl<'a> MatchState<'a> {
table_state.select(Some(0));
let selected_line = if selected == 0 {
&self.result.all
&self.result.lines
} else if self.filter.is_empty() {
&self.result.grouped[selected - 1]
&self.result.by_identifier()[selected - 1]
} else {
self.result
.grouped
.by_identifier()
.iter()
.filter(|grouped| grouped.matches(&self.filter))
.nth(selected - 1)
@ -106,7 +103,7 @@ impl<'a> MatchState<'a> {
};
let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState {
UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(),
table_state,
previous: Box::new(self.into()),
@ -117,7 +114,7 @@ impl<'a> MatchState<'a> {
}
}
impl PartialEq for MatchState<'_> {
impl PartialEq for GroupState<'_> {
fn eq(&self, other: &Self) -> bool {
self.result.result == other.result.result
}
@ -130,7 +127,7 @@ pub enum GroupedLogGrouping {
}
#[derive(Clone)]
pub struct GroupedLogsState<'a> {
pub struct LogsByIdentifierState<'a> {
pub lines: Cow<'a, [&'a LogLine<'a>]>,
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
@ -139,7 +136,7 @@ pub struct GroupedLogsState<'a> {
pub grouping: GroupedLogGrouping,
}
impl<'a> GroupedLogsState<'a> {
impl<'a> LogsByIdentifierState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
@ -157,7 +154,7 @@ impl<'a> GroupedLogsState<'a> {
}
}
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
fn enter(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let log = self.get_selected(selected);
let raw_line = app.get_source_line(log.line_number).unwrap();
let full_line = match parse_line_full(raw_line) {
@ -187,12 +184,12 @@ impl<'a> GroupedLogsState<'a> {
})
}
fn by_request(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
fn by_request(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let log = self.get_selected(selected);
let lines: Vec<_> = app.lines_by_request(&log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState {
UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(),
mode: Mode::Normal,
filter: Filter::default(),
@ -203,7 +200,7 @@ impl<'a> GroupedLogsState<'a> {
}
}
impl PartialEq for GroupedLogsState<'_> {
impl PartialEq for LogsByIdentifierState<'_> {
fn eq(&self, other: &Self) -> bool {
self.lines == other.lines
}
@ -230,11 +227,11 @@ pub struct LogState<'a> {
}
impl<'a> LogState<'a> {
fn by_request(self, app: &'a App<'a>) -> UiState<'a> {
fn by_request(self, app: &App<'a>) -> UiState<'a> {
let lines: Vec<_> = app.lines_by_request(&self.log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState {
UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(),
mode: Mode::Normal,
filter: Filter::default(),
@ -252,11 +249,10 @@ impl PartialEq for LogState<'_> {
}
impl<'a> UiState<'a> {
pub fn new(app: &'a App<'a>) -> Self {
pub fn new(app: &App) -> Self {
let mut table_state = TableState::default();
table_state.select(Some(0));
UiState::MatchList(MatchListState {
app,
UiState::GroupList(GroupListState {
table_state: ScrollbarTableState::new(app.match_lines()),
filter: Filter::default(),
mode: Mode::Normal,
@ -265,9 +261,9 @@ impl<'a> UiState<'a> {
pub fn page(&self) -> UiPage {
match self {
UiState::Quit | UiState::MatchList(_) => UiPage::MatchList,
UiState::Match(_) => UiPage::Match,
UiState::GroupedLogs(_) => UiPage::Logs,
UiState::Quit | UiState::GroupList(_) => UiPage::MatchList,
UiState::Group(_) => UiPage::Match,
UiState::ByIdentifier(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log,
UiState::Errors(_) => UiPage::Errors,
UiState::Error(_) => UiPage::Error,
@ -276,45 +272,45 @@ impl<'a> UiState<'a> {
pub fn mode(&self) -> Mode {
match self {
UiState::MatchList(state) => state.mode,
UiState::Match(state) => state.mode,
UiState::GroupedLogs(state) => state.mode,
UiState::GroupList(state) => state.mode,
UiState::Group(state) => state.mode,
UiState::ByIdentifier(state) => state.mode,
_ => Mode::Normal,
}
}
pub fn set_mode(&mut self, mode: Mode) {
match self {
UiState::MatchList(state) => state.mode = mode,
UiState::Match(state) => state.mode = mode,
UiState::GroupedLogs(state) => state.mode = mode,
UiState::GroupList(state) => state.mode = mode,
UiState::Group(state) => state.mode = mode,
UiState::ByIdentifier(state) => state.mode = mode,
_ => {}
}
}
pub fn filter(&self) -> Option<&Filter> {
match self {
UiState::MatchList(state) => Some(&state.filter),
UiState::Match(state) => Some(&state.filter),
UiState::GroupedLogs(state) => Some(&state.filter),
UiState::GroupList(state) => Some(&state.filter),
UiState::Group(state) => Some(&state.filter),
UiState::ByIdentifier(state) => Some(&state.filter),
_ => None,
}
}
pub fn filter_mut(&mut self) -> Option<&mut Filter> {
match self {
UiState::MatchList(state) => Some(&mut state.filter),
UiState::Match(state) => Some(&mut state.filter),
UiState::GroupedLogs(state) => Some(&mut state.filter),
UiState::GroupList(state) => Some(&mut state.filter),
UiState::Group(state) => Some(&mut state.filter),
UiState::ByIdentifier(state) => Some(&mut state.filter),
_ => None,
}
}
fn table_state(&self) -> Option<&ScrollbarTableState> {
match self {
UiState::MatchList(state) => Some(&state.table_state),
UiState::Match(state) => Some(&state.table_state),
UiState::GroupedLogs(state) => Some(&state.table_state),
UiState::GroupList(state) => Some(&state.table_state),
UiState::Group(state) => Some(&state.table_state),
UiState::ByIdentifier(state) => Some(&state.table_state),
UiState::Log(state) => Some(&state.table_state),
UiState::Errors(state) => Some(&state.table_state),
_ => None,
@ -323,9 +319,9 @@ impl<'a> UiState<'a> {
fn table_state_mut(&mut self) -> Option<&mut ScrollbarTableState> {
match self {
UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state),
UiState::GroupedLogs(state) => Some(&mut state.table_state),
UiState::GroupList(state) => Some(&mut state.table_state),
UiState::Group(state) => Some(&mut state.table_state),
UiState::ByIdentifier(state) => Some(&mut state.table_state),
UiState::Log(state) => Some(&mut state.table_state),
UiState::Errors(state) => Some(&mut state.table_state),
_ => None,
@ -348,9 +344,9 @@ impl<'a> UiState<'a> {
}
}
pub fn index_for_row(&self, row: usize) -> usize {
pub fn index_for_row(&self, row: usize, app: &App) -> usize {
match self {
UiState::MatchList(MatchListState { app, filter, .. }) => {
UiState::GroupList(GroupListState { filter, .. }) => {
let mut total_height = 0;
let match_row_counts = app
.matches
@ -381,9 +377,9 @@ impl<'a> UiState<'a> {
/// get the offset of the "main content" from the top of the screen
pub fn content_offset(&self) -> u16 {
match self {
UiState::MatchList(_) => UI_HEADER_SIZE + 1,
UiState::Match(_) => UI_HEADER_SIZE + 1,
UiState::GroupedLogs(_) => UI_HEADER_SIZE + 1,
UiState::GroupList(_) => UI_HEADER_SIZE + 1,
UiState::Group(_) => UI_HEADER_SIZE + 1,
UiState::ByIdentifier(_) => UI_HEADER_SIZE + 1,
UiState::Log(_) => 0,
UiState::Errors(_) => 0,
UiState::Error(_) => 0,
@ -391,12 +387,12 @@ impl<'a> UiState<'a> {
}
}
pub fn process(self, event: UiEvent, app: &'a App<'a>) -> (bool, UiState<'a>) {
pub fn process(self, event: UiEvent, app: &App<'a>) -> (bool, UiState<'a>) {
match (self, event) {
(UiState::Quit, _) => (true, UiState::Quit),
(_, UiEvent::Quit) => (true, UiState::Quit),
(
UiState::MatchList(MatchListState {
UiState::GroupList(GroupListState {
mode: Mode::Normal, ..
}),
UiEvent::Back,
@ -425,14 +421,14 @@ impl<'a> UiState<'a> {
}
(true, state)
}
(UiState::MatchList(state), UiEvent::Select) => {
(UiState::GroupList(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected, app))
}
(UiState::MatchList(state), UiEvent::Enter(selected)) => {
(UiState::GroupList(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app))
}
(UiState::MatchList(state), UiEvent::Errors) => {
(UiState::GroupList(state), UiEvent::Errors) => {
let table_state = ScrollbarTableState::new(app.error_count());
(
true,
@ -442,19 +438,19 @@ impl<'a> UiState<'a> {
}),
)
}
(UiState::Match(state), UiEvent::Select) => {
(UiState::Group(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected))
}
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::GroupedLogs(state), UiEvent::Select) => {
(UiState::Group(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::ByIdentifier(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected, app))
}
(UiState::GroupedLogs(state), UiEvent::Enter(selected)) => {
(UiState::ByIdentifier(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app))
}
(UiState::GroupedLogs(state), UiEvent::Copy) => {
(UiState::ByIdentifier(state), UiEvent::Copy) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
@ -462,7 +458,7 @@ impl<'a> UiState<'a> {
let line = state.lines[selected];
let raw = app.get_source_line(line.line_number).unwrap_or_default();
copy_osc(raw);
(false, UiState::GroupedLogs(state))
(false, UiState::ByIdentifier(state))
}
(UiState::Log(state), UiEvent::Copy) => {
let raw = app
@ -471,7 +467,7 @@ impl<'a> UiState<'a> {
copy_osc(raw);
(false, UiState::Log(state))
}
(UiState::GroupedLogs(state), UiEvent::ByRequest) => {
(UiState::ByIdentifier(state), UiEvent::ByRequest) => {
let selected = state.selected();
(true, state.by_request(selected, app))
}
@ -512,7 +508,7 @@ impl<'a> UiState<'a> {
(true, ui)
}
(
mut ui @ UiState::MatchList(MatchListState {
mut ui @ UiState::GroupList(GroupListState {
mode: Mode::FilterInput,
..
}),
@ -526,8 +522,8 @@ impl<'a> UiState<'a> {
}
(
UiState::Match(MatchState { previous, .. })
| UiState::GroupedLogs(GroupedLogsState { previous, .. })
UiState::Group(GroupState { previous, .. })
| UiState::ByIdentifier(LogsByIdentifierState { previous, .. })
| UiState::Log(LogState { previous, .. })
| UiState::Errors(ErrorLinesState { previous, .. }),
UiEvent::Back,