mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 10:04:12 +02:00
more tui
This commit is contained in:
parent
f9a1aa1415
commit
6e0c662fb4
13 changed files with 263 additions and 45 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -207,6 +207,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tinystr",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1000,6 +1001,16 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ cloud-log-analyser-data = { version = "0.1.0", path = "./data" }
|
||||||
zip = "2.1.5"
|
zip = "2.1.5"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
ratatui = "0.27.0"
|
ratatui = "0.27.0"
|
||||||
|
tinystr = { version = "0.7.6", features = ["serde"] }
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
@ -38,6 +38,21 @@ impl LogLevel {
|
||||||
};
|
};
|
||||||
matcher_level == *self || matcher_level == LogLevel::Exception || *self == LogLevel::Unknown
|
matcher_level == *self || matcher_level == LogLevel::Exception || *self == LogLevel::Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LogLevel::Debug => "debug",
|
||||||
|
LogLevel::Info => "info",
|
||||||
|
LogLevel::Notice => "notice",
|
||||||
|
LogLevel::Warn => "warn",
|
||||||
|
LogLevel::Error => "error",
|
||||||
|
LogLevel::Alert => "alert",
|
||||||
|
LogLevel::Critical => "critical",
|
||||||
|
LogLevel::Emergency => "emergency",
|
||||||
|
LogLevel::Exception => "exception",
|
||||||
|
LogLevel::Unknown => "log",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
|
|
||||||
23
src/app.rs
23
src/app.rs
|
|
@ -1,12 +1,33 @@
|
||||||
|
use crate::logline::LogLine;
|
||||||
use crate::matcher::MatchResult;
|
use crate::matcher::MatchResult;
|
||||||
use cloud_log_analyser_data::StatementList;
|
use cloud_log_analyser_data::StatementList;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
pub lines: Vec<LogLine>,
|
||||||
pub log_statements: StatementList,
|
pub log_statements: StatementList,
|
||||||
pub matches: Vec<LogMatch>,
|
pub matches: Vec<LogMatch>,
|
||||||
|
pub error_count: usize,
|
||||||
|
pub unmatched: Vec<UnMatched>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LogMatch {
|
pub struct LogMatch {
|
||||||
pub result: MatchResult,
|
pub result: MatchResult,
|
||||||
pub count: usize,
|
pub lines: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogMatch {
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.lines.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UnMatched {
|
||||||
|
pub app: String,
|
||||||
|
pub lines: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnMatched {
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.lines.len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,32 @@
|
||||||
use cloud_log_analyser_data::LogLevel;
|
use cloud_log_analyser_data::LogLevel;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Cow;
|
use tinystr::TinyAsciiStr;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LogLine<'a> {
|
pub struct LogLine {
|
||||||
pub version: &'a str,
|
pub version: TinyAsciiStr<8>,
|
||||||
pub level: LogLevel,
|
pub level: LogLevel,
|
||||||
pub message: Cow<'a, str>,
|
pub message: String,
|
||||||
pub exception: Option<Exception<'a>>,
|
pub exception: Option<Exception>,
|
||||||
pub app: Cow<'a, str>,
|
pub app: TinyAsciiStr<16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogLine<'_> {
|
impl LogLine {
|
||||||
pub fn major_version(&self) -> Option<u32> {
|
pub fn major_version(&self) -> Option<u32> {
|
||||||
let major = self
|
let major = self
|
||||||
.version
|
.version
|
||||||
.split_once('.')
|
.split_once('.')
|
||||||
.map(|(major, _)| major)
|
.map(|(major, _)| major)
|
||||||
.unwrap_or(self.version);
|
.unwrap_or(self.version.as_str());
|
||||||
major.parse().ok()
|
major.parse().ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct Exception<'a> {
|
pub struct Exception {
|
||||||
pub exception: Cow<'a, str>,
|
pub exception: String,
|
||||||
pub file: Cow<'a, str>,
|
pub file: String,
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
pub previous: Option<Box<Exception<'a>>>,
|
pub previous: Option<Box<Exception>>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
src/main.rs
41
src/main.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::app::{App, LogMatch};
|
use crate::app::{App, LogMatch, UnMatched};
|
||||||
use crate::error::LogError;
|
use crate::error::LogError;
|
||||||
use crate::logfile::LogFile;
|
use crate::logfile::LogFile;
|
||||||
use crate::logline::LogLine;
|
use crate::logline::LogLine;
|
||||||
|
|
@ -9,7 +9,6 @@ use cloud_log_analyser_data::{get_statements, MAX_VERSION};
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::ops::AddAssign;
|
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
@ -34,7 +33,7 @@ fn main() -> MainResult {
|
||||||
})?;
|
})?;
|
||||||
let mut lines = log_file.iter();
|
let mut lines = log_file.iter();
|
||||||
|
|
||||||
let mut counts: HashMap<MatchResult, usize> = HashMap::new();
|
let mut counts: HashMap<MatchResult, Vec<usize>> = HashMap::new();
|
||||||
let first = lines.next().unwrap();
|
let first = lines.next().unwrap();
|
||||||
let first_parsed: LogLine = serde_json::from_str(&first).unwrap();
|
let first_parsed: LogLine = serde_json::from_str(&first).unwrap();
|
||||||
|
|
||||||
|
|
@ -43,8 +42,9 @@ fn main() -> MainResult {
|
||||||
|
|
||||||
let lines = once(first).chain(lines);
|
let lines = once(first).chain(lines);
|
||||||
let mut error_count = 0;
|
let mut error_count = 0;
|
||||||
let mut unmatched_total = 0;
|
let mut unmatched_counts: HashMap<String, Vec<usize>> = HashMap::new();
|
||||||
let mut unmatched_counts = HashMap::new();
|
let mut parsed_lines = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
for line in lines {
|
for line in lines {
|
||||||
if line.starts_with('{') {
|
if line.starts_with('{') {
|
||||||
let parsed = match serde_json::from_str::<LogLine>(&line) {
|
let parsed = match serde_json::from_str::<LogLine>(&line) {
|
||||||
|
|
@ -55,31 +55,42 @@ fn main() -> MainResult {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(index) = matcher.match_log(&parsed) {
|
if let Some(index) = matcher.match_log(&parsed) {
|
||||||
counts.entry(index).or_default().add_assign(1);
|
counts.entry(index).or_default().push(i);
|
||||||
} else {
|
} else {
|
||||||
if args.unmatched && parsed.app != "PHP" {
|
if args.unmatched && parsed.app != "PHP" {
|
||||||
println!("{} :{:?}", parsed.message, &parsed.exception);
|
println!("{} :{:?}", parsed.message, &parsed.exception);
|
||||||
}
|
}
|
||||||
unmatched_total += 1;
|
if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_str()) {
|
||||||
if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_ref()) {
|
entry.push(i)
|
||||||
*entry += 1;
|
|
||||||
} else {
|
} else {
|
||||||
unmatched_counts.insert(parsed.app.to_string(), 1);
|
unmatched_counts.insert(parsed.app.to_string(), vec![i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parsed_lines.push(parsed);
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut counts: Vec<(_, _)> = counts.into_iter().collect();
|
let mut matched_lines: Vec<(_, _)> = counts.into_iter().collect();
|
||||||
counts.sort_by_key(|(_, count)| *count);
|
matched_lines.sort_by_key(|(_, lines)| lines.len());
|
||||||
counts.reverse();
|
matched_lines.reverse();
|
||||||
|
|
||||||
|
let mut unmatched_lines: Vec<(_, _)> = unmatched_counts.into_iter().collect();
|
||||||
|
unmatched_lines.sort_by_key(|(_, lines)| lines.len());
|
||||||
|
unmatched_lines.reverse();
|
||||||
|
|
||||||
let app = App {
|
let app = App {
|
||||||
|
lines: parsed_lines,
|
||||||
log_statements: statements,
|
log_statements: statements,
|
||||||
matches: counts
|
matches: matched_lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(result, count)| LogMatch { result, count })
|
.map(|(result, lines)| LogMatch { result, lines })
|
||||||
.collect(),
|
.collect(),
|
||||||
|
unmatched: unmatched_lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|(app, lines)| UnMatched { app, lines })
|
||||||
|
.collect(),
|
||||||
|
error_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
run_ui(app)?;
|
run_ui(app)?;
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ impl Matcher {
|
||||||
if let Some(exception) = &log.exception {
|
if let Some(exception) = &log.exception {
|
||||||
for (i, log_match) in self.matches.iter().enumerate() {
|
for (i, log_match) in self.matches.iter().enumerate() {
|
||||||
if log_match.line == exception.line
|
if log_match.line == exception.line
|
||||||
&& log_match.exception.as_deref() == Some(exception.exception.as_ref())
|
&& log_match.exception == Some(exception.exception.as_str())
|
||||||
&& exception.file.as_ref().ends_with(log_match.path)
|
&& exception.file.ends_with(log_match.path)
|
||||||
{
|
{
|
||||||
return Some(MatchResult::Single(i));
|
return Some(MatchResult::Single(i));
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +60,7 @@ impl Matcher {
|
||||||
for (i, log_match) in self.matches.iter().enumerate() {
|
for (i, log_match) in self.matches.iter().enumerate() {
|
||||||
if log_match.has_meaningful_message {
|
if log_match.has_meaningful_message {
|
||||||
if log.level.matches(log_match.level)
|
if log.level.matches(log_match.level)
|
||||||
&& log_match.pattern.is_match(log.message.as_ref())
|
&& log_match.pattern.is_match(log.message.as_str())
|
||||||
&& log_match.pattern_length >= best_length
|
&& log_match.pattern_length >= best_length
|
||||||
{
|
{
|
||||||
if log_match.pattern_length > best_length {
|
if log_match.pattern_length > best_length {
|
||||||
|
|
|
||||||
41
src/ui/footer.rs
Normal file
41
src/ui/footer.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::ui::state::UiPage;
|
||||||
|
use ratatui::layout::Constraint;
|
||||||
|
use ratatui::prelude::Style;
|
||||||
|
use ratatui::style::palette::tailwind;
|
||||||
|
use ratatui::widgets::{Row, Table};
|
||||||
|
|
||||||
|
pub fn footer(app: &App, page: UiPage) -> Table {
|
||||||
|
let footer_style = Style::default()
|
||||||
|
.bg(tailwind::BLACK)
|
||||||
|
.fg(tailwind::GREEN.c600);
|
||||||
|
|
||||||
|
let widths = [
|
||||||
|
Constraint::Percentage(100),
|
||||||
|
Constraint::Min(25),
|
||||||
|
Constraint::Min(20),
|
||||||
|
];
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
[Row::new([
|
||||||
|
help(page).to_string(),
|
||||||
|
format!(
|
||||||
|
"{} unmatched items",
|
||||||
|
app.unmatched
|
||||||
|
.iter()
|
||||||
|
.map(|unmatched| unmatched.count())
|
||||||
|
.sum::<usize>()
|
||||||
|
),
|
||||||
|
format!("{} parse errors", app.error_count),
|
||||||
|
])],
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
.style(footer_style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help(page: UiPage) -> &'static str {
|
||||||
|
match page {
|
||||||
|
UiPage::MatchList => "«Q» Exit - «Enter» Select",
|
||||||
|
UiPage::Match => "«Q» Exit - «Esc» Back",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,15 @@
|
||||||
use crate::app::{App, LogMatch};
|
use crate::app::{App, LogMatch};
|
||||||
|
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE};
|
||||||
use ratatui::prelude::*;
|
use ratatui::prelude::*;
|
||||||
use ratatui::style::palette::tailwind;
|
|
||||||
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
pub fn match_list(app: &App) -> Table {
|
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"]
|
let header = ["Statement", "File", "Line", "Count"]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Cell::from)
|
.map(Cell::from)
|
||||||
.collect::<Row>()
|
.collect::<Row>()
|
||||||
.style(header_style)
|
.style(TABLE_HEADER_STYLE)
|
||||||
.height(1);
|
.height(1);
|
||||||
|
|
||||||
let widths = [
|
let widths = [
|
||||||
|
|
@ -31,7 +23,7 @@ pub fn match_list(app: &App) -> Table {
|
||||||
widths,
|
widths,
|
||||||
)
|
)
|
||||||
.header(header)
|
.header(header)
|
||||||
.highlight_style(selected_style)
|
.highlight_style(TABLE_SELECTED_STYLE)
|
||||||
.highlight_spacing(HighlightSpacing::Always);
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
table
|
table
|
||||||
}
|
}
|
||||||
|
|
@ -46,5 +38,5 @@ fn log_row<'a>(result: &LogMatch, app: &'a App) -> Row<'a> {
|
||||||
writeln!(&mut paths, "{}", statement.path()).unwrap();
|
writeln!(&mut paths, "{}", statement.path()).unwrap();
|
||||||
writeln!(&mut lines, "{}", statement.line).unwrap();
|
writeln!(&mut lines, "{}", statement.line).unwrap();
|
||||||
}
|
}
|
||||||
Row::new([message, paths, lines, result.count.to_string()]).height(result.result.len() as u16)
|
Row::new([message, paths, lines, result.count().to_string()]).height(result.result.len() as u16)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::error::UiError;
|
use crate::error::UiError;
|
||||||
|
use crate::ui::footer::footer;
|
||||||
use crate::ui::match_list::match_list;
|
use crate::ui::match_list::match_list;
|
||||||
|
use crate::ui::single_match::single_match;
|
||||||
use crate::ui::state::{UiEvent, UiState};
|
use crate::ui::state::{UiEvent, UiState};
|
||||||
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
|
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
use ratatui::crossterm::terminal::{
|
use ratatui::crossterm::terminal::{
|
||||||
|
|
@ -12,8 +14,11 @@ use ratatui::Terminal;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
|
|
||||||
|
mod footer;
|
||||||
mod match_list;
|
mod match_list;
|
||||||
|
mod single_match;
|
||||||
mod state;
|
mod state;
|
||||||
|
pub mod style;
|
||||||
|
|
||||||
pub fn run_ui(app: App) -> Result<(), UiError> {
|
pub fn run_ui(app: App) -> Result<(), UiError> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
@ -46,6 +51,7 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
|
||||||
KeyCode::Esc => Some(UiEvent::Back),
|
KeyCode::Esc => Some(UiEvent::Back),
|
||||||
KeyCode::Down => Some(UiEvent::Down),
|
KeyCode::Down => Some(UiEvent::Down),
|
||||||
KeyCode::Up => Some(UiEvent::Up),
|
KeyCode::Up => Some(UiEvent::Up),
|
||||||
|
KeyCode::Enter => Some(UiEvent::Select),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -55,10 +61,25 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
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)])
|
||||||
|
.split(frame.size());
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
UiState::Quit => {}
|
UiState::Quit => {}
|
||||||
UiState::MatchList { table_state } => {
|
UiState::MatchList { table_state } => {
|
||||||
frame.render_stateful_widget(match_list(app), frame.size(), table_state);
|
frame.render_stateful_widget(match_list(app), layout[0], table_state);
|
||||||
|
frame.render_widget(footer(app, page), layout[1]);
|
||||||
|
}
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/ui/single_match.rs
Normal file
35
src/ui/single_match.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::app::{App, LogMatch};
|
||||||
|
use crate::logline::LogLine;
|
||||||
|
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE};
|
||||||
|
use ratatui::layout::Constraint;
|
||||||
|
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
|
||||||
|
|
||||||
|
pub fn single_match<'a>(app: &'a App, matches: &'a LogMatch) -> Table<'a> {
|
||||||
|
let lines = matches.lines.iter().map(|i| &app.lines[*i]);
|
||||||
|
|
||||||
|
let header = ["Level", "App", "Message"]
|
||||||
|
.into_iter()
|
||||||
|
.map(Cell::from)
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(TABLE_HEADER_STYLE)
|
||||||
|
.height(1);
|
||||||
|
|
||||||
|
let widths = [
|
||||||
|
Constraint::Min(10),
|
||||||
|
Constraint::Min(20),
|
||||||
|
Constraint::Percentage(100),
|
||||||
|
];
|
||||||
|
let table = Table::new(lines.map(|line| log_row(line)), widths)
|
||||||
|
.header(header)
|
||||||
|
.highlight_style(TABLE_SELECTED_STYLE)
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_row(line: &LogLine) -> Row {
|
||||||
|
Row::new([
|
||||||
|
line.level.as_str(),
|
||||||
|
line.app.as_str(),
|
||||||
|
line.message.as_str(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,13 @@ use table_state::TableStateExt;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum UiState {
|
pub enum UiState {
|
||||||
MatchList { table_state: TableState },
|
MatchList {
|
||||||
|
table_state: TableState,
|
||||||
|
},
|
||||||
|
Match {
|
||||||
|
selected: usize,
|
||||||
|
table_state: TableState,
|
||||||
|
},
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,6 +23,13 @@ impl Default for UiState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiState {
|
impl UiState {
|
||||||
|
pub fn page(&self) -> UiPage {
|
||||||
|
match self {
|
||||||
|
UiState::Quit | UiState::MatchList { .. } => UiPage::MatchList,
|
||||||
|
UiState::Match { .. } => UiPage::Match,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process(self, event: UiEvent, app: &App) -> UiState {
|
pub fn process(self, event: UiEvent, app: &App) -> UiState {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(UiState::Quit, _) => UiState::Quit,
|
(UiState::Quit, _) => UiState::Quit,
|
||||||
|
|
@ -30,6 +43,52 @@ impl UiState {
|
||||||
table_state.up(app.matches.len());
|
table_state.up(app.matches.len());
|
||||||
UiState::MatchList { table_state }
|
UiState::MatchList { table_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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
UiState::Match {
|
||||||
|
selected: index, ..
|
||||||
|
},
|
||||||
|
UiEvent::Back,
|
||||||
|
) => {
|
||||||
|
let mut table_state = TableState::default();
|
||||||
|
table_state.select(Some(index));
|
||||||
|
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::Match {
|
||||||
|
mut table_state,
|
||||||
|
selected,
|
||||||
|
},
|
||||||
|
UiEvent::Up,
|
||||||
|
) => {
|
||||||
|
table_state.up(app.matches[selected].count());
|
||||||
|
UiState::Match {
|
||||||
|
table_state,
|
||||||
|
selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(state @ UiState::Match { .. }, _) => state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +98,12 @@ pub enum UiEvent {
|
||||||
Back,
|
Back,
|
||||||
Up,
|
Up,
|
||||||
Down,
|
Down,
|
||||||
|
Select,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UiPage {
|
||||||
|
MatchList,
|
||||||
|
Match,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod table_state {
|
mod table_state {
|
||||||
|
|
|
||||||
5
src/ui/style.rs
Normal file
5
src/ui/style.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
use ratatui::prelude::Style;
|
||||||
|
use ratatui::style::palette::tailwind;
|
||||||
|
|
||||||
|
pub const TABLE_HEADER_STYLE: Style = Style::new().bg(tailwind::BLACK).fg(tailwind::GREEN.c600);
|
||||||
|
pub const TABLE_SELECTED_STYLE: Style = Style::new().fg(tailwind::BLACK).bg(tailwind::GREEN.c600);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue