tree view

This commit is contained in:
Jana Dönszelmann 2026-02-25 12:45:01 +01:00
parent 79639be9da
commit d8e445b5f7
No known key found for this signature in database
5 changed files with 202 additions and 96 deletions

View file

@ -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 = "<no message>"; const NO_MESSAGE: &str = "<no message>";
const SPACES_BEFORE: &str = " "; const SPACES_BEFORE: &str = " ";
@ -147,7 +147,7 @@ impl LogEntry {
SPACES_BEFORE.to_string(), SPACES_BEFORE.to_string(),
single_field(raw), single_field(raw),
self.message_or_name(), self.message_or_name(),
inline_depth, tree,
), ),
LogEntry::Sub { LogEntry::Sub {
enter, sub_entries, .. enter, sub_entries, ..
@ -161,14 +161,14 @@ impl LogEntry {
), ),
format!("{val}"), format!("{val}"),
self.message_or_name(), self.message_or_name(),
inline_depth, tree,
) )
} else { } else {
LineText::new( LineText::new(
SPACES_BEFORE.to_string(), SPACES_BEFORE.to_string(),
single_field(enter), single_field(enter),
self.message_or_name(), self.message_or_name(),
inline_depth, tree,
) )
} }
} }

View file

@ -50,6 +50,10 @@ impl LogStream for LogEntryStream {
inline_depth: self.inline_depth, inline_depth: self.inline_depth,
}) })
} }
fn enclosing_log_entry(&self) -> Option<(Rc<LogEntry>, usize)> {
Some((Rc::clone(&self.inner), self.inline_depth))
}
} }
impl IntoLogStream for &Rc<LogEntry> { impl IntoLogStream for &Rc<LogEntry> {
@ -84,29 +88,32 @@ pub trait LogStream {
fn next(&mut self) -> Option<(Rc<LogEntry>, usize)>; fn next(&mut self) -> Option<(Rc<LogEntry>, usize)>;
fn prev(&mut self) -> Option<(Rc<LogEntry>, usize)>; fn prev(&mut self) -> Option<(Rc<LogEntry>, usize)>;
fn enclosing_log_entry(&self) -> Option<(Rc<LogEntry>, usize)>;
fn clone(&self) -> Box<dyn LogStream>; fn clone(&self) -> Box<dyn LogStream>;
fn filter(&self, filter: Rc<Filter>) -> FilteredLogStream { fn filter(&self, filter: Rc<Filter>) -> FilteredLogStream {
FilteredLogStream { FilteredLogStream {
filter: filter, filter: filter,
stack: vec![self.clone()], stack: vec![(self.enclosing_log_entry(), self.clone())],
} }
} }
} }
pub struct FilteredLogStream { pub struct FilteredLogStream {
filter: Rc<Filter>, filter: Rc<Filter>,
stack: Vec<Box<dyn LogStream>>, stack: Vec<(Option<(Rc<LogEntry>, usize)>, Box<dyn LogStream>)>,
} }
macro_rules! generate_candidate { macro_rules! generate_candidate {
($_self: tt, $iter_method: ident) => { ($_self: tt, $iter_method: ident, $forwards: expr) => {
loop { 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 let Some((top, inline_depth)) = top.$iter_method() {
// if we can find it in the top of stack iterator, neat! // if we can find it in the top of stack iterator, neat!
return Some((top, inline_depth)); 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 // Otherwise, try popping the stack once and try again
$_self.stack.pop(); $_self.stack.pop();
} else { } else {
@ -119,9 +126,10 @@ macro_rules! generate_candidate {
} }
macro_rules! generate_filter { macro_rules! generate_filter {
($_self: tt, $candidate: ident, $into_iter: ident) => { ($_self: tt, $candidate: ident, $into_iter: ident, $forwards: expr) => {
loop { loop {
let (elem, inline_depth) = $_self.$candidate()?; let (elem, inline_depth) = $_self.$candidate()?;
let Filter { matcher, kind } = $_self.filter.as_ref(); let Filter { matcher, kind } = $_self.filter.as_ref();
if matcher.matches(&elem) { if matcher.matches(&elem) {
@ -130,10 +138,16 @@ macro_rules! generate_filter {
// When we inline, add this item to the stack // When we inline, add this item to the stack
// so we continue iterating inside it. // so we continue iterating inside it.
if let Some(iter) = elem.$into_iter(inline_depth + 1) { 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 so we actually return a nested item.
continue; if $forwards {
return Some((elem, inline_depth));
} else {
continue;
}
} }
FilterKind::Remove => { FilterKind::Remove => {
// continue past removed items // continue past removed items
@ -149,20 +163,20 @@ macro_rules! generate_filter {
impl FilteredLogStream { impl FilteredLogStream {
fn next_candidate(&mut self) -> Option<(Rc<LogEntry>, usize)> { fn next_candidate(&mut self) -> Option<(Rc<LogEntry>, usize)> {
generate_candidate!(self, next) generate_candidate!(self, next, true)
} }
fn prev_candidate(&mut self) -> Option<(Rc<LogEntry>, usize)> { fn prev_candidate(&mut self) -> Option<(Rc<LogEntry>, usize)> {
generate_candidate!(self, prev) generate_candidate!(self, prev, false)
} }
} }
impl LogStream for FilteredLogStream { impl LogStream for FilteredLogStream {
fn next(&mut self) -> Option<(Rc<LogEntry>, usize)> { fn next(&mut self) -> Option<(Rc<LogEntry>, usize)> {
generate_filter!(self, next_candidate, from_start) generate_filter!(self, next_candidate, from_start, true)
} }
fn prev(&mut self) -> Option<(Rc<LogEntry>, usize)> { fn prev(&mut self) -> Option<(Rc<LogEntry>, usize)> {
generate_filter!(self, prev_candidate, from_end) generate_filter!(self, prev_candidate, from_end, false)
} }
fn clone(&self) -> Box<dyn LogStream> { fn clone(&self) -> Box<dyn LogStream> {
@ -171,8 +185,12 @@ impl LogStream for FilteredLogStream {
stack: self stack: self
.stack .stack
.iter() .iter()
.map(|i| LogStream::clone(i.as_ref())) .map(|(enclosing, i)| (enclosing.clone(), LogStream::clone(i.as_ref())))
.collect(), .collect(),
}) })
} }
fn enclosing_log_entry(&self) -> Option<(Rc<LogEntry>, usize)> {
self.stack[0].0.clone()
}
} }

View file

@ -142,4 +142,8 @@ impl LogStream for LogFileReaderStream {
curr: self.curr, curr: self.curr,
}) })
} }
fn enclosing_log_entry(&self) -> Option<(Rc<LogEntry>, usize)> {
None
}
} }

View file

@ -1,18 +1,91 @@
use std::rc::Rc; use std::rc::Rc;
use itertools::Itertools;
use ratatui::widgets::{List, ListItem, Widget}; use ratatui::widgets::{List, ListItem, Widget};
use regex::Regex; use regex::Regex;
use serde_json::Value;
use crate::tui::{ use crate::tui::{
log_viewer::{FieldMatcher, InputState, InputTarget}, log_viewer::{FieldMatcher, InputState, InputTarget},
model::{LogEntry, pretty_print_value}, model::LogEntry,
widgets::{ widgets::{
line_text::Highlighted, line_text::Highlighted,
styled::{IntoStyled, Styled}, styled::{IntoStyled, Styled},
}, },
}; };
pub struct TreeState {
tree_prefixes: Vec<String>,
}
impl TreeState {
pub fn from_items(items: &[(Rc<LogEntry>, 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> { pub struct Items<'a> {
items: Vec<(Rc<LogEntry>, usize)>, items: Vec<(Rc<LogEntry>, usize)>,
selected_offset: usize, selected_offset: usize,
@ -46,83 +119,93 @@ impl Widget for Styled<'_, &Items<'_>> {
where where
Self: Sized, Self: Sized,
{ {
let list = List::new(self.inner.items.iter().enumerate().map( let ts = TreeState::from_items(&self.items);
|(idx, (i, inline_depth))| {
let line_text = i.line_text(*inline_depth);
let mut line = line_text.styled(&self.styles); let list = List::new(
if idx == self.selected_offset self.inner
&& let InputState::None | InputState::Target(InputTarget::This) = .items
self.input_state .iter()
{ .map(|(entry, _)| entry)
line.highlight(Highlighted::All); .enumerate()
} else if let InputState::Target(InputTarget::Text(s)) = self.input_state .zip(ts.tree_prefixes)
&& let Some(msg) = &line.message_text .map(|((idx, entry), tree)| {
{ let line_text = entry.line_text(tree);
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),
};
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); 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); Widget::render(list, area, buf);
} }
} }

View file

@ -11,7 +11,7 @@ pub enum Highlighted {
pub struct LineText { pub struct LineText {
prefix: String, prefix: String,
pub message: String, pub message: String,
inline_depth: usize, tree: String,
pub message_text: Option<String>, pub message_text: Option<String>,
highlighted: Highlighted, highlighted: Highlighted,
} }
@ -21,13 +21,13 @@ impl LineText {
prefix: String, prefix: String,
message: String, message: String,
message_text: Option<String>, message_text: Option<String>,
inline_depth: usize, tree: String,
) -> Self { ) -> Self {
Self { Self {
prefix, prefix,
message, message,
message_text, message_text,
inline_depth, tree,
highlighted: Highlighted::None, highlighted: Highlighted::None,
} }
} }
@ -42,7 +42,8 @@ impl Into<Line<'static>> for Styled<'_, LineText> {
let mut spans = Vec::new(); let mut spans = Vec::new();
spans.push(Span::from(self.inner.prefix)); 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 { match self.inner.highlighted {
Highlighted::None => { Highlighted::None => {