tree view
This commit is contained in:
parent
79639be9da
commit
d8e445b5f7
5 changed files with 202 additions and 96 deletions
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,11 +138,17 @@ 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.
|
||||||
|
if $forwards {
|
||||||
|
return Some((elem, inline_depth));
|
||||||
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
FilterKind::Remove => {
|
FilterKind::Remove => {
|
||||||
// continue past removed items
|
// continue past removed items
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,4 +142,8 @@ impl LogStream for LogFileReaderStream {
|
||||||
curr: self.curr,
|
curr: self.curr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enclosing_log_entry(&self) -> Option<(Rc<LogEntry>, usize)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,9 +119,17 @@ 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 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);
|
||||||
|
|
||||||
let mut line = line_text.styled(&self.styles);
|
let mut line = line_text.styled(&self.styles);
|
||||||
if idx == self.selected_offset
|
if idx == self.selected_offset
|
||||||
|
|
@ -92,7 +173,8 @@ impl Widget for Styled<'_, &Items<'_>> {
|
||||||
FieldMatcher::Contains(c) => {
|
FieldMatcher::Contains(c) => {
|
||||||
if msg.contains(c)
|
if msg.contains(c)
|
||||||
&& let Some(start_offset) = line.message.find(msg)
|
&& let Some(start_offset) = line.message.find(msg)
|
||||||
&& let Some(contains_offset) = line.message[start_offset..].find(c)
|
&& let Some(contains_offset) =
|
||||||
|
line.message[start_offset..].find(c)
|
||||||
{
|
{
|
||||||
let start = start_offset + contains_offset;
|
let start = start_offset + contains_offset;
|
||||||
line.highlight(Highlighted::Range {
|
line.highlight(Highlighted::Range {
|
||||||
|
|
@ -102,9 +184,10 @@ impl Widget for Styled<'_, &Items<'_>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let InputState::Target(InputTarget::Fields(Some(f))) = self.input_state
|
} else if let InputState::Target(InputTarget::Fields(Some(f))) =
|
||||||
|
self.input_state
|
||||||
&& let Some((name, value)) = &self.selected_footer_field
|
&& let Some((name, value)) = &self.selected_footer_field
|
||||||
&& let Some(current_log_value) = i.all_fields().get(&name)
|
&& let Some(current_log_value) = entry.all_fields().get(&name)
|
||||||
{
|
{
|
||||||
let matches = match f {
|
let matches = match f {
|
||||||
FieldMatcher::EqualTo => value == current_log_value,
|
FieldMatcher::EqualTo => value == current_log_value,
|
||||||
|
|
@ -121,8 +204,8 @@ impl Widget for Styled<'_, &Items<'_>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
ListItem::new(line)
|
ListItem::new(line)
|
||||||
},
|
}),
|
||||||
));
|
);
|
||||||
Widget::render(list, area, buf);
|
Widget::render(list, area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +43,7 @@ impl Into<Line<'static>> for Styled<'_, LineText> {
|
||||||
|
|
||||||
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 => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue