From 0457d63bd081d2c09dc614caaa9939a91e735f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= Date: Wed, 25 Feb 2026 13:51:58 +0100 Subject: [PATCH] don't show empty elems and mouse support --- Cargo.lock | 1 + Cargo.toml | 1 + src/tui/log_viewer.rs | 77 +++++++++++++++++++++++------ src/tui/mod.rs | 104 +++++++++++++++++++++++++++++++++------ src/tui/model.rs | 20 ++++++-- src/tui/widgets/items.rs | 11 +++-- 6 files changed, 173 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 765ecb9..2cf6d04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1191,6 +1191,7 @@ name = "rustc-logviz" version = "0.1.0" dependencies = [ "clap", + "crossterm", "itertools", "jiff", "nix 0.31.1", diff --git a/Cargo.toml b/Cargo.toml index e9b192c..bd16f70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ thiserror = "2" itertools = "0.14" nix = {version = "0.31", features = ["process", "signal"]} regex = "1" +crossterm = "*" diff --git a/src/tui/log_viewer.rs b/src/tui/log_viewer.rs index 80f874a..016c144 100644 --- a/src/tui/log_viewer.rs +++ b/src/tui/log_viewer.rs @@ -145,7 +145,12 @@ pub struct LogViewer { filters: Vec>, pub root_stream: Box, + pub last_height: usize, + pub last_offset: usize, + pub last_fields_offset: usize, + pub last_fields_height: usize, + pub footer_list: ListState, pub input_state: InputState, @@ -162,7 +167,12 @@ impl LogViewer { root_stream: stream.clone(), cache: HashMap::new(), footer_list: ListState::default(), + last_height: 0, + last_offset: 0, + last_fields_offset: 0, + last_fields_height: 0, + filters: Vec::new(), input_state: InputState::None, } @@ -326,35 +336,61 @@ impl LogViewer { Some((res, self.curr.selection_offset)) } + pub fn click(&mut self, row: u16) { + if row as usize >= self.last_offset { + let row_in_list = row as usize - self.last_offset; + if row_in_list < self.last_height { + if self.curr.selection_offset == row_in_list { + self.input_state = InputState::None; + self.enter(); + } else { + self.curr.selection_offset = row_in_list; + self.input_state = InputState::Target(InputTarget::This); + } + } + } + + if row as usize >= self.last_fields_offset { + let row_in_fields = row as usize - self.last_fields_offset; + if row_in_fields < self.last_fields_height { + self.input_state = + InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))); + self.footer_list.select(Some(row_in_fields)); + } + } + } + pub fn selected(&self) -> Option<(Rc, usize)> { self.curr.selected() } pub fn prev(&mut self) { match self.input_state { - InputState::None => { + InputState::Target(InputTarget::Fields(..)) => { + self.footer_list.previous(); + self.input_state = InputState::Target(InputTarget::Fields(None)); + } + _ => { if self.curr.selection_offset == 0 { let _ = self.curr.iter.prev(); } else { self.curr.selection_offset -= 1; } + self.input_state = InputState::None; } - InputState::Target(InputTarget::Fields(None)) => { - self.footer_list.previous(); - } - _ => {} } } pub fn next(&mut self) { match self.input_state { - InputState::None => { - self.curr.selection_offset += 1; - } - InputState::Target(InputTarget::Fields(None)) => { + InputState::Target(InputTarget::Fields(..)) => { self.footer_list.next(); + self.input_state = InputState::Target(InputTarget::Fields(None)); + } + _ => { + self.curr.selection_offset += 1; + self.input_state = InputState::None; } - _ => {} } } @@ -376,14 +412,15 @@ impl LogViewer { pub fn home(&mut self) { match self.input_state { - InputState::None => { + InputState::Target(InputTarget::Fields(..)) => { + self.footer_list.select(Some(0)); + self.input_state = InputState::Target(InputTarget::Fields(None)); + } + _ => { self.curr.selection_offset = 0; while self.curr.iter.prev().is_some() {} + self.input_state = InputState::None; } - InputState::Target(InputTarget::Fields(None)) => { - self.footer_list.select(Some(0)); - } - _ => {} } } @@ -409,6 +446,13 @@ impl LogViewer { return; }; + if i.clone().next().is_none() { + return; + } + if i.clone().next().is_some_and(|(i, _)| i.is_return()) { + return; + } + self.stack.push(mem::replace( &mut self.curr, LogView { @@ -421,7 +465,8 @@ impl LogViewer { } } InputState::Target(InputTarget::Fields(None)) => { - self.footer_list.next(); + self.input_state = + InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))) } _ => {} } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 4c62981..fed84f1 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,13 +1,15 @@ +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, MouseButton, MouseEventKind}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen}, +}; use ratatui_themes::{Theme, ThemeName}; use std::{ - cell::RefCell, fs::{self, DirEntry}, - io, + io::{self, Stdout}, ops::ControlFlow, path::{Path, PathBuf}, process::exit, rc::Rc, - time::Instant, }; use tui_widget_list::{ListBuilder, ListView}; @@ -23,11 +25,12 @@ use crate::tui::{ widgets::styled::IntoStyled, }; use ratatui::{ - DefaultTerminal, + DefaultTerminal, Terminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, layout::{Constraint, HorizontalAlignment, Layout, Rect}, - style::{self, Style}, + prelude::CrosstermBackend, + style::Style, text::Line, widgets::{ Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, @@ -75,11 +78,29 @@ perform action on selected target: alt-i inline "; +fn setup_terminal() -> io::Result>> { + let mut stdout = io::stdout(); + crossterm::terminal::enable_raw_mode()?; + { + use ::std::io::Write; + crossterm::queue!(stdout, EnterAlternateScreen, EnableMouseCapture) + .and_then(|()| ::std::io::Write::flush(stdout.by_ref())) + }?; + Terminal::new(CrosstermBackend::new(stdout)) +} + +fn teardown_terminal(terminal: &mut Terminal>) -> io::Result<()> { + let mut stdout = io::stdout(); + crossterm::terminal::disable_raw_mode()?; + crossterm::execute!(stdout, LeaveAlternateScreen, DisableMouseCapture,)?; + Ok(()) +} + pub fn run(logs_dir: PathBuf, compiler_root: Option, theme: ThemeName) { - let terminal = ratatui::init(); + let mut terminal = setup_terminal().unwrap(); let theme = Theme::new(theme); - let app_result = App::new(logs_dir, compiler_root, theme).run(terminal); - ratatui::restore(); + let app_result = App::new(logs_dir, compiler_root, theme).run(&mut terminal); + teardown_terminal(&mut terminal).unwrap(); if let Err(e) = app_result { eprintln!("error in tui: {e:?}"); @@ -92,6 +113,7 @@ enum Tab { files: Vec, state: ListState, last_height: usize, + last_offset: usize, }, LogViewer(LogViewer), Empty, @@ -171,6 +193,7 @@ impl App { files, state: ListState::default(), last_height: 0, + last_offset: 0, }, Err(_) => Tab::Empty, } @@ -192,6 +215,7 @@ impl App { files, state, last_height, + last_offset: _, } => match key.code { KeyCode::Char('j') | KeyCode::Down => state.select_next(), KeyCode::Char('k') | KeyCode::Up => state.select_previous(), @@ -337,7 +361,7 @@ impl App { ratatui::restore(); let self_pid = nix::unistd::getpid(); let _ = nix::sys::signal::kill(self_pid, nix::sys::signal::SIGTSTP); - *terminal = ratatui::init(); + *terminal = setup_terminal().unwrap(); } KeyCode::Char('?') => { if matches!(self.current_tab(), Tab::Help) { @@ -357,20 +381,62 @@ impl App { ControlFlow::Continue(()) } - fn run(mut self, mut terminal: DefaultTerminal) -> io::Result<()> { + fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { loop { self.last_error.check_expired(); 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(); + match event::read()? { + Event::Key(key) => { + // 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(()); + // always active + if self.handle_generic_keycode(key, terminal).is_break() { + break Ok(()); + } } + Event::FocusGained => {} + Event::FocusLost => {} + Event::Mouse(mouse_event) => { + if let MouseEventKind::Up(MouseButton::Left) = mouse_event.kind { + if let Tab::LogViewer(lv) = self.current_tab() { + lv.click(mouse_event.row); + } else if let Tab::FileChooser { + files, + state, + last_height: _, + last_offset, + } = self.current_tab() + { + if mouse_event.row as usize > *last_offset { + let row = mouse_event.row as usize - *last_offset; + if row < files.len() { + if state.selected() == Some(row) + && let Some(selected) = files.get(row) + { + match LogfileReader::new(&selected.path()) { + Ok(i) => { + self.current_file = Some(i.clone()); + self.replace_tab(Tab::LogViewer(LogViewer::new( + i.iter(), + ))); + } + Err(_) => { + panic!() + } + } + } else { + state.select(Some(row)); + } + } + } + } + } + } + Event::Paste(_) => {} + Event::Resize(_, _) => {} } } } @@ -489,6 +555,7 @@ impl Widget for &mut App { files, state, last_height, + last_offset, } => { let list = List::new(files.iter().map(|file| { ListItem::new(file.file_name().to_string_lossy().into_owned()) @@ -497,10 +564,12 @@ impl Widget for &mut App { .highlight_style(styles.highlighted); *last_height = main_area.height as usize; + *last_offset = main_area.y as usize; StatefulWidget::render(list, main_area, buf, state); } Tab::LogViewer(lv) => { + lv.last_offset = main_area.y as usize; lv.update_num_items(main_area.height as usize); let (items, selected_offset) = lv @@ -549,6 +618,9 @@ impl Widget for &mut App { .render(main_area, buf); let items = lv.footer_fields(); + lv.last_fields_offset = footer_area.y as usize; + lv.last_fields_height = items.len(); + let width = 20; let builder = ListBuilder::new(|cx| { let Some((k, v)) = &items.get(cx.index) else { diff --git a/src/tui/model.rs b/src/tui/model.rs index 7ddb6bb..99d1fa7 100644 --- a/src/tui/model.rs +++ b/src/tui/model.rs @@ -62,6 +62,10 @@ impl LogEntry { (PathBuf::from(entry.filename.clone()), entry.line_number) } + pub fn is_return(&self) -> bool { + self.all_fields().get("return").is_some() + } + pub fn hash(&self) -> u64 { let mut hasher = DefaultHasher::new(); match self { @@ -154,11 +158,17 @@ impl LogEntry { } => { if let Some(val) = enter.all_fields().fields.get("name") { LineText::new( - format!( - "{:4}⭣{:4}⇊ ", - sub_entries.len(), - self.count().wrapping_sub(1) - ), + if sub_entries.is_empty() + || sub_entries.get(0).is_some_and(|i| i.is_return()) + { + SPACES_BEFORE.to_string() + } else { + format!( + "{:4}⭣{:4}⇊ ", + sub_entries.len(), + self.count().wrapping_sub(1) + ) + }, format!("↪ {val}"), self.message_or_name(), tree, diff --git a/src/tui/widgets/items.rs b/src/tui/widgets/items.rs index 4e91248..214626f 100644 --- a/src/tui/widgets/items.rs +++ b/src/tui/widgets/items.rs @@ -138,11 +138,14 @@ impl Widget for Styled<'_, &Items<'_>> { let mut line = line_text.styled(&self.styles); if idx == self.selected_offset - && let InputState::None | InputState::Target(InputTarget::This) = - self.input_state + && let InputState::None + | InputState::Target(InputTarget::This) + | InputState::Target(InputTarget::Fields(..)) = self.input_state { line.highlight(Highlighted::All); - } else if let InputState::Target(InputTarget::Text(s)) = self.input_state + } + + if let InputState::Target(InputTarget::Text(s)) = self.input_state && let Some(msg) = &line.message_text { match s { @@ -169,7 +172,7 @@ impl Widget for Styled<'_, &Items<'_>> { if let Ok(regex) = regex_cache.get_or_init(|| { let regex = Regex::new(r); if let Err(e) = ®ex { - self.last_error.set(format!("{e}")); + self.last_error.set(e.to_string()); } regex }) && let Some(start_offset) = line.message.find(msg)