From cc4ecf40d73cb0e56f0fb023a906078fc55d58b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= Date: Wed, 25 Feb 2026 09:21:50 +0100 Subject: [PATCH] modal --- Cargo.lock | 1 + Cargo.toml | 1 + src/tui/filter.rs | 182 ++++++------------ src/tui/log_viewer.rs | 94 ++++++++- src/tui/mod.rs | 433 +++++++++++++++++++++++------------------- src/tui/model.rs | 22 ++- 6 files changed, 402 insertions(+), 331 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79f66bc..765ecb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,6 +1196,7 @@ dependencies = [ "nix 0.31.1", "ratatui", "ratatui-themes", + "regex", "serde", "serde_json", "thiserror 2.0.18", diff --git a/Cargo.toml b/Cargo.toml index f4abe8f..e9b192c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ serde_json = "1" thiserror = "2" itertools = "0.14" nix = {version = "0.31", features = ["process", "signal"]} +regex = "1" diff --git a/src/tui/filter.rs b/src/tui/filter.rs index 3d99275..02260ae 100644 --- a/src/tui/filter.rs +++ b/src/tui/filter.rs @@ -1,51 +1,77 @@ -use crate::tui::model::LogEntry; +use regex::bytes::Regex; -#[derive(Clone)] -pub enum WipMatcher { - Field { - name: Option, - value: Option, - }, - Specific { - hash: Option, - }, +use crate::tui::{ + log_viewer::{FieldMatcher, InputTarget, LogViewer}, + model::{LogEntry, pretty_print_value}, +}; + +pub enum MatcherValue { + Exact(String), + Regex(Regex), + Prefix(String), + Contains(String), } -impl WipMatcher { - fn validate(&self) -> Option { +impl MatcherValue { + pub fn from_field_matcher(fm: FieldMatcher, selected: Option) -> Option { + match fm { + FieldMatcher::EqualTo => Some(Self::Exact(selected?)), + FieldMatcher::Prefix(p) => Some(Self::Prefix(p)), + FieldMatcher::Regex(r) => Some(Self::Regex(Regex::new(&r).ok()?)), + FieldMatcher::Contains(c) => Some(Self::Contains(c)), + } + } + + pub fn matches(&self, v: &str) -> bool { match self { - WipMatcher::Field { - name: Some(name), - value: Some(value), - } => Some(Matcher::Field { - name: name.clone(), - value: value.clone(), - }), - WipMatcher::Specific { hash } => Some(Matcher::Specific { hash: (*hash)? }), - _ => None, + MatcherValue::Exact(e) => e == v, + MatcherValue::Regex(regex) => regex.is_match(v.as_bytes()), + MatcherValue::Prefix(p) => v.starts_with(p), + MatcherValue::Contains(c) => v.contains(c), } } } pub enum Matcher { - Field { - name: String, - value: serde_json::Value, - }, - Specific { - hash: u64, - }, + Field { name: String, value: MatcherValue }, + Message { value: MatcherValue }, + Specific { hash: u64 }, } impl Matcher { pub fn matches(&self, entry: &LogEntry) -> bool { match self { + Matcher::Specific { hash } => entry.hash() == *hash, Matcher::Field { name, value } => entry .all_fields() .fields .get(name) - .is_some_and(|v| v == value), - Matcher::Specific { hash } => entry.hash() == *hash, + .is_some_and(|v| value.matches(&pretty_print_value(&v))), + Matcher::Message { value } => { + entry.message_or_name().is_some_and(|v| value.matches(&v)) + } + } + } + + pub fn from_input(target: InputTarget, lv: &LogViewer) -> Option { + match target { + InputTarget::Fields(fm) => { + let value = lv.footer_fields().get(lv.footer_list.selected?)?.clone(); + Some(Self::Field { + name: value.0, + value: MatcherValue::from_field_matcher(fm?, Some(value.1.to_string()))?, + }) + } + InputTarget::Text(fm) => Some(Self::Message { + value: MatcherValue::from_field_matcher( + fm, + lv.selected().and_then(|(i, _)| i.message_or_name()), + )?, + }), + InputTarget::This => lv + .selected() + .map(|(i, _)| Self::Specific { hash: i.hash() }), + InputTarget::Surround => todo!(), } } } @@ -69,99 +95,3 @@ impl Filter { } } } - -#[derive(Clone)] -pub struct WipFilter { - pub matcher: Option, - pub kind: Option, - pub selection: FilterSelection, -} - -impl WipFilter { - pub fn validate(&self) -> Option { - let Self { - matcher, - kind, - selection: _, - } = self; - let Some(matcher) = matcher else { return None }; - Some(Filter { - matcher: matcher.validate()?, - kind: kind.clone()?, - }) - } - - pub fn clear(&mut self) { - match self.selection { - FilterSelection::Kind => self.kind = None, - FilterSelection::MatcherKind => {} - FilterSelection::Matcher => {} - FilterSelection::Confirm => {} - } - } - - pub fn right(&mut self) { - match self.selection { - FilterSelection::Kind => { - self.kind = Some(match self.kind { - None => FilterKind::Inline, - Some(FilterKind::Inline) => FilterKind::Remove, - Some(FilterKind::Remove) => FilterKind::Inline, - }) - } - FilterSelection::MatcherKind => {} - FilterSelection::Matcher => {} - FilterSelection::Confirm => {} - } - } - pub fn left(&mut self) { - match self.selection { - FilterSelection::Kind => { - self.kind = Some(match self.kind { - None => FilterKind::Remove, - Some(FilterKind::Remove) => FilterKind::Inline, - Some(FilterKind::Inline) => FilterKind::Inline, - }) - } - FilterSelection::MatcherKind => {} - FilterSelection::Matcher => {} - FilterSelection::Confirm => {} - } - } -} - -#[derive(Clone, Copy)] -pub enum FilterSelection { - Kind, - MatcherKind, - Matcher, - Confirm, -} - -impl FilterSelection { - pub fn next(&mut self) { - *self = match *self { - Self::Kind => Self::MatcherKind, - Self::MatcherKind => Self::Matcher, - Self::Matcher => Self::Confirm, - Self::Confirm => Self::Confirm, - }; - } - - pub fn prev(&mut self) { - *self = match self { - Self::Kind => Self::Kind, - Self::MatcherKind => Self::Kind, - Self::Matcher => Self::MatcherKind, - Self::Confirm => Self::Matcher, - }; - } -} - -#[derive(Clone, Copy)] -pub enum FieldMatcherSelection { - Field, - Value, -} - -impl FieldMatcherSelection {} diff --git a/src/tui/log_viewer.rs b/src/tui/log_viewer.rs index 2e4865c..399d901 100644 --- a/src/tui/log_viewer.rs +++ b/src/tui/log_viewer.rs @@ -32,27 +32,101 @@ impl Clone for LogView { } } -pub enum InputTarget { - Fields, +#[derive(Clone)] +pub enum FieldMatcher { + EqualTo, + Prefix(String), + Regex(String), + Contains(String), } +impl FieldMatcher { + pub fn show(&self) -> String { + match self { + FieldMatcher::EqualTo => "equal to selected value".to_string(), + Self::Prefix(s) => format!("with a prefix of `{s}`"), + Self::Regex(s) => format!("matching /{s}/"), + Self::Contains(s) => format!("containing `{s}`"), + } + } +} + +#[derive(Clone)] +pub enum InputTarget { + Fields(Option), + Text(FieldMatcher), + This, + Surround, +} + +impl InputTarget { + pub fn show(&self) -> String { + match self { + Self::Fields(None) => "logs with a field...".to_string(), + Self::Fields(Some(fm)) => format!("logs with the selected field {}", fm.show()), + Self::Text(fm) => format!("logs {}", fm.show()), + Self::This => format!("this log"), + Self::Surround => format!("the log surrounding the current view"), + } + } +} + +#[derive(Clone)] pub enum InputState { None, Target(InputTarget), } impl InputState { + pub fn capture_string(&mut self) -> Option<&mut String> { + match self { + InputState::None => None, + InputState::Target(InputTarget::This) => None, + InputState::Target(InputTarget::Fields(None)) => None, + InputState::Target(InputTarget::Surround) => None, + + // require arbitrary text input + InputState::Target(InputTarget::Fields(Some(FieldMatcher::Contains(s)))) => Some(s), + InputState::Target(InputTarget::Text(FieldMatcher::Contains(s))) => Some(s), + InputState::Target(InputTarget::Fields(Some(FieldMatcher::Prefix(s)))) => Some(s), + InputState::Target(InputTarget::Text(FieldMatcher::Prefix(s))) => Some(s), + InputState::Target(InputTarget::Fields(Some(FieldMatcher::Regex(s)))) => Some(s), + InputState::Target(InputTarget::Text(FieldMatcher::Regex(s))) => Some(s), + + InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))) => None, + InputState::Target(InputTarget::Text(FieldMatcher::EqualTo)) => None, + } + } + + pub fn captures_input(&mut self) -> bool { + self.capture_string().is_some() + } + + pub fn show(&self) -> String { + match self { + InputState::None => "".to_string(), + InputState::Target(input_target) => input_target.show(), + } + } + pub fn reset(&mut self) { *self = Self::None; } pub fn target(&mut self, target: InputTarget) { - *self = Self::Target(target); + if let Self::Target(t) = self + && mem::discriminant(t) == mem::discriminant(&target) + && !self.captures_input() + { + self.reset(); + } else { + *self = Self::Target(target); + } } } pub struct LogViewer { - stack: Vec, + pub stack: Vec, curr: LogView, cache: HashMap, LogView>, filters: Vec>, @@ -253,9 +327,10 @@ impl LogViewer { self.curr.selection_offset -= 1; } } - InputState::Target(InputTarget::Fields) => { + InputState::Target(InputTarget::Fields(None)) => { self.footer_list.previous(); } + _ => {} } } @@ -264,9 +339,10 @@ impl LogViewer { InputState::None => { self.curr.selection_offset += 1; } - InputState::Target(InputTarget::Fields) => { + InputState::Target(InputTarget::Fields(None)) => { self.footer_list.next(); } + _ => {} } } @@ -292,9 +368,10 @@ impl LogViewer { self.curr.selection_offset = 0; while self.curr.iter.prev().is_some() {} } - InputState::Target(InputTarget::Fields) => { + InputState::Target(InputTarget::Fields(None)) => { self.footer_list.select(Some(0)); } + _ => {} } } @@ -331,9 +408,10 @@ impl LogViewer { self.curr = cached_view.clone(); } } - InputState::Target(InputTarget::Fields) => { + InputState::Target(InputTarget::Fields(None)) => { self.footer_list.next(); } + _ => {} } } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 3811bd0..1f5f1a2 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use ratatui_themes::{Theme, ThemeName}; +use regex::bytes::Regex; use std::{ fs::{self, DirEntry}, io, @@ -11,12 +12,13 @@ use std::{ use tui_widget_list::{ListBuilder, ListView}; use crate::tui::{ - filter::{FilterKind, WipMatcher}, + filter::FilterKind, log_viewer::{InputState, InputTarget, LogViewer}, model::pretty_print_value, }; use crate::tui::{ - filter::{FilterSelection, WipFilter}, + filter::{Filter, Matcher}, + log_viewer::FieldMatcher, reader::LogfileReader, }; use ratatui::{ @@ -25,7 +27,7 @@ use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, layout::{Constraint, HorizontalAlignment, Layout, Rect}, style::Style, - text::{Line, Text}, + text::{Line, Span, Text}, widgets::{ Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, }, @@ -37,6 +39,40 @@ pub mod model; pub mod processing; pub mod reader; +const HELP_TEXT: &str = "Generic: + ? show help + cancel focus or close tab + u undo + r redo + pgdwn&pgup move page + down&up move single + j&k move single + Home/G move to start + + <- / backspace / h exit nested view + -> / enter / l enter nested view + +─────────────────────────────────────────────────────── +targeting logs: + + f fields + t the selected log + s surrounding element + +either a field after `f` or the text of the current log: + p ... with a prefix + r ... matching a regex + / ... matching a regex + e ... equal to selected + c ... containing + +─────────────────────────────────────────────────────── +perform action on selected target: + + control-d delete + control-i inline +"; + pub fn run(logs_dir: PathBuf, compiler_root: Option, theme: ThemeName) { let terminal = ratatui::init(); let theme = Theme::new(theme); @@ -56,9 +92,6 @@ enum Tab { last_height: usize, }, LogViewer(LogViewer), - CreateFilter { - filter: WipFilter, - }, Empty, Help, } @@ -70,36 +103,11 @@ impl Tab { (Tab::FileChooser { .. }, _) => "choose a file".to_string(), (Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()), (Tab::LogViewer(_), None) => "logs".to_string(), - (Tab::CreateFilter { .. }, _) => "create filter".to_string(), (Tab::Help, _) => "help".to_string(), } } } -fn initialize_filter(lv: &mut LogViewer, kind: Option) -> WipFilter { - todo!() - // let matcher = if lv.fields_selected { - // let footer_fields = lv.footer_fields(); - // let (key, value) = footer_fields - // .get(lv.footer_list.selected.unwrap_or(0)) - // .map_or((None, None), |(k, v)| (Some(k), Some(v))); - // Some(WipMatcher::Field { - // name: key.cloned(), - // value: value.cloned(), - // }) - // } else { - // Some(WipMatcher::Specific { - // hash: lv.selected().map(|(i, _)| i.hash()), - // }) - // }; - - // WipFilter { - // matcher, - // kind, - // selection: filter::FilterSelection::Kind, - // } -} - struct App { tabs: Vec, logs_dir: PathBuf, @@ -204,6 +212,54 @@ impl App { _ => {} }, Tab::LogViewer(lv) => match key.code { + // delete + KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) => { + if let InputState::None = lv.input_state { + lv.input_state = InputState::Target(InputTarget::This); + } + + if let InputState::Target(t) = lv.input_state.clone() + && let Some(m) = Matcher::from_input(t, lv) + { + lv.add_filter(Rc::new(Filter { + matcher: m, + kind: FilterKind::Remove, + })); + lv.input_state.reset(); + } + } + // inline + KeyCode::Char('i') if key.modifiers.contains(KeyModifiers::ALT) => { + if let InputState::None = lv.input_state { + lv.input_state = InputState::Target(InputTarget::This); + } + + if let InputState::Target(t) = lv.input_state.clone() + && let Some(m) = Matcher::from_input(t, lv) + { + lv.add_filter(Rc::new(Filter { + matcher: m, + kind: FilterKind::Inline, + })); + lv.input_state.reset(); + } + } + KeyCode::Char(c) if lv.input_state.captures_input() => { + lv.input_state.capture_string().unwrap().push(c); + } + KeyCode::Backspace if lv.input_state.captures_input() => { + lv.input_state.capture_string().unwrap().pop(); + } + KeyCode::Esc if lv.input_state.captures_input() => { + if let InputState::Target(InputTarget::Fields(s @ Some(_))) = + &mut lv.input_state + { + *s = None + } else { + lv.input_state.reset(); + } + } + _ if lv.input_state.captures_input() => {} KeyCode::Char('j') | KeyCode::Down => lv.next(), KeyCode::Char('k') | KeyCode::Up => lv.prev(), KeyCode::PageUp => lv.page_up(), @@ -214,11 +270,50 @@ impl App { KeyCode::Right => lv.enter(), KeyCode::Enter => lv.enter(), KeyCode::Char('f') => { - lv.input_state.target(InputTarget::Fields); + lv.input_state.target(InputTarget::Fields(None)); + lv.footer_list.select(Some(0)); + } + KeyCode::Esc => lv.input_state.reset(), + KeyCode::Char('s') if !lv.stack.is_empty() => { + lv.input_state.target(InputTarget::Surround); + } + KeyCode::Char('t') => { + lv.input_state.target(InputTarget::This); + } + KeyCode::Char('r') | KeyCode::Char('/') => { + let v = FieldMatcher::Regex(String::new()); + if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state { + *f = Some(v); + } else { + lv.input_state.target(InputTarget::Text(v)); + } + } + KeyCode::Char('p') => { + let v = FieldMatcher::Prefix(String::new()); + if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state { + *f = Some(v); + } else { + lv.input_state.target(InputTarget::Text(v)); + } + } + KeyCode::Char('c') => { + let v = FieldMatcher::Contains(String::new()); + if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state { + *f = Some(v); + } else { + lv.input_state.target(InputTarget::Text(v)); + } + } + KeyCode::Char('e') => { + let v = FieldMatcher::EqualTo; + if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state { + *f = Some(v); + } else { + lv.input_state.target(InputTarget::Text(v)); + } } _ => {} }, - Tab::CreateFilter { filter } => todo!(), } } @@ -347,11 +442,10 @@ impl Widget for &mut App { Tab::FileChooser { .. } => (false, true), Tab::LogViewer(lv) => { let target_fields = - matches!(lv.input_state, InputState::Target(InputTarget::Fields)); + matches!(lv.input_state, InputState::Target(InputTarget::Fields(..))); (target_fields, !target_fields) } Tab::Empty => (false, false), - Tab::CreateFilter { .. } => (false, false), }; let main_area = { @@ -427,18 +521,123 @@ impl Widget for &mut App { .items(main_area.height as usize) .unwrap_or_else(|| (Vec::new(), 0)); - Paragraph::new(selected_offset.to_string()).render(right, buf); + Line::from(lv.input_state.show()).render(right, buf); let list = List::new(items.into_iter().enumerate().map( |(idx, (i, inline_depth))| { - let line = i.line_text(false, inline_depth); + let line_text = i.line_text(false, inline_depth); - let mut list_item = ListItem::new(line); + let mut line = Line::from(line_text.clone()); + if idx == selected_offset + && let InputState::None | InputState::Target(InputTarget::This) = + lv.input_state + { + line = Line::from(line_text).style(highlighted); + } else if let InputState::Target(InputTarget::Text(s)) = &lv.input_state + && let Some(msg) = i.message_or_name() + { + match s { + FieldMatcher::EqualTo => { + if lv + .selected() + .and_then(|(i, _)| i.message_or_name()) + .is_some_and(|m| m == msg) + { + line = line.style(highlighted); + } + } + FieldMatcher::Prefix(p) => { + if msg.starts_with(p) + && let Some(offset) = line_text.find(&msg) + { + let spans = vec![ + Span::from(line_text[..offset].to_string()), + Span::from( + line_text[offset..(offset + p.len())] + .to_string(), + ) + .style(highlighted), + Span::from( + line_text[(offset + p.len())..].to_string(), + ), + ]; - if idx == selected_offset { - list_item = list_item.style(highlighted); + line = Line::from(spans); + } + } + FieldMatcher::Regex(r) => { + if let Ok(regex) = Regex::new(r) + && let Some(start_offset) = line_text.find(&msg) + && let Some(m) = regex.find(msg.as_bytes()) + { + let spans = vec![ + Span::from( + line_text[..start_offset + m.start()] + .to_string(), + ), + Span::from( + line_text[start_offset + m.start() + ..start_offset + m.end()] + .to_string(), + ) + .style(highlighted), + Span::from( + line_text[start_offset + m.end()..].to_string(), + ), + ]; + + line = Line::from(spans); + } + } + FieldMatcher::Contains(c) => { + if msg.contains(c) + && let Some(start_offset) = line_text.find(&msg) + && let Some(contains_offset) = + line_text[start_offset..].find(c) + { + let start = start_offset + contains_offset; + let spans = vec![ + Span::from(line_text[..start].to_string()), + Span::from( + line_text[start..(start + c.len())].to_string(), + ) + .style(highlighted), + Span::from( + line_text[(start + c.len())..].to_string(), + ), + ]; + + line = Line::from(spans); + } + } + } + } else if let InputState::Target(InputTarget::Fields(Some(f))) = + &lv.input_state + && let Some(selected_field_offset) = lv.footer_list.selected + && let Some((name, value)) = + lv.footer_fields().get(selected_field_offset) + && let Some(current_log_value) = i.all_fields().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.to_string().as_bytes()) + }), + FieldMatcher::Contains(c) => { + current_log_value.to_string().contains(c) + } + }; + + if matches { + line = line.style(highlighted); + } } + let list_item = ListItem::new(line); + list_item }, )); @@ -504,159 +703,7 @@ impl Widget for &mut App { inner }; - Paragraph::new( - " -Generic: - ? show help - cancel focus or close tab - u undo - r redo - pgdwn&pgup move page - down&up move single - j&k move single - Home/G move to start - - <- / backspace / h exit nested view - -> / enter / l enter nested view - -─────────────────────────────────────────────────────── -targeting logs: - - f fields - p prefix - r regex - c current - s surrounding element - -─────────────────────────────────────────────────────── -perform action on selected target: - - d delete - i inline - ", - ) - .render(popup_area, buf); - } - Tab::CreateFilter { filter } => { - Clear.render(popup_area, buf); - let popup_area = { - let block = Block::bordered() - .title_top("create filter") - .style(default) - .padding(Padding::symmetric(3, 1)) - .border_style(border_selected); - let inner = block.inner(popup_area); - block.render(popup_area, buf); - inner - }; - - let [kind, matcher_kind, matcher_area, confirm] = Layout::vertical([ - Constraint::Length(5), - Constraint::Length(5), - Constraint::Fill(1), - Constraint::Length(1), - ]) - .areas(popup_area); - - let text = match &filter.kind { - None => "", - Some(FilterKind::Inline) => "inline items", - Some(FilterKind::Remove) => "remove item and sub-items", - }; - Paragraph::new(format!("⏴ {text} ⏵")) - .centered() - .style( - if matches!(filter.selection, filter::FilterSelection::Kind) { - highlighted - } else { - default - }, - ) - .block( - Block::bordered() - .title_top("transformation") - .padding(Padding::uniform(1)), - ) - .render(kind, buf); - - let text = match filter.matcher.as_ref() { - None => "", - Some(WipMatcher::Field { .. }) => "all logs where field matches", - Some(WipMatcher::Specific { .. }) => "this specific log", - }; - Paragraph::new(format!("⏴ {text} ⏵")) - .centered() - .style( - if matches!(filter.selection, filter::FilterSelection::MatcherKind) { - highlighted - } else { - default - }, - ) - .block( - Block::bordered() - .title_top("matcher") - .padding(Padding::uniform(1)), - ) - .render(matcher_kind, buf); - - match &filter.matcher { - Some(WipMatcher::Field { name, value }) => { - let [field_area, value_area, _] = Layout::vertical([ - Constraint::Length(5), - Constraint::Length(5), - Constraint::Fill(1), - ]) - .areas(matcher_area); - - Paragraph::new(format!("{}", name.clone().unwrap_or_default())) - .centered() - .style( - if matches!(filter.selection, filter::FilterSelection::Matcher) - { - highlighted - } else { - default - }, - ) - .block( - Block::bordered() - .title_top("field name") - .padding(Padding::uniform(1)), - ) - .render(field_area, buf); - - Paragraph::new(format!("{}", value.clone().unwrap_or_default())) - .centered() - .style( - if matches!(filter.selection, filter::FilterSelection::Matcher) - { - highlighted - } else { - default - }, - ) - .block( - Block::bordered() - .title_top("value") - .padding(Padding::uniform(1)), - ) - .render(value_area, buf); - } - Some(WipMatcher::Specific { .. }) => {} - None => {} - } - - Paragraph::new("confirm") - .centered() - .style( - if matches!(filter.selection, filter::FilterSelection::Confirm) { - highlighted - } else { - default - }, - ) - .render(confirm, buf); + Paragraph::new(HELP_TEXT).render(popup_area, buf); } } } diff --git a/src/tui/model.rs b/src/tui/model.rs index c59c484..43b593a 100644 --- a/src/tui/model.rs +++ b/src/tui/model.rs @@ -7,7 +7,6 @@ use std::{ }; use jiff::Timestamp; -use ratatui::text::Line; use serde::Deserialize; use serde_json::Value; @@ -109,7 +108,22 @@ impl LogEntry { } } - pub fn line_text(&self, accessed: bool, inline_depth: usize) -> Line<'static> { + pub fn message_or_name(&self) -> Option { + match self { + LogEntry::Single { raw } => raw.fields.message().map(|i| i.to_string()), + LogEntry::Sub { enter, .. } => { + if let Some(val) = enter.all_fields().fields.get("name") + && let Some(s) = val.as_str() + { + Some(s.to_string()) + } else { + enter.fields.message().map(|i| i.to_string()) + } + } + } + } + + pub fn line_text(&self, accessed: bool, inline_depth: usize) -> String { const NO_MESSAGE: &str = ""; const SPACES_BEFORE: &str = " "; let indent = " >".repeat(inline_depth); @@ -144,11 +158,11 @@ impl LogEntry { if let Some(val) = enter.all_fields().fields.get("name") && let Some(s) = val.as_str() { - Line::from(format!( + format!( "{:4}⭣{:4}⇊ ┃{indent}↪ {s}", sub_entries.len(), self.count().wrapping_sub(1) - )) + ) } else { format!("{SPACES_BEFORE}┃{indent}{}", single_field(enter)).into() }