mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
start working on tui
This commit is contained in:
parent
5d0a447a21
commit
f9a1aa1415
12 changed files with 709 additions and 58 deletions
12
src/app.rs
Normal file
12
src/app.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::matcher::MatchResult;
|
||||
use cloud_log_analyser_data::StatementList;
|
||||
|
||||
pub struct App {
|
||||
pub log_statements: StatementList,
|
||||
pub matches: Vec<LogMatch>,
|
||||
}
|
||||
|
||||
pub struct LogMatch {
|
||||
pub result: MatchResult,
|
||||
pub count: usize,
|
||||
}
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
use thiserror::Error;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UiError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Log(#[from] LogError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LogError {
|
||||
#[error("Error while reading input file '{path}': {err:#}")]
|
||||
|
|
|
|||
30
src/main.rs
30
src/main.rs
|
|
@ -1,7 +1,9 @@
|
|||
use crate::app::{App, LogMatch};
|
||||
use crate::error::LogError;
|
||||
use crate::logfile::LogFile;
|
||||
use crate::logline::LogLine;
|
||||
use crate::matcher::{MatchResult, Matcher};
|
||||
use crate::ui::run_ui;
|
||||
use clap::Parser;
|
||||
use cloud_log_analyser_data::{get_statements, MAX_VERSION};
|
||||
use main_error::MainResult;
|
||||
|
|
@ -9,10 +11,12 @@ use std::collections::HashMap;
|
|||
use std::iter::once;
|
||||
use std::ops::AddAssign;
|
||||
|
||||
mod app;
|
||||
mod error;
|
||||
mod logfile;
|
||||
mod logline;
|
||||
mod matcher;
|
||||
mod ui;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
|
|
@ -66,25 +70,19 @@ fn main() -> MainResult {
|
|||
}
|
||||
}
|
||||
|
||||
if args.unmatched {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut counts: Vec<(_, _)> = counts.into_iter().collect();
|
||||
counts.sort_by_key(|(_, count)| *count);
|
||||
counts.reverse();
|
||||
for (match_result, count) in counts {
|
||||
println!("{}: {}", match_result.display(&statements), count);
|
||||
}
|
||||
if unmatched_total > 0 {
|
||||
eprintln!("\n{unmatched_total} lines couldn't be matched:");
|
||||
for (app, count) in unmatched_counts {
|
||||
eprintln!("\t{app}: {count}");
|
||||
}
|
||||
}
|
||||
if error_count > 0 {
|
||||
eprintln!("\n{error_count} lines failed to parse as valid log json");
|
||||
}
|
||||
|
||||
let app = App {
|
||||
log_statements: statements,
|
||||
matches: counts
|
||||
.into_iter()
|
||||
.map(|(result, count)| LogMatch { result, count })
|
||||
.collect(),
|
||||
};
|
||||
|
||||
run_ui(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use crate::logline::LogLine;
|
||||
use cloud_log_analyser_data::{LogLevel, LoggingStatement, StatementList};
|
||||
use itertools::Either;
|
||||
use regex::Regex;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::iter::once;
|
||||
|
||||
pub struct LogMatch {
|
||||
level: LogLevel,
|
||||
|
|
@ -90,15 +92,35 @@ pub enum MatchResult {
|
|||
}
|
||||
|
||||
impl MatchResult {
|
||||
pub fn display<'a>(&'a self, log_statements: &'a StatementList) -> impl Display + 'a {
|
||||
pub fn display<'a>(
|
||||
&'a self,
|
||||
log_statements: &'a StatementList,
|
||||
max_length: usize,
|
||||
) -> impl Display + 'a {
|
||||
MatchResultDisplay {
|
||||
max_length,
|
||||
log_statements,
|
||||
result: &self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
MatchResult::Single(_) => 1,
|
||||
MatchResult::List(list) => list.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
match self {
|
||||
MatchResult::Single(index) => Either::Left(once(*index)),
|
||||
MatchResult::List(list) => Either::Right(list.iter().copied()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MatchResultDisplay<'a> {
|
||||
max_length: usize,
|
||||
log_statements: &'a StatementList,
|
||||
result: &'a MatchResult,
|
||||
}
|
||||
|
|
@ -110,17 +132,20 @@ impl Display for MatchResultDisplay<'_> {
|
|||
if let Some(statement) = self.log_statements.get(*index) {
|
||||
write!(f, "{statement}")
|
||||
} else {
|
||||
write!(f, "unknown statement")
|
||||
write!(f, "«unknown statement»")
|
||||
}
|
||||
}
|
||||
MatchResult::List(list) => {
|
||||
writeln!(f, "{} possible matches:", list.len())?;
|
||||
for index in list {
|
||||
// todo: max length
|
||||
for (i, index) in list.iter().enumerate() {
|
||||
if let Some(statement) = self.log_statements.get(*index) {
|
||||
writeln!(f, " {statement}")?;
|
||||
if i > 0 {
|
||||
write!(f, " or ")?;
|
||||
}
|
||||
write!(f, "{statement}\n")?;
|
||||
}
|
||||
}
|
||||
write!(f, " ")
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/ui/match_list.rs
Normal file
50
src/ui/match_list.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::app::{App, LogMatch};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::palette::tailwind;
|
||||
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
||||
use std::fmt::Write;
|
||||
|
||||
pub fn match_list(app: &App) -> Table {
|
||||
let header_style = Style::default()
|
||||
.bg(tailwind::BLACK)
|
||||
.fg(tailwind::GREEN.c600);
|
||||
let selected_style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.bg(tailwind::BLACK)
|
||||
.fg(tailwind::GREEN.c600);
|
||||
|
||||
let header = ["Statement", "File", "Line", "Count"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(header_style)
|
||||
.height(1);
|
||||
|
||||
let widths = [
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Min(10),
|
||||
Constraint::Min(10),
|
||||
];
|
||||
let table = Table::new(
|
||||
app.matches.iter().map(|result| log_row(result, app)),
|
||||
widths,
|
||||
)
|
||||
.header(header)
|
||||
.highlight_style(selected_style)
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
table
|
||||
}
|
||||
|
||||
fn log_row<'a>(result: &LogMatch, app: &'a App) -> Row<'a> {
|
||||
let mut message = String::new();
|
||||
let mut paths = String::new();
|
||||
let mut lines = String::new();
|
||||
for index in result.result.iter() {
|
||||
let statement = app.log_statements.get(index).expect("invalid match index");
|
||||
writeln!(&mut message, "{}", statement.message()).unwrap();
|
||||
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)
|
||||
}
|
||||
64
src/ui/mod.rs
Normal file
64
src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use crate::app::App;
|
||||
use crate::error::UiError;
|
||||
use crate::ui::match_list::match_list;
|
||||
use crate::ui::state::{UiEvent, UiState};
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use ratatui::crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::crossterm::{event, ExecutableCommand};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::Terminal;
|
||||
use std::io;
|
||||
use std::io::stdout;
|
||||
|
||||
mod match_list;
|
||||
mod state;
|
||||
|
||||
pub fn run_ui(app: App) -> Result<(), UiError> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
|
||||
let mut ui_state = UiState::default();
|
||||
|
||||
while !matches!(ui_state, UiState::Quit) {
|
||||
terminal.draw(|frame| ui(frame, &app, &mut ui_state))?;
|
||||
if let Some(event) = handle_events()? {
|
||||
ui_state = ui_state.process(event, &app);
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events() -> io::Result<Option<UiEvent>> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == event::KeyEventKind::Press {
|
||||
return Ok(match key.code {
|
||||
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
Some(UiEvent::Quit)
|
||||
}
|
||||
KeyCode::Char('q') => Some(UiEvent::Quit),
|
||||
KeyCode::Esc => Some(UiEvent::Back),
|
||||
KeyCode::Down => Some(UiEvent::Down),
|
||||
KeyCode::Up => Some(UiEvent::Up),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||
match state {
|
||||
UiState::Quit => {}
|
||||
UiState::MatchList { table_state } => {
|
||||
frame.render_stateful_widget(match_list(app), frame.size(), table_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/ui/state.rs
Normal file
63
src/ui/state.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use crate::app::App;
|
||||
use ratatui::widgets::TableState;
|
||||
use table_state::TableStateExt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UiState {
|
||||
MatchList { table_state: TableState },
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
fn default() -> Self {
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(0));
|
||||
UiState::MatchList { table_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
pub fn process(self, event: UiEvent, app: &App) -> UiState {
|
||||
match (self, event) {
|
||||
(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 }
|
||||
}
|
||||
(UiState::MatchList { mut table_state }, UiEvent::Up) => {
|
||||
table_state.up(app.matches.len());
|
||||
UiState::MatchList { table_state }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UiEvent {
|
||||
Quit,
|
||||
Back,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
mod table_state {
|
||||
use ratatui::widgets::TableState;
|
||||
|
||||
pub trait TableStateExt {
|
||||
fn up(&mut self, count: usize);
|
||||
fn down(&mut self, count: usize);
|
||||
}
|
||||
|
||||
impl TableStateExt for TableState {
|
||||
fn up(&mut self, count: usize) {
|
||||
let current = self.selected().unwrap_or(0);
|
||||
self.select(Some(if current == 0 { count - 1 } else { current - 1 }))
|
||||
}
|
||||
|
||||
fn down(&mut self, count: usize) {
|
||||
let current = self.selected().unwrap_or(0);
|
||||
self.select(Some((current + 1).rem_euclid(count)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue