mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
histogram
This commit is contained in:
parent
e7b70fd00e
commit
13f1f31dd8
12 changed files with 395 additions and 128 deletions
|
|
@ -19,13 +19,7 @@ pub fn footer(app: &App, page: UiPage) -> Table {
|
|||
Table::new(
|
||||
[Row::new([
|
||||
help(page).to_string(),
|
||||
format!(
|
||||
"{} unmatched items",
|
||||
app.unmatched
|
||||
.iter()
|
||||
.map(|unmatched| unmatched.count())
|
||||
.sum::<usize>()
|
||||
),
|
||||
format!("{} unmatched items", app.unmatched.len()),
|
||||
format!("{} parse errors", app.error_count),
|
||||
])],
|
||||
widths,
|
||||
|
|
@ -36,6 +30,6 @@ pub fn footer(app: &App, page: UiPage) -> Table {
|
|||
fn help(page: UiPage) -> &'static str {
|
||||
match page {
|
||||
UiPage::MatchList => "«Q» Exit - «Enter» Select",
|
||||
UiPage::Match => "«Q» Exit - «Esc» Back",
|
||||
UiPage::Match | UiPage::All | UiPage::Unmatched => "«Q» Exit - «Esc» Back",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/ui/histogram.rs
Normal file
44
src/ui/histogram.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use crate::timegraph::TimeGraph;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::Sparkline;
|
||||
|
||||
pub struct UiHistogram<'a> {
|
||||
data: &'a TimeGraph,
|
||||
}
|
||||
|
||||
impl<'a> UiHistogram<'a> {
|
||||
pub fn new(data: &'a TimeGraph) -> Self {
|
||||
UiHistogram { data }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for UiHistogram<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let values = self.data.counts(area.width as usize);
|
||||
let sparkline = Sparkline::default().data(&values);
|
||||
sparkline.render(area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
const SPARKS: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
pub fn sparkline(values: &[u64]) -> String {
|
||||
let max = values.iter().copied().max().unwrap() as f64;
|
||||
let len = SPARKS.len() as f64 - 1.0;
|
||||
values
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|val| {
|
||||
let rel = val as f64 / max;
|
||||
SPARKS[(rel * len) as usize]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sparkline() {
|
||||
assert_eq!(" ▇█", sparkline(&[0, 900, 1000]));
|
||||
}
|
||||
|
|
@ -1,31 +1,62 @@
|
|||
use crate::app::{App, LogMatch};
|
||||
use crate::ui::histogram::sparkline;
|
||||
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE};
|
||||
use itertools::Either;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
||||
use std::fmt::Write;
|
||||
use std::iter::{empty, once};
|
||||
|
||||
pub fn match_list(app: &App) -> Table {
|
||||
let header = ["Statement", "File", "Line", "Count"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(TABLE_HEADER_STYLE)
|
||||
.height(1);
|
||||
let header = [
|
||||
Text::from("Statement"),
|
||||
Text::from("File"),
|
||||
Text::from("Line").alignment(Alignment::Right),
|
||||
Text::from("Time"),
|
||||
Text::from("Count"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(TABLE_HEADER_STYLE)
|
||||
.height(1);
|
||||
|
||||
let widths = [
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Min(10),
|
||||
Constraint::Percentage(70),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Min(6),
|
||||
Constraint::Length(10),
|
||||
Constraint::Min(10),
|
||||
];
|
||||
let table = Table::new(
|
||||
app.matches.iter().map(|result| log_row(result, app)),
|
||||
|
||||
let all = Row::new([
|
||||
Text::from("All lines"),
|
||||
Text::from(String::new()),
|
||||
Text::from(String::new()).alignment(Alignment::Right),
|
||||
Text::from(sparkline(&app.histogram.counts(10))),
|
||||
Text::from(app.lines.len().to_string()),
|
||||
]);
|
||||
let unmatched = if app.unmatched.is_empty() {
|
||||
Either::Right(empty())
|
||||
} else {
|
||||
Either::Left(once(Row::new([
|
||||
Text::from("Unmatched lines"),
|
||||
Text::from(String::new()),
|
||||
Text::from(String::new()).alignment(Alignment::Right),
|
||||
Text::from(sparkline(&app.unmatched_histogram.counts(10))),
|
||||
Text::from(app.unmatched.len().to_string()),
|
||||
])))
|
||||
};
|
||||
|
||||
Table::new(
|
||||
once(all)
|
||||
.chain(app.matches.iter().map(|result| log_row(result, app)))
|
||||
.chain(unmatched),
|
||||
widths,
|
||||
)
|
||||
.header(header)
|
||||
.highlight_style(TABLE_SELECTED_STYLE)
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
table
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
}
|
||||
|
||||
fn log_row<'a>(result: &LogMatch, app: &'a App) -> Row<'a> {
|
||||
|
|
@ -38,5 +69,12 @@ fn log_row<'a>(result: &LogMatch, app: &'a App) -> Row<'a> {
|
|||
writeln!(&mut paths, "{}", statement.path()).unwrap();
|
||||
writeln!(&mut lines, "{}", statement.line).unwrap();
|
||||
}
|
||||
Row::new([message, paths, lines, result.count().to_string()]).height(result.result.len() as u16)
|
||||
Row::new([
|
||||
Text::from(message),
|
||||
Text::from(paths),
|
||||
Text::from(lines).alignment(Alignment::Right),
|
||||
Text::from(sparkline(&result.histogram.counts(10))),
|
||||
Text::from(result.count().to_string()),
|
||||
])
|
||||
.height(result.result.len() as u16)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use crate::app::App;
|
||||
use crate::error::UiError;
|
||||
use crate::ui::footer::footer;
|
||||
use crate::ui::histogram::UiHistogram;
|
||||
use crate::ui::match_list::match_list;
|
||||
use crate::ui::single_match::single_match;
|
||||
use crate::ui::single_match::grouped_lines;
|
||||
use crate::ui::state::{UiEvent, UiState};
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use ratatui::crossterm::terminal::{
|
||||
|
|
@ -15,6 +16,7 @@ use std::io;
|
|||
use std::io::stdout;
|
||||
|
||||
mod footer;
|
||||
mod histogram;
|
||||
mod match_list;
|
||||
mod single_match;
|
||||
mod state;
|
||||
|
|
@ -48,10 +50,12 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
|
|||
Some(UiEvent::Quit)
|
||||
}
|
||||
KeyCode::Char('q') => Some(UiEvent::Quit),
|
||||
KeyCode::Esc => Some(UiEvent::Back),
|
||||
KeyCode::Down => Some(UiEvent::Down),
|
||||
KeyCode::Up => Some(UiEvent::Up),
|
||||
KeyCode::Enter => Some(UiEvent::Select),
|
||||
KeyCode::Esc | KeyCode::Left => Some(UiEvent::Back),
|
||||
KeyCode::Down => Some(UiEvent::Down(1)),
|
||||
KeyCode::Up => Some(UiEvent::Up(1)),
|
||||
KeyCode::PageDown => Some(UiEvent::Down(10)),
|
||||
KeyCode::PageUp => Some(UiEvent::Up(10)),
|
||||
KeyCode::Enter | KeyCode::Right => Some(UiEvent::Select),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
|
@ -64,22 +68,48 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
|||
let page = state.page();
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Percentage(100), Constraint::Length(1)])
|
||||
.constraints(vec![
|
||||
Constraint::Min(5),
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
match state {
|
||||
UiState::Quit => {}
|
||||
UiState::MatchList { table_state } => {
|
||||
frame.render_stateful_widget(match_list(app), layout[0], table_state);
|
||||
frame.render_widget(footer(app, page), layout[1]);
|
||||
let selected = table_state.selected().unwrap_or(0);
|
||||
let histogram = if selected == 0 {
|
||||
&app.histogram
|
||||
} else if selected < app.matches.len() + 1 {
|
||||
let log_match = &app.matches[selected - 1];
|
||||
&log_match.histogram
|
||||
} else {
|
||||
&app.unmatched_histogram
|
||||
};
|
||||
frame.render_widget(UiHistogram::new(histogram), layout[0]);
|
||||
frame.render_stateful_widget(match_list(app), layout[1], table_state);
|
||||
frame.render_widget(footer(app, page), layout[2]);
|
||||
}
|
||||
UiState::Match {
|
||||
selected: index,
|
||||
table_state,
|
||||
} => {
|
||||
let log_match = &app.matches[*index];
|
||||
frame.render_stateful_widget(single_match(app, log_match), layout[0], table_state);
|
||||
frame.render_widget(footer(app, page), layout[1]);
|
||||
let lines = log_match.lines.iter().map(|i| &app.lines[*i]);
|
||||
|
||||
frame.render_widget(UiHistogram::new(&log_match.histogram), layout[0]);
|
||||
frame.render_stateful_widget(grouped_lines(lines), layout[1], table_state);
|
||||
frame.render_widget(footer(app, page), layout[2]);
|
||||
}
|
||||
UiState::All { table_state } => {
|
||||
frame.render_stateful_widget(grouped_lines(app.lines.iter()), layout[1], table_state);
|
||||
frame.render_widget(footer(app, page), layout[2]);
|
||||
}
|
||||
UiState::Unmatched { table_state } => {
|
||||
let lines = app.unmatched.iter().map(|i| &app.lines[*i]);
|
||||
frame.render_stateful_widget(grouped_lines(lines), layout[1], table_state);
|
||||
frame.render_widget(footer(app, page), layout[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
use crate::app::{App, LogMatch};
|
||||
use crate::logline::LogLine;
|
||||
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE, TIME_FORMAT};
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
||||
use time::format_description::well_known::Iso8601;
|
||||
|
||||
pub fn single_match<'a>(app: &'a App, matches: &'a LogMatch) -> Table<'a> {
|
||||
let lines = matches.lines.iter().map(|i| &app.lines[*i]);
|
||||
|
||||
pub fn grouped_lines<'a, I: Iterator<Item = &'a LogLine> + 'a>(lines: I) -> Table<'a> {
|
||||
let header = ["Level", "App", "Message", "Date"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
|
|
|
|||
122
src/ui/state.rs
122
src/ui/state.rs
|
|
@ -11,6 +11,12 @@ pub enum UiState {
|
|||
selected: usize,
|
||||
table_state: TableState,
|
||||
},
|
||||
All {
|
||||
table_state: TableState,
|
||||
},
|
||||
Unmatched {
|
||||
table_state: TableState,
|
||||
},
|
||||
Quit,
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +33,28 @@ impl UiState {
|
|||
match self {
|
||||
UiState::Quit | UiState::MatchList { .. } => UiPage::MatchList,
|
||||
UiState::Match { .. } => UiPage::Match,
|
||||
UiState::All { .. } => UiPage::All,
|
||||
UiState::Unmatched { .. } => UiPage::Unmatched,
|
||||
}
|
||||
}
|
||||
|
||||
fn table_state(&mut self) -> Option<&mut TableState> {
|
||||
match self {
|
||||
UiState::MatchList { table_state } => Some(table_state),
|
||||
UiState::Match { table_state, .. } => Some(table_state),
|
||||
UiState::All { table_state } => Some(table_state),
|
||||
UiState::Unmatched { table_state } => Some(table_state),
|
||||
UiState::Quit => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn table_count(&self, app: &App) -> usize {
|
||||
match self {
|
||||
UiState::MatchList { .. } => app.match_lines(),
|
||||
UiState::Match { selected, .. } => app.matches[*selected].count(),
|
||||
UiState::All { .. } => app.lines.len(),
|
||||
UiState::Unmatched { .. } => app.unmatched.len(),
|
||||
UiState::Quit => 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,21 +63,33 @@ impl UiState {
|
|||
(UiState::Quit, _) => UiState::Quit,
|
||||
(_, UiEvent::Quit) => UiState::Quit,
|
||||
(UiState::MatchList { .. }, UiEvent::Back) => UiState::Quit,
|
||||
(UiState::MatchList { mut table_state }, UiEvent::Down) => {
|
||||
table_state.down(app.matches.len());
|
||||
UiState::MatchList { table_state }
|
||||
(mut state, UiEvent::Down(step)) => {
|
||||
let count = state.table_count(app);
|
||||
if let Some(table_state) = state.table_state() {
|
||||
table_state.down(count, step)
|
||||
}
|
||||
state
|
||||
}
|
||||
(UiState::MatchList { mut table_state }, UiEvent::Up) => {
|
||||
table_state.up(app.matches.len());
|
||||
UiState::MatchList { table_state }
|
||||
(mut state, UiEvent::Up(step)) => {
|
||||
let count = state.table_count(app);
|
||||
if let Some(table_state) = state.table_state() {
|
||||
table_state.up(count, step)
|
||||
}
|
||||
state
|
||||
}
|
||||
(UiState::MatchList { table_state }, UiEvent::Select) => {
|
||||
let selected = table_state.selected().unwrap_or(0);
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(0));
|
||||
UiState::Match {
|
||||
selected,
|
||||
table_state,
|
||||
if selected == 0 {
|
||||
UiState::All { table_state }
|
||||
} else if selected == app.match_lines() - 1 {
|
||||
UiState::Unmatched { table_state }
|
||||
} else {
|
||||
UiState::Match {
|
||||
selected: selected - 1,
|
||||
table_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
|
|
@ -59,36 +99,20 @@ impl UiState {
|
|||
UiEvent::Back,
|
||||
) => {
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(index));
|
||||
table_state.select(Some(index + 1));
|
||||
UiState::MatchList { table_state }
|
||||
}
|
||||
(
|
||||
UiState::Match {
|
||||
mut table_state,
|
||||
selected,
|
||||
},
|
||||
UiEvent::Down,
|
||||
) => {
|
||||
table_state.down(app.matches[selected].count());
|
||||
UiState::Match {
|
||||
table_state,
|
||||
selected,
|
||||
}
|
||||
(UiState::All { .. }, UiEvent::Back) => {
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(0));
|
||||
UiState::MatchList { table_state }
|
||||
}
|
||||
(
|
||||
UiState::Match {
|
||||
mut table_state,
|
||||
selected,
|
||||
},
|
||||
UiEvent::Up,
|
||||
) => {
|
||||
table_state.up(app.matches[selected].count());
|
||||
UiState::Match {
|
||||
table_state,
|
||||
selected,
|
||||
}
|
||||
(UiState::Unmatched { .. }, UiEvent::Back) => {
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(app.match_lines() - 1));
|
||||
UiState::MatchList { table_state }
|
||||
}
|
||||
(state @ UiState::Match { .. }, _) => state,
|
||||
(state, _) => state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,33 +120,45 @@ impl UiState {
|
|||
pub enum UiEvent {
|
||||
Quit,
|
||||
Back,
|
||||
Up,
|
||||
Down,
|
||||
Up(usize),
|
||||
Down(usize),
|
||||
Select,
|
||||
}
|
||||
|
||||
pub enum UiPage {
|
||||
MatchList,
|
||||
Match,
|
||||
All,
|
||||
Unmatched,
|
||||
}
|
||||
|
||||
mod table_state {
|
||||
use ratatui::widgets::TableState;
|
||||
|
||||
pub trait TableStateExt {
|
||||
fn up(&mut self, count: usize);
|
||||
fn down(&mut self, count: usize);
|
||||
fn up(&mut self, count: usize, step: usize);
|
||||
fn down(&mut self, count: usize, step: usize);
|
||||
}
|
||||
|
||||
impl TableStateExt for TableState {
|
||||
fn up(&mut self, count: usize) {
|
||||
fn up(&mut self, count: usize, step: usize) {
|
||||
let current = self.selected().unwrap_or(0);
|
||||
self.select(Some(if current == 0 { count - 1 } else { current - 1 }))
|
||||
let after = if step > current {
|
||||
count - 1
|
||||
} else {
|
||||
current - step
|
||||
};
|
||||
self.select(Some(after))
|
||||
}
|
||||
|
||||
fn down(&mut self, count: usize) {
|
||||
fn down(&mut self, count: usize, step: usize) {
|
||||
let current = self.selected().unwrap_or(0);
|
||||
self.select(Some((current + 1).rem_euclid(count)))
|
||||
let after = if step >= count - current {
|
||||
0
|
||||
} else {
|
||||
current + step
|
||||
};
|
||||
self.select(Some(after))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue