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,5 +1,5 @@
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::table::ScrollbarTable;
use ratatui::prelude::*;
@ -8,12 +8,14 @@ use std::borrow::Cow;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub fn grouping_list<'a, G: GroupingResult>(
items: &'a [LogGrouping<'a, G>],
pub fn grouping_list<'a>(
items: &[LogGrouping<'a>],
ui: &GroupingUi,
time_range: RangeInclusive<OffsetDateTime>,
filter: &Filter,
) -> ScrollbarTable<'a> {
let header = G::Grouping::HEADER
let header = ui
.header
.iter()
.copied()
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
@ -25,12 +27,15 @@ pub fn grouping_list<'a, G: GroupingResult>(
.style(TABLE_HEADER_STYLE)
.height(1);
let items: Vec<Row<'a>> = items
.iter()
.filter(|result| result.matches(filter))
.map(|result| grouped_row(result, time_range.clone()))
.collect();
ScrollbarTable::new(
items
.iter()
.filter(|result| result.matches(filter))
.map(|result| grouped_row(result, time_range.clone())),
G::Grouping::WIDTHS
items,
ui.widths
.iter()
.copied()
.chain([Constraint::Length(10), Constraint::Min(10)]),
@ -38,14 +43,14 @@ pub fn grouping_list<'a, G: GroupingResult>(
.header(header)
}
fn grouped_row<'a, G: GroupingResult>(
grouping: &'a LogGrouping<'a, G>,
fn grouped_row<'a>(
grouping: &LogGrouping<'a>,
time_range: RangeInclusive<OffsetDateTime>,
) -> Row<'a> {
if let Some(match_result) = &grouping.result {
let grouping_columns = match_result.render();
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()),
]);
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(""),
Text::from(""),
Text::from(grouping.sparkline(time_range)),
Text::from(grouping.sparkline(time_range).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::state::{Mode, UiPage, UiState};
use ratatui::crossterm::event;
@ -30,7 +29,7 @@ pub enum PopMode {
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))? {
match event::read()? {
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::ScrollDown => Some(UiEvent::Scroll(1)),
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,
})

View file

@ -1,6 +1,6 @@
use crate::app::App;
use crate::error::UiError;
use crate::matcher::MatchResult;
use crate::grouping::LogGrouping;
use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list;
use crate::ui::footer::footer;
@ -38,12 +38,12 @@ mod state;
pub mod style;
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();
let mut terminal = init_tui()?;
terminal.clear().ok();
let mut ui_state: UiState = UiState::new(&app);
let mut ui_state: UiState = UiState::new(matches);
let mut update = true;
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))?;
}
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);
}
}
@ -84,9 +84,9 @@ pub fn restore_tui() -> io::Result<()> {
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()) {
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() {
Some(selected)
} else {
@ -112,16 +112,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state {
UiState::Quit => {}
UiState::GroupList(GroupListState {
items,
table_state,
filter,
ui,
..
}) => {
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_stateful_widget(
grouping_list::<MatchResult>(app.matches, app.time_range(), filter),
grouping_list(items, ui, app.time_range(), filter),
layout[1],
table_state,
);

View file

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

View file

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