diff --git a/Cargo.lock b/Cargo.lock index 49f3759..abe375c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "color-ansi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56460573dab12b4bb6dfdb0abbb021e35d6a2bbd925acd2f5a9accff5654a8e" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1163,6 +1169,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty-print" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ad516586f2191e7ce412b9b164e61ee6403638aa70e98672b978c1f448e63f" +dependencies = [ + "color-ansi", + "unicode-segmentation", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1430,6 +1446,7 @@ dependencies = [ "jiff", "logparse", "nix 0.31.1", + "pretty-print", "ratatui", "ratatui-themes", "regex", diff --git a/Cargo.toml b/Cargo.toml index 64c0aad..fe5329e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ regex = "1" crossterm = "*" dumpster = "2.1" logparse = {path = "./logparse/", version="0.2.0"} +pretty-print = "0.1" diff --git a/logparse/src/spans.rs b/logparse/src/spans.rs index b696069..ee7ecb6 100644 --- a/logparse/src/spans.rs +++ b/logparse/src/spans.rs @@ -46,9 +46,13 @@ pub struct Span<'a> { } /// Configuration options for [`into_spans`] +#[derive(Default)] +#[non_exhaustive] pub struct Config { /// Turn sequences of more than 1 space into exactly 1 space. pub collapse_space: bool, + /// Pretty print: wrap at braces etc + pub pretty_print: bool, } pub trait IntoSpans<'a>: private::IntoSpansImpl<'a> {} diff --git a/src/tui/filter.rs b/src/tui/filter.rs index db2e9f0..a09fba4 100644 --- a/src/tui/filter.rs +++ b/src/tui/filter.rs @@ -6,7 +6,7 @@ use crate::tui::{ LogViewer, input::{FieldMatcher, InputTarget}, }, - model::{FieldsName, LogEntry}, + model::{LogEntry, SpanDescriptor}, }; mod serialize_regex { @@ -56,7 +56,7 @@ impl MatcherValue { #[derive(Serialize, Deserialize, Debug)] pub enum Matcher { Field { - span: FieldsName, + span: SpanDescriptor, name: String, value: MatcherValue, }, diff --git a/src/tui/log_viewer/mod.rs b/src/tui/log_viewer/mod.rs index 5427692..13e8b18 100644 --- a/src/tui/log_viewer/mod.rs +++ b/src/tui/log_viewer/mod.rs @@ -7,7 +7,7 @@ use crate::tui::{ input::{FieldMatcher, InputState, InputTarget}, view::LogView, }, - model::{FieldsName, LogEntry, id}, + model::{LogEntry, SpanDescriptor, id}, processing::Cursor, widgets::{fieldtree, last_error::LastError}, }; @@ -54,7 +54,7 @@ impl LogViewer { } } - pub fn get_selected_field(&self) -> Option<(FieldsName, String, String)> { + pub fn get_selected_field(&self) -> Option<(SpanDescriptor, String, String)> { let (span_idx, name_idx) = self.field_state.get()?; let entry = self.selected().map(|(s, _)| s)?; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index cd2eaf2..7e07d71 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -5,6 +5,8 @@ use crossterm::{ }, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; +use itertools::Itertools; +use logparse::{self as lp, Config, into_spans, parse_input}; use ratatui_themes::{Color, Theme, ThemeName}; use std::{ borrow::Cow, @@ -23,10 +25,11 @@ use crate::tui::{ LogViewer, input::{FieldMatcher, InputState, InputTarget}, }, + model::SpanDescriptor, reader::LogfileReader, widgets::{ fieldtree::FieldTree, hyperlink::Hyperlink, items::Items, last_error::LastError, - styled::IntoStyled, + line_text::style_span, styled::IntoStyled, }, }; use ratatui::{ @@ -37,7 +40,7 @@ use ratatui::{ prelude::CrosstermBackend, style::Style, symbols::merge::MergeStrategy, - text::Line, + text::{Line, Span}, widgets::{ Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, }, @@ -64,6 +67,7 @@ const HELP_TEXT: &str = "Generic: -> / enter / l enter nested view f toggle show active filters + z zoom in on a selected field ─────────────────────────────────────────────────────── targeting logs: @@ -127,6 +131,11 @@ enum Tab { LogViewer(LogViewer), Empty, Help, + Zoom { + span: SpanDescriptor, + name: String, + value: String, + }, } impl Tab { @@ -137,6 +146,11 @@ impl Tab { (Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()), (Tab::LogViewer(_), None) => "logs".to_string(), (Tab::Help, _) => "help".to_string(), + (Tab::Zoom { span, name, .. }, _) => match span { + SpanDescriptor::Main => format!("{name}"), + SpanDescriptor::Span(s) => format!("{name} in {s}"), + SpanDescriptor::Numbered(n) => format!("{name} in span #{n}"), + }, } } } @@ -244,6 +258,7 @@ impl App { match self.tabs.last_mut().unwrap() { Tab::Help => {} Tab::Empty => {} + Tab::Zoom { .. } => {} Tab::FileChooser { files, state, @@ -327,6 +342,11 @@ impl App { KeyCode::Char('u') => lv.undo(), KeyCode::Char('r') => lv.redo(), + KeyCode::Char('z') => { + if let Some((span, name, value)) = lv.get_selected_field() { + self.push_tab(Tab::Zoom { span, name, value }); + } + } KeyCode::Tab => { lv.input_state.target(InputTarget::Fields(None)); @@ -592,6 +612,7 @@ impl Widget for &mut App { let (footer_focused, header_focused) = match self.current_tab() { Tab::Help => (false, false), + Tab::Zoom { .. } => (false, false), Tab::FileChooser { .. } => (false, true), Tab::LogViewer(lv) => { let target_fields = @@ -716,6 +737,42 @@ impl Widget for &mut App { Paragraph::new(HELP_TEXT).render(popup_area, buf); } + Tab::Zoom { value, .. } => { + Clear.render(popup_area, buf); + let popup_area = { + let block = Block::bordered() + .title_top("zoom") + .style(styles.default) + .padding(Padding::symmetric(3, 1)) + .border_style(styles.border_highlighted); + let inner = block.inner(popup_area); + block.render(popup_area, buf); + inner + }; + + if let Ok(parsed) = parse_input(&value) { + let spans = into_spans( + parsed, + Config { + collapse_space: true, + }, + ); + + let spans = spans + .into_iter() + .map(|lp::Span { text, kind }| { + let span = Span::from(text.into_owned()); + + let style = style_span(kind, styles.default, &styles); + span.style(style) + }) + .collect_vec(); + + Paragraph::new(Line::from(spans)).render(popup_area, buf); + } else { + Paragraph::new(value.clone()).render(popup_area, buf); + } + } } self.last_error.clone().styled(&styles).render(error, buf); diff --git a/src/tui/model.rs b/src/tui/model.rs index 575d238..3ae353a 100644 --- a/src/tui/model.rs +++ b/src/tui/model.rs @@ -396,7 +396,7 @@ unsafe impl TraceWith for RawLogEntry { } #[derive(PartialEq, Eq, Debug, Hash, Clone, PartialOrd, Ord, Serialize, Deserialize)] -pub enum FieldsName { +pub enum SpanDescriptor { /// The main fields (not of a span) of a log entry Main, Span(String), @@ -409,23 +409,23 @@ pub struct SpansRef<'a> { } impl<'a> SpansRef<'a> { - pub fn find(&self, span: &FieldsName, name: &str) -> Option<&String> { + pub fn find(&self, span: &SpanDescriptor, name: &str) -> Option<&String> { self.named() .find(|(name, _)| name == span) .and_then(|(_, fields)| fields.get(name)) } - pub fn named(&self) -> impl Iterator { - iter::once((FieldsName::Main, self.main)).chain(self.spans.iter().rev().enumerate().map( - |(idx, fields)| { + pub fn named(&self) -> impl Iterator { + iter::once((SpanDescriptor::Main, self.main)).chain( + self.spans.iter().rev().enumerate().map(|(idx, fields)| { ( fields .name() - .map(|i| FieldsName::Span(i.to_string())) - .unwrap_or_else(|| FieldsName::Numbered(idx)), + .map(|i| SpanDescriptor::Span(i.to_string())) + .unwrap_or_else(|| SpanDescriptor::Numbered(idx)), fields, ) - }, - )) + }), + ) } } diff --git a/src/tui/reader.rs b/src/tui/reader.rs index b4ab2a8..268d661 100644 --- a/src/tui/reader.rs +++ b/src/tui/reader.rs @@ -167,7 +167,7 @@ mod tests { use crate::tui::{ filter::{Filter, FilterKind, Matcher, MatcherValue}, log_viewer::filters::Filters, - model::FieldsName, + model::SpanDescriptor, processing::Cursor, widgets::last_error::LastError, }; @@ -311,7 +311,7 @@ mod tests { matcher: Matcher::Field { name: "message".to_string(), value: MatcherValue::Exact("foo".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Remove, })); @@ -337,7 +337,7 @@ mod tests { matcher: Matcher::Field { name: "message".to_string(), value: MatcherValue::Exact("baz".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Remove, })); @@ -368,7 +368,7 @@ mod tests { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); @@ -415,7 +415,7 @@ mod tests { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); @@ -465,7 +465,7 @@ mod tests { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest1".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); @@ -473,7 +473,7 @@ mod tests { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest2".to_string()), - span: FieldsName::Main, + span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); diff --git a/src/tui/widgets/fieldtree.rs b/src/tui/widgets/fieldtree.rs index e841ccb..7e81d95 100644 --- a/src/tui/widgets/fieldtree.rs +++ b/src/tui/widgets/fieldtree.rs @@ -15,7 +15,7 @@ use tui_widget_list::{ListBuilder, ListState, ListView}; use crate::tui::{ block_around, log_viewer::LogViewer, - model::{FieldsName, LogEntry, LogFields}, + model::{SpanDescriptor, LogEntry, LogFields}, widgets::{line_text::style_span, styled::Styled}, }; @@ -91,7 +91,7 @@ impl<'a> FieldTree<'a> { self.lv.selected().map(|(s, _)| s) } - fn current_span_fields(&mut self, spans: &[(FieldsName, &LogFields)]) -> Vec<(String, String)> { + fn current_span_fields(&mut self, spans: &[(SpanDescriptor, &LogFields)]) -> Vec<(String, String)> { let mut selected = self.state().current_span.selected.unwrap_or(0); if selected > spans.len() { selected = spans.len().saturating_sub(1); @@ -120,7 +120,7 @@ impl<'a> FieldTree<'a> { } impl Styled<'_, &mut FieldTree<'_>> { - fn areas(&self, area: Rect, buf: &mut Buffer, name: Option<&FieldsName>) -> (Rect, Rect, Rect) { + fn areas(&self, area: Rect, buf: &mut Buffer, name: Option<&SpanDescriptor>) -> (Rect, Rect, Rect) { let [spans_area, fields_area] = Layout::horizontal([ Constraint::Length(if self.spans_focussed() || !self.focussed { 25 @@ -146,9 +146,9 @@ impl Styled<'_, &mut FieldTree<'_>> { self.styles, Some(if let Some(i) = name { match i { - FieldsName::Main => "own fields".to_string(), - FieldsName::Span(s) => format!("fields of {s}"), - FieldsName::Numbered(idx) => format!("fields of span #{idx}"), + SpanDescriptor::Main => "own fields".to_string(), + SpanDescriptor::Span(s) => format!("fields of {s}"), + SpanDescriptor::Numbered(idx) => format!("fields of span #{idx}"), } } else { "fields".to_string() @@ -191,9 +191,9 @@ impl Widget for Styled<'_, &mut FieldTree<'_>> { }; let mut item = Line::from(match span_name { - FieldsName::Main => "own fields".to_string(), - FieldsName::Span(s) => s.to_string(), - FieldsName::Numbered(idx) => format!("span #{idx}"), + SpanDescriptor::Main => "own fields".to_string(), + SpanDescriptor::Span(s) => s.to_string(), + SpanDescriptor::Numbered(idx) => format!("span #{idx}"), }); if cx.is_selected && self_focussed { item = item.style(self.styles.highlighted); diff --git a/src/tui/widgets/items.rs b/src/tui/widgets/items.rs index c57237c..0d1d64e 100644 --- a/src/tui/widgets/items.rs +++ b/src/tui/widgets/items.rs @@ -9,7 +9,7 @@ use crate::tui::{ filters::Filters, input::{FieldMatcher, InputState, InputTarget}, }, - model::{FieldsName, LogEntry}, + model::{SpanDescriptor, LogEntry}, widgets::{ last_error::LastError, line_text::Highlighted, @@ -23,7 +23,7 @@ pub struct Items<'a> { input_state: &'a InputState, filters: &'a Filters, - selected_footer_field: Option<(FieldsName, String, String)>, + selected_footer_field: Option<(SpanDescriptor, String, String)>, last_error: LastError, } @@ -33,7 +33,7 @@ impl<'a> Items<'a> { filters: &'a Filters, selected_offset: usize, input_state: &'a InputState, - selected_footer_field: Option<(FieldsName, String, String)>, + selected_footer_field: Option<(SpanDescriptor, String, String)>, last_error: LastError, ) -> Self { Self {