diff --git a/src/tui/model.rs b/src/tui/model.rs index ef01496..7ddb6bb 100644 --- a/src/tui/model.rs +++ b/src/tui/model.rs @@ -123,7 +123,7 @@ impl LogEntry { } } - pub fn line_text(&self, inline_depth: usize) -> LineText { + pub fn line_text(&self, tree: String) -> LineText { const NO_MESSAGE: &str = ""; const SPACES_BEFORE: &str = " "; @@ -147,7 +147,7 @@ impl LogEntry { SPACES_BEFORE.to_string(), single_field(raw), self.message_or_name(), - inline_depth, + tree, ), LogEntry::Sub { enter, sub_entries, .. @@ -161,14 +161,14 @@ impl LogEntry { ), format!("↪ {val}"), self.message_or_name(), - inline_depth, + tree, ) } else { LineText::new( SPACES_BEFORE.to_string(), single_field(enter), self.message_or_name(), - inline_depth, + tree, ) } } diff --git a/src/tui/processing.rs b/src/tui/processing.rs index 284978d..b4d380e 100644 --- a/src/tui/processing.rs +++ b/src/tui/processing.rs @@ -50,6 +50,10 @@ impl LogStream for LogEntryStream { inline_depth: self.inline_depth, }) } + + fn enclosing_log_entry(&self) -> Option<(Rc, usize)> { + Some((Rc::clone(&self.inner), self.inline_depth)) + } } impl IntoLogStream for &Rc { @@ -84,29 +88,32 @@ pub trait LogStream { fn next(&mut self) -> Option<(Rc, usize)>; fn prev(&mut self) -> Option<(Rc, usize)>; + fn enclosing_log_entry(&self) -> Option<(Rc, usize)>; + fn clone(&self) -> Box; fn filter(&self, filter: Rc) -> FilteredLogStream { FilteredLogStream { filter: filter, - stack: vec![self.clone()], + stack: vec![(self.enclosing_log_entry(), self.clone())], } } } pub struct FilteredLogStream { filter: Rc, - stack: Vec>, + stack: Vec<(Option<(Rc, usize)>, Box)>, } macro_rules! generate_candidate { - ($_self: tt, $iter_method: ident) => { + ($_self: tt, $iter_method: ident, $forwards: expr) => { loop { - let top = $_self.stack.last_mut().unwrap(); + let stack_len = $_self.stack.len(); + let (enclosing, top) = $_self.stack.last_mut().unwrap(); if let Some((top, inline_depth)) = top.$iter_method() { // if we can find it in the top of stack iterator, neat! return Some((top, inline_depth)); - } else if $_self.stack.len() > 1 { + } else if stack_len > 1 { // Otherwise, try popping the stack once and try again $_self.stack.pop(); } else { @@ -119,9 +126,10 @@ macro_rules! generate_candidate { } macro_rules! generate_filter { - ($_self: tt, $candidate: ident, $into_iter: ident) => { + ($_self: tt, $candidate: ident, $into_iter: ident, $forwards: expr) => { loop { let (elem, inline_depth) = $_self.$candidate()?; + let Filter { matcher, kind } = $_self.filter.as_ref(); if matcher.matches(&elem) { @@ -130,10 +138,16 @@ macro_rules! generate_filter { // When we inline, add this item to the stack // so we continue iterating inside it. if let Some(iter) = elem.$into_iter(inline_depth + 1) { - $_self.stack.push(Box::new(iter)); + $_self + .stack + .push((Some((Rc::clone(&elem), inline_depth)), Box::new(iter))); } // Continue so we actually return a nested item. - continue; + if $forwards { + return Some((elem, inline_depth)); + } else { + continue; + } } FilterKind::Remove => { // continue past removed items @@ -149,20 +163,20 @@ macro_rules! generate_filter { impl FilteredLogStream { fn next_candidate(&mut self) -> Option<(Rc, usize)> { - generate_candidate!(self, next) + generate_candidate!(self, next, true) } fn prev_candidate(&mut self) -> Option<(Rc, usize)> { - generate_candidate!(self, prev) + generate_candidate!(self, prev, false) } } impl LogStream for FilteredLogStream { fn next(&mut self) -> Option<(Rc, usize)> { - generate_filter!(self, next_candidate, from_start) + generate_filter!(self, next_candidate, from_start, true) } fn prev(&mut self) -> Option<(Rc, usize)> { - generate_filter!(self, prev_candidate, from_end) + generate_filter!(self, prev_candidate, from_end, false) } fn clone(&self) -> Box { @@ -171,8 +185,12 @@ impl LogStream for FilteredLogStream { stack: self .stack .iter() - .map(|i| LogStream::clone(i.as_ref())) + .map(|(enclosing, i)| (enclosing.clone(), LogStream::clone(i.as_ref()))) .collect(), }) } + + fn enclosing_log_entry(&self) -> Option<(Rc, usize)> { + self.stack[0].0.clone() + } } diff --git a/src/tui/reader.rs b/src/tui/reader.rs index c37b706..f36c78a 100644 --- a/src/tui/reader.rs +++ b/src/tui/reader.rs @@ -142,4 +142,8 @@ impl LogStream for LogFileReaderStream { curr: self.curr, }) } + + fn enclosing_log_entry(&self) -> Option<(Rc, usize)> { + None + } } diff --git a/src/tui/widgets/items.rs b/src/tui/widgets/items.rs index 57b06e4..dc030a6 100644 --- a/src/tui/widgets/items.rs +++ b/src/tui/widgets/items.rs @@ -1,18 +1,91 @@ use std::rc::Rc; +use itertools::Itertools; use ratatui::widgets::{List, ListItem, Widget}; use regex::Regex; -use serde_json::Value; use crate::tui::{ log_viewer::{FieldMatcher, InputState, InputTarget}, - model::{LogEntry, pretty_print_value}, + model::LogEntry, widgets::{ line_text::Highlighted, styled::{IntoStyled, Styled}, }, }; +pub struct TreeState { + tree_prefixes: Vec, +} + +impl TreeState { + pub fn from_items(items: &[(Rc, usize)]) -> Self { + let mut res = Vec::new(); + let mut curr = String::new(); + let mut prev_depth = 0; + + for (depth, next_depth) in items.iter().map(|i| i.1).circular_tuple_windows() { + if depth > prev_depth { + if depth > 1 { + let _ = curr.pop(); + let _ = curr.pop(); + curr.push('│'); + curr.push(' '); + } + for _ in prev_depth..depth.saturating_sub(1) { + curr.push(' '); + curr.push(' '); + } + if next_depth < depth { + curr.push(' '); + curr.push(' '); + curr.push(' '); + curr.push('└'); + curr.push('─'); + } else { + curr.push(' '); + curr.push(' '); + curr.push('├'); + curr.push('─'); + } + } else if depth == prev_depth && next_depth < prev_depth { + let _ = curr.pop(); + let _ = curr.pop(); + curr.push('└'); + curr.push('─'); + } else if depth < prev_depth { + for _ in depth..prev_depth { + let _ = curr.pop(); + let _ = curr.pop(); + let _ = curr.pop(); + let _ = curr.pop(); + let _ = curr.pop(); + } + + if depth > 0 { + let _ = curr.pop(); + let _ = curr.pop(); + if next_depth < depth - 1 { + curr.push('└'); + curr.push('─'); + } else { + curr.push('├'); + curr.push('─'); + } + } + } + + prev_depth = depth; + if depth != 0 { + res.push(format!("{curr} ")); + } else { + res.push(String::new()); + } + } + + Self { tree_prefixes: res } + } +} + pub struct Items<'a> { items: Vec<(Rc, usize)>, selected_offset: usize, @@ -46,83 +119,93 @@ impl Widget for Styled<'_, &Items<'_>> { where Self: Sized, { - let list = List::new(self.inner.items.iter().enumerate().map( - |(idx, (i, inline_depth))| { - let line_text = i.line_text(*inline_depth); + let ts = TreeState::from_items(&self.items); - let mut line = line_text.styled(&self.styles); - if idx == self.selected_offset - && let InputState::None | InputState::Target(InputTarget::This) = - self.input_state - { - line.highlight(Highlighted::All); - } else if let InputState::Target(InputTarget::Text(s)) = self.input_state - && let Some(msg) = &line.message_text - { - match s { - FieldMatcher::EqualTo => { - if self - .selected() - .and_then(|i| i.message_or_name()) - .is_some_and(|m| &m == msg) - { - line.highlight(Highlighted::All); - } - } - FieldMatcher::Prefix(p) => { - if msg.starts_with(p) - && let Some(offset) = line.message.find(msg) - { - line.highlight(Highlighted::Range { - from: offset, - to: offset + p.len(), - }); - } - } - FieldMatcher::Regex(r) => { - if let Ok(regex) = Regex::new(r) - && let Some(start_offset) = line.message.find(msg) - && let Some(m) = regex.find(msg) - { - let from = start_offset + m.start(); - let to = start_offset + m.end(); - line.highlight(Highlighted::Range { from, to }); - } - } - FieldMatcher::Contains(c) => { - if msg.contains(c) - && let Some(start_offset) = line.message.find(msg) - && let Some(contains_offset) = line.message[start_offset..].find(c) - { - let start = start_offset + contains_offset; - line.highlight(Highlighted::Range { - from: start, - to: start + c.len(), - }); - } - } - } - } else if let InputState::Target(InputTarget::Fields(Some(f))) = self.input_state - && let Some((name, value)) = &self.selected_footer_field - && let Some(current_log_value) = i.all_fields().get(&name) - { - let matches = match f { - FieldMatcher::EqualTo => value == current_log_value, - FieldMatcher::Prefix(v) => current_log_value.to_string().starts_with(v), - FieldMatcher::Regex(r) => { - Regex::new(r).is_ok_and(|i| i.is_match(current_log_value)) - } - FieldMatcher::Contains(c) => current_log_value.to_string().contains(c), - }; + let list = List::new( + self.inner + .items + .iter() + .map(|(entry, _)| entry) + .enumerate() + .zip(ts.tree_prefixes) + .map(|((idx, entry), tree)| { + let line_text = entry.line_text(tree); - if matches { + let mut line = line_text.styled(&self.styles); + if idx == self.selected_offset + && let InputState::None | InputState::Target(InputTarget::This) = + self.input_state + { line.highlight(Highlighted::All); - } - } + } else if let InputState::Target(InputTarget::Text(s)) = self.input_state + && let Some(msg) = &line.message_text + { + match s { + FieldMatcher::EqualTo => { + if self + .selected() + .and_then(|i| i.message_or_name()) + .is_some_and(|m| &m == msg) + { + line.highlight(Highlighted::All); + } + } + FieldMatcher::Prefix(p) => { + if msg.starts_with(p) + && let Some(offset) = line.message.find(msg) + { + line.highlight(Highlighted::Range { + from: offset, + to: offset + p.len(), + }); + } + } + FieldMatcher::Regex(r) => { + if let Ok(regex) = Regex::new(r) + && let Some(start_offset) = line.message.find(msg) + && let Some(m) = regex.find(msg) + { + let from = start_offset + m.start(); + let to = start_offset + m.end(); + line.highlight(Highlighted::Range { from, to }); + } + } + FieldMatcher::Contains(c) => { + if msg.contains(c) + && let Some(start_offset) = line.message.find(msg) + && let Some(contains_offset) = + line.message[start_offset..].find(c) + { + let start = start_offset + contains_offset; + line.highlight(Highlighted::Range { + from: start, + to: start + c.len(), + }); + } + } + } + } else if let InputState::Target(InputTarget::Fields(Some(f))) = + self.input_state + && let Some((name, value)) = &self.selected_footer_field + && let Some(current_log_value) = entry.all_fields().get(&name) + { + let matches = match f { + FieldMatcher::EqualTo => value == current_log_value, + FieldMatcher::Prefix(v) => current_log_value.to_string().starts_with(v), + FieldMatcher::Regex(r) => { + Regex::new(r).is_ok_and(|i| i.is_match(current_log_value)) + } + FieldMatcher::Contains(c) => current_log_value.to_string().contains(c), + }; - ListItem::new(line) - }, - )); + if matches { + line.highlight(Highlighted::All); + } + } + + ListItem::new(line) + }), + ); Widget::render(list, area, buf); } } diff --git a/src/tui/widgets/line_text.rs b/src/tui/widgets/line_text.rs index cfebffe..a16ea30 100644 --- a/src/tui/widgets/line_text.rs +++ b/src/tui/widgets/line_text.rs @@ -11,7 +11,7 @@ pub enum Highlighted { pub struct LineText { prefix: String, pub message: String, - inline_depth: usize, + tree: String, pub message_text: Option, highlighted: Highlighted, } @@ -21,13 +21,13 @@ impl LineText { prefix: String, message: String, message_text: Option, - inline_depth: usize, + tree: String, ) -> Self { Self { prefix, message, message_text, - inline_depth, + tree, highlighted: Highlighted::None, } } @@ -42,7 +42,8 @@ impl Into> for Styled<'_, LineText> { let mut spans = Vec::new(); spans.push(Span::from(self.inner.prefix)); - spans.push(Span::from("┃")); + spans.push(Span::from("┃ ")); + spans.push(Span::from(self.inner.tree)); match self.inner.highlighted { Highlighted::None => {