help overview and input rework

This commit is contained in:
Jana Dönszelmann 2026-02-25 01:08:35 +01:00
parent a6d501977c
commit f733c65bcf
No known key found for this signature in database
3 changed files with 275 additions and 214 deletions

View file

@ -59,7 +59,7 @@ enum Preset {
compiler_root: Option<PathBuf>, compiler_root: Option<PathBuf>,
/// Path where the compiler source code lives, for links in the TUI to work. /// 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")] #[arg(long = "theme")]
theme: Theme, theme: Theme,
}, },

View file

@ -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 { pub struct LogViewer {
stack: Vec<LogView>, stack: Vec<LogView>,
curr: LogView, curr: LogView,
@ -40,8 +59,9 @@ pub struct LogViewer {
pub root_stream: Box<dyn LogStream>, pub root_stream: Box<dyn LogStream>,
pub last_height: usize, pub last_height: usize,
pub footer_selected: bool,
pub footer_list: ListState, pub footer_list: ListState,
pub input_state: InputState,
} }
impl LogViewer { impl LogViewer {
@ -55,9 +75,9 @@ impl LogViewer {
root_stream: stream.clone(), root_stream: stream.clone(),
cache: HashMap::new(), cache: HashMap::new(),
footer_list: ListState::default(), footer_list: ListState::default(),
footer_selected: false,
last_height: 0, last_height: 0,
filters: Vec::new(), filters: Vec::new(),
input_state: InputState::None,
} }
} }
@ -224,36 +244,35 @@ impl LogViewer {
self.curr.selected() self.curr.selected()
} }
fn update_footer_select(&mut self) {
self.footer_list.select(Some(0));
}
pub fn prev(&mut self) { pub fn prev(&mut self) {
if self.footer_selected { match self.input_state {
self.footer_list.previous(); InputState::None => {
} else {
if self.curr.selection_offset == 0 { if self.curr.selection_offset == 0 {
let _ = self.curr.iter.prev(); let _ = self.curr.iter.prev();
} else { } else {
self.curr.selection_offset -= 1; self.curr.selection_offset -= 1;
} }
self.update_footer_select(); }
InputState::Target(InputTarget::Fields) => {
self.footer_list.previous();
}
} }
} }
pub fn next(&mut self) { pub fn next(&mut self) {
if self.footer_selected { match self.input_state {
self.footer_list.next(); InputState::None => {
} else {
self.curr.selection_offset += 1; self.curr.selection_offset += 1;
self.update_footer_select(); }
InputState::Target(InputTarget::Fields) => {
self.footer_list.next();
}
} }
} }
pub fn page_down(&mut self) { pub fn page_down(&mut self) {
self.curr.selection_offset += self.last_height; self.curr.selection_offset += self.last_height;
self.footer_selected = false; self.input_state.reset();
self.update_footer_select();
} }
pub fn page_up(&mut self) { pub fn page_up(&mut self) {
@ -264,17 +283,18 @@ impl LogViewer {
self.curr.selection_offset -= 1; self.curr.selection_offset -= 1;
} }
} }
self.footer_selected = false; self.input_state.reset();
self.update_footer_select();
} }
pub fn home(&mut self) { pub fn home(&mut self) {
if self.footer_selected { match self.input_state {
self.footer_list.select(Some(0)); InputState::None => {
} else {
self.curr.selection_offset = 0; self.curr.selection_offset = 0;
while self.curr.iter.prev().is_some() {} while self.curr.iter.prev().is_some() {}
self.update_footer_select(); }
InputState::Target(InputTarget::Fields) => {
self.footer_list.select(Some(0));
}
} }
} }
@ -287,16 +307,12 @@ impl LogViewer {
if let Some(stack) = self.stack.pop() { if let Some(stack) = self.stack.pop() {
self.curr = stack; self.curr = stack;
} }
self.footer_selected = false; self.input_state.reset();
self.update_footer_select();
}
pub fn switch_focus(&mut self) {
self.footer_selected = !self.footer_selected;
} }
pub fn enter(&mut self) { pub fn enter(&mut self) {
if !self.footer_selected { match self.input_state {
InputState::None => {
let Some((s, _)) = self.selected() else { let Some((s, _)) = self.selected() else {
return; return;
}; };
@ -314,7 +330,10 @@ impl LogViewer {
if let Some(cached_view) = self.cache.get(&self.path()) { if let Some(cached_view) = self.cache.get(&self.path()) {
self.curr = cached_view.clone(); self.curr = cached_view.clone();
} }
self.update_footer_select(); }
InputState::Target(InputTarget::Fields) => {
self.footer_list.next();
}
} }
} }
} }

View file

@ -3,6 +3,7 @@ use ratatui_themes::{Theme, ThemeName};
use std::{ use std::{
fs::{self, DirEntry}, fs::{self, DirEntry},
io, io,
ops::ControlFlow,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::exit, process::exit,
rc::Rc, rc::Rc,
@ -11,7 +12,7 @@ use tui_widget_list::{ListBuilder, ListView};
use crate::tui::{ use crate::tui::{
filter::{FilterKind, WipMatcher}, filter::{FilterKind, WipMatcher},
log_viewer::LogViewer, log_viewer::{InputState, InputTarget, LogViewer},
}; };
use crate::tui::{ use crate::tui::{
filter::{FilterSelection, WipFilter}, filter::{FilterSelection, WipFilter},
@ -20,7 +21,7 @@ use crate::tui::{
use ratatui::{ use ratatui::{
DefaultTerminal, DefaultTerminal,
buffer::Buffer, buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyModifiers}, crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
layout::{Constraint, HorizontalAlignment, Layout, Rect}, layout::{Constraint, HorizontalAlignment, Layout, Rect},
style::Style, style::Style,
text::{Line, Text}, text::{Line, Text},
@ -58,6 +59,7 @@ enum Tab {
filter: WipFilter, filter: WipFilter,
}, },
Empty, Empty,
Help,
} }
impl Tab { impl Tab {
@ -68,31 +70,33 @@ impl Tab {
(Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()), (Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()),
(Tab::LogViewer(_), None) => "logs".to_string(), (Tab::LogViewer(_), None) => "logs".to_string(),
(Tab::CreateFilter { .. }, _) => "create filter".to_string(), (Tab::CreateFilter { .. }, _) => "create filter".to_string(),
(Tab::Help, _) => "help".to_string(),
} }
} }
} }
fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter { fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter {
let matcher = if lv.footer_selected { todo!()
let footer_fields = lv.footer_fields(); // let matcher = if lv.fields_selected {
let (key, value) = footer_fields // let footer_fields = lv.footer_fields();
.get(lv.footer_list.selected.unwrap_or(0)) // let (key, value) = footer_fields
.map_or((None, None), |(k, v)| (Some(k), Some(v))); // .get(lv.footer_list.selected.unwrap_or(0))
Some(WipMatcher::Field { // .map_or((None, None), |(k, v)| (Some(k), Some(v)));
name: key.cloned(), // Some(WipMatcher::Field {
value: value.cloned(), // name: key.cloned(),
}) // value: value.cloned(),
} else { // })
Some(WipMatcher::Specific { // } else {
hash: lv.selected().map(|(i, _)| i.hash()), // Some(WipMatcher::Specific {
}) // hash: lv.selected().map(|(i, _)| i.hash()),
}; // })
// };
WipFilter { // WipFilter {
matcher, // matcher,
kind, // kind,
selection: filter::FilterSelection::Kind, // selection: filter::FilterSelection::Kind,
} // }
} }
struct App { struct App {
@ -166,106 +170,22 @@ impl App {
self.tabs.last_mut().unwrap() self.tabs.last_mut().unwrap()
} }
fn run(mut self, mut terminal: DefaultTerminal) -> io::Result<()> { fn handle_current_tab_keycode(&mut self, key: KeyEvent) {
loop { match self.tabs.last_mut().unwrap() {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; Tab::Help => {}
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::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 { Tab::FileChooser {
state, last_height, .. files,
} => state.scroll_down_by(*last_height as u16), state,
Tab::LogViewer(lv) => { last_height,
lv.page_down(); } => match key.code {
} KeyCode::Char('j') | KeyCode::Down => state.select_next(),
Tab::Empty => {} KeyCode::Char('k') | KeyCode::Up => state.select_previous(),
Tab::CreateFilter { .. } => {} KeyCode::PageUp => state.scroll_up_by(*last_height as u16),
}, KeyCode::PageDown => state.scroll_down_by(*last_height as u16),
(KeyCode::PageUp, tab) => match tab { KeyCode::Char('G') | KeyCode::Home => state.select_first(),
Tab::FileChooser { KeyCode::Char('g') | KeyCode::End => state.select_last(),
state, last_height, .. KeyCode::Enter => {
} => 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() if let Some(selected) = state.selected()
&& let Some(selected) = files.get(selected) && let Some(selected) = files.get(selected)
{ {
@ -280,40 +200,111 @@ impl App {
} }
} }
} }
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();
}
}
},
_ => {} _ => {}
},
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()))?;
if let Event::Key(key) = event::read()? {
// to initialize, but we then do get manually for borrow reasons
self.current_tab();
// 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); .areas(popup_area);
let (footer_focused, header_focused) = match self.current_tab() { let (footer_focused, header_focused) = match self.current_tab() {
Tab::Help => (false, false),
Tab::FileChooser { .. } => (false, true), 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::Empty => (false, false),
Tab::CreateFilter { .. } => (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); StatefulWidget::render(list, footer_area, buf, &mut lv.footer_list);
} }
Tab::Empty => {} 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
<esc> 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 } => { Tab::CreateFilter { filter } => {
Clear.render(popup_area, buf); Clear.render(popup_area, buf);
let popup_area = { let popup_area = {