From f733c65bcf88f10e64f3f054503a70e44c6917ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= Date: Wed, 25 Feb 2026 01:08:35 +0100 Subject: [PATCH] help overview and input rework --- src/main.rs | 2 +- src/tui/log_viewer.rs | 123 ++++++++------ src/tui/mod.rs | 364 +++++++++++++++++++++++------------------- 3 files changed, 275 insertions(+), 214 deletions(-) diff --git a/src/main.rs b/src/main.rs index fb2bfc9..4a9e553 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ enum Preset { compiler_root: Option, /// Path where the compiler source code lives, for links in the TUI to work. - #[arg(default_value_t = Theme(ThemeName::OneDarkPro))] + #[arg(default_value_t = Theme(ThemeName::Dracula))] #[arg(long = "theme")] theme: Theme, }, diff --git a/src/tui/log_viewer.rs b/src/tui/log_viewer.rs index 7cf02cd..2e4865c 100644 --- a/src/tui/log_viewer.rs +++ b/src/tui/log_viewer.rs @@ -32,6 +32,25 @@ impl Clone for LogView { } } +pub enum InputTarget { + Fields, +} + +pub enum InputState { + None, + Target(InputTarget), +} + +impl InputState { + pub fn reset(&mut self) { + *self = Self::None; + } + + pub fn target(&mut self, target: InputTarget) { + *self = Self::Target(target); + } +} + pub struct LogViewer { stack: Vec, curr: LogView, @@ -40,8 +59,9 @@ pub struct LogViewer { pub root_stream: Box, pub last_height: usize, - pub footer_selected: bool, pub footer_list: ListState, + + pub input_state: InputState, } impl LogViewer { @@ -55,9 +75,9 @@ impl LogViewer { root_stream: stream.clone(), cache: HashMap::new(), footer_list: ListState::default(), - footer_selected: false, last_height: 0, filters: Vec::new(), + input_state: InputState::None, } } @@ -224,36 +244,35 @@ impl LogViewer { self.curr.selected() } - fn update_footer_select(&mut self) { - self.footer_list.select(Some(0)); - } - pub fn prev(&mut self) { - if self.footer_selected { - self.footer_list.previous(); - } else { - if self.curr.selection_offset == 0 { - let _ = self.curr.iter.prev(); - } else { - self.curr.selection_offset -= 1; + match self.input_state { + InputState::None => { + if self.curr.selection_offset == 0 { + let _ = self.curr.iter.prev(); + } else { + self.curr.selection_offset -= 1; + } + } + InputState::Target(InputTarget::Fields) => { + self.footer_list.previous(); } - self.update_footer_select(); } } pub fn next(&mut self) { - if self.footer_selected { - self.footer_list.next(); - } else { - self.curr.selection_offset += 1; - self.update_footer_select(); + match self.input_state { + InputState::None => { + self.curr.selection_offset += 1; + } + InputState::Target(InputTarget::Fields) => { + self.footer_list.next(); + } } } pub fn page_down(&mut self) { self.curr.selection_offset += self.last_height; - self.footer_selected = false; - self.update_footer_select(); + self.input_state.reset(); } pub fn page_up(&mut self) { @@ -264,17 +283,18 @@ impl LogViewer { self.curr.selection_offset -= 1; } } - self.footer_selected = false; - self.update_footer_select(); + self.input_state.reset(); } pub fn home(&mut self) { - if self.footer_selected { - self.footer_list.select(Some(0)); - } else { - self.curr.selection_offset = 0; - while self.curr.iter.prev().is_some() {} - self.update_footer_select(); + match self.input_state { + InputState::None => { + self.curr.selection_offset = 0; + while self.curr.iter.prev().is_some() {} + } + InputState::Target(InputTarget::Fields) => { + self.footer_list.select(Some(0)); + } } } @@ -287,34 +307,33 @@ impl LogViewer { if let Some(stack) = self.stack.pop() { self.curr = stack; } - self.footer_selected = false; - self.update_footer_select(); - } - - pub fn switch_focus(&mut self) { - self.footer_selected = !self.footer_selected; + self.input_state.reset(); } pub fn enter(&mut self) { - if !self.footer_selected { - let Some((s, _)) = self.selected() else { - return; - }; - let Some(i) = s.from_start(0) else { - return; - }; + match self.input_state { + InputState::None => { + let Some((s, _)) = self.selected() else { + return; + }; + let Some(i) = s.from_start(0) else { + return; + }; - self.stack.push(mem::replace( - &mut self.curr, - LogView { - iter: Box::new(i), - selection_offset: 0, - }, - )); - if let Some(cached_view) = self.cache.get(&self.path()) { - self.curr = cached_view.clone(); + self.stack.push(mem::replace( + &mut self.curr, + LogView { + iter: Box::new(i), + selection_offset: 0, + }, + )); + if let Some(cached_view) = self.cache.get(&self.path()) { + self.curr = cached_view.clone(); + } + } + InputState::Target(InputTarget::Fields) => { + self.footer_list.next(); } - self.update_footer_select(); } } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index a92d57d..d6a7068 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -3,6 +3,7 @@ use ratatui_themes::{Theme, ThemeName}; use std::{ fs::{self, DirEntry}, io, + ops::ControlFlow, path::{Path, PathBuf}, process::exit, rc::Rc, @@ -11,7 +12,7 @@ use tui_widget_list::{ListBuilder, ListView}; use crate::tui::{ filter::{FilterKind, WipMatcher}, - log_viewer::LogViewer, + log_viewer::{InputState, InputTarget, LogViewer}, }; use crate::tui::{ filter::{FilterSelection, WipFilter}, @@ -20,7 +21,7 @@ use crate::tui::{ use ratatui::{ DefaultTerminal, buffer::Buffer, - crossterm::event::{self, Event, KeyCode, KeyModifiers}, + crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, layout::{Constraint, HorizontalAlignment, Layout, Rect}, style::Style, text::{Line, Text}, @@ -58,6 +59,7 @@ enum Tab { filter: WipFilter, }, Empty, + Help, } impl Tab { @@ -68,31 +70,33 @@ impl Tab { (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 { - let matcher = if lv.footer_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()), - }) - }; + 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, - } + // WipFilter { + // matcher, + // kind, + // selection: filter::FilterSelection::Kind, + // } } struct App { @@ -166,6 +170,92 @@ impl App { self.tabs.last_mut().unwrap() } + fn handle_current_tab_keycode(&mut self, key: KeyEvent) { + match self.tabs.last_mut().unwrap() { + Tab::Help => {} + Tab::Empty => {} + Tab::FileChooser { + files, + state, + last_height, + } => match key.code { + KeyCode::Char('j') | KeyCode::Down => state.select_next(), + KeyCode::Char('k') | KeyCode::Up => state.select_previous(), + KeyCode::PageUp => state.scroll_up_by(*last_height as u16), + KeyCode::PageDown => state.scroll_down_by(*last_height as u16), + KeyCode::Char('G') | KeyCode::Home => state.select_first(), + KeyCode::Char('g') | KeyCode::End => state.select_last(), + KeyCode::Enter => { + if let Some(selected) = state.selected() + && let Some(selected) = files.get(selected) + { + match LogfileReader::new(&selected.path()) { + Ok(i) => { + self.current_file = Some(i.clone()); + self.replace_tab(Tab::LogViewer(LogViewer::new(i.iter()))); + } + Err(_) => { + panic!() + } + } + } + } + _ => {} + }, + Tab::LogViewer(lv) => match key.code { + KeyCode::Char('j') | KeyCode::Down => lv.next(), + KeyCode::Char('k') | KeyCode::Up => lv.prev(), + KeyCode::PageUp => lv.page_up(), + KeyCode::PageDown => lv.page_down(), + KeyCode::Char('G') | KeyCode::Home => lv.home(), + KeyCode::Char('g') | KeyCode::End => todo!(), + KeyCode::Backspace | KeyCode::Left => lv.back(), + KeyCode::Right => lv.enter(), + KeyCode::Enter => lv.enter(), + KeyCode::Char('f') => { + lv.input_state.target(InputTarget::Fields); + } + _ => {} + }, + Tab::CreateFilter { filter } => todo!(), + } + } + + fn handle_generic_keycode( + &mut self, + key: KeyEvent, + terminal: &mut DefaultTerminal, + ) -> ControlFlow<()> { + let num_tabs = self.tabs.len(); + match key.code { + KeyCode::Char('q') => return ControlFlow::Break(()), + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + return ControlFlow::Break(()); + } + KeyCode::Char('z') if key.modifiers.contains(KeyModifiers::CONTROL) => { + ratatui::restore(); + let self_pid = nix::unistd::getpid(); + let _ = nix::sys::signal::kill(self_pid, nix::sys::signal::SIGTSTP); + *terminal = ratatui::init(); + } + KeyCode::Char('?') => { + if matches!(self.current_tab(), Tab::Help) { + self.pop_tab(); + } else { + self.push_tab(Tab::Help); + } + } + KeyCode::Esc if num_tabs > 1 => { + self.pop_tab(); + } + _ => { + self.handle_current_tab_keycode(key); + } + } + + ControlFlow::Continue(()) + } + fn run(mut self, mut terminal: DefaultTerminal) -> io::Result<()> { loop { terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; @@ -173,147 +263,48 @@ impl App { if let Event::Key(key) = event::read()? { // to initialize, but we then do get manually for borrow reasons self.current_tab(); - let num_tabs = self.tabs.len(); - match (key.code, self.tabs.last_mut().unwrap()) { - (KeyCode::Char('q'), _) => return Ok(()), - (KeyCode::Char('c'), _) if key.modifiers.contains(KeyModifiers::CONTROL) => { - return Ok(()); - } - (KeyCode::Char('z'), _) if key.modifiers.contains(KeyModifiers::CONTROL) => { - ratatui::restore(); - let self_pid = nix::unistd::getpid(); - let _ = nix::sys::signal::kill(self_pid, nix::sys::signal::SIGTSTP); - terminal = ratatui::init(); - } - (KeyCode::Esc, _) if num_tabs > 1 => { - self.pop_tab(); - } - (KeyCode::Char('j') | KeyCode::Down, tab) => match tab { - Tab::FileChooser { state, .. } => state.select_next(), - Tab::LogViewer(lv) => lv.next(), - Tab::Empty => {} - Tab::CreateFilter { filter } => { - filter.selection.next(); - } - }, - (KeyCode::Char('k') | KeyCode::Up, tab) => match tab { - Tab::FileChooser { state, .. } => state.select_previous(), - Tab::LogViewer(lv) => { - lv.prev(); - } - Tab::Empty => {} - Tab::CreateFilter { filter } => { - filter.selection.prev(); - } - }, - (KeyCode::PageDown, tab) => match tab { - Tab::FileChooser { - state, last_height, .. - } => state.scroll_down_by(*last_height as u16), - Tab::LogViewer(lv) => { - lv.page_down(); - } - Tab::Empty => {} - Tab::CreateFilter { .. } => {} - }, - (KeyCode::PageUp, tab) => match tab { - Tab::FileChooser { - state, last_height, .. - } => state.scroll_up_by(*last_height as u16), - Tab::LogViewer(lv) => { - lv.page_up(); - } - Tab::Empty => {} - Tab::CreateFilter { .. } => {} - }, - (KeyCode::Char('G') | KeyCode::Home, tab) => match tab { - Tab::FileChooser { state, .. } => state.select_first(), - Tab::LogViewer(lv) => { - lv.home(); - } - Tab::Empty => {} - Tab::CreateFilter { .. } => {} - }, - (KeyCode::Char('g') | KeyCode::End, tab) => match tab { - Tab::FileChooser { state, .. } => state.select_last(), - Tab::LogViewer(_) => {} - Tab::Empty => {} - Tab::CreateFilter { .. } => {} - }, - (KeyCode::Backspace | KeyCode::Left | KeyCode::Esc, Tab::LogViewer(lv)) => { - lv.back(); - } - (KeyCode::Backspace, Tab::CreateFilter { filter }) => { - filter.clear(); - } - (KeyCode::Right, Tab::CreateFilter { filter }) => { - filter.right(); - } - (KeyCode::Left, Tab::CreateFilter { filter }) => { - filter.left(); - } - (KeyCode::Right, Tab::LogViewer(lv)) => lv.enter(), - (KeyCode::Tab, Tab::LogViewer(lv)) => { - lv.switch_focus(); - } - (KeyCode::Char('r'), Tab::LogViewer(lv)) => { - let filter = initialize_filter(lv, Some(FilterKind::Remove)); - self.push_tab(Tab::CreateFilter { filter }); - } - (KeyCode::Char('i'), Tab::LogViewer(lv)) => { - let filter = initialize_filter(lv, Some(FilterKind::Inline)); - self.push_tab(Tab::CreateFilter { filter }); - } - (KeyCode::Enter, tab) => match tab { - Tab::FileChooser { files, state, .. } => { - if let Some(selected) = state.selected() - && let Some(selected) = files.get(selected) - { - match LogfileReader::new(&selected.path()) { - Ok(i) => { - self.current_file = Some(i.clone()); - self.replace_tab(Tab::LogViewer(LogViewer::new(i.iter()))); - } - Err(_) => { - panic!() - } - } - } - } - Tab::LogViewer(lv) => { - if lv.footer_selected { - let filter = initialize_filter(lv, None); - self.push_tab(Tab::CreateFilter { filter }); - } else { - lv.enter() - } - } - Tab::Empty => {} - Tab::CreateFilter { filter } => { - if let FilterSelection::Confirm = filter.selection { - let filter_clone = filter.clone(); - if let Some(lv) = self.tabs.iter_mut().rev().find_map(|i| { - if let Tab::LogViewer(lv) = i { - Some(lv) - } else { - None - } - }) && let Some(filter) = filter_clone.validate() - { - lv.add_filter(Rc::new(filter)); - self.pop_tab(); - if let Tab::LogViewer(lv) = self.current_tab() { - lv.footer_selected = false; - } - } - } else { - filter.selection.next(); - } - } - }, - _ => {} + // always active + if self.handle_generic_keycode(key, &mut terminal).is_break() { + break Ok(()); } + + // match (key.code, self.tabs.last_mut().unwrap()) { + // (KeyCode::Tab, Tab::LogViewer(lv)) => { + // lv.switch_focus(); + // } + // (KeyCode::Char('r'), Tab::LogViewer(lv)) => { + // let filter = initialize_filter(lv, Some(FilterKind::Remove)); + // self.push_tab(Tab::CreateFilter { filter }); + // } + // (KeyCode::Char('i'), Tab::LogViewer(lv)) => { + // let filter = initialize_filter(lv, Some(FilterKind::Inline)); + // self.push_tab(Tab::CreateFilter { filter }); + // } + + // if let FilterSelection::Confirm = filter.selection { + // let filter_clone = filter.clone(); + // if let Some(lv) = self.tabs.iter_mut().rev().find_map(|i| { + // if let Tab::LogViewer(lv) = i { + // Some(lv) + // } else { + // None + // } + // }) && let Some(filter) = filter_clone.validate() + // { + // lv.add_filter(Rc::new(filter)); + // self.pop_tab(); + + // if let Tab::LogViewer(lv) = self.current_tab() { + // lv.footer_selected = false; + // } + // } + // } else { + // filter.selection.next(); + // } + // }, + // _ => {} + // } } } } @@ -351,8 +342,13 @@ impl Widget for &mut App { .areas(popup_area); let (footer_focused, header_focused) = match self.current_tab() { + Tab::Help => (false, false), Tab::FileChooser { .. } => (false, true), - Tab::LogViewer(lv) => (lv.footer_selected, !lv.footer_selected), + Tab::LogViewer(lv) => { + let target_fields = + matches!(lv.input_state, InputState::Target(InputTarget::Fields)); + (target_fields, !target_fields) + } Tab::Empty => (false, false), Tab::CreateFilter { .. } => (false, false), }; @@ -496,6 +492,52 @@ impl Widget for &mut App { StatefulWidget::render(list, footer_area, buf, &mut lv.footer_list); } Tab::Empty => {} + Tab::Help => { + Clear.render(popup_area, buf); + let popup_area = { + let block = Block::bordered() + .title_top("help") + .style(default) + .padding(Padding::symmetric(3, 1)) + .border_style(border_selected); + let inner = block.inner(popup_area); + block.render(popup_area, buf); + 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 = {