don't show empty elems and mouse support

This commit is contained in:
Jana Dönszelmann 2026-02-25 13:51:58 +01:00
parent 4a7817a239
commit 0457d63bd0
No known key found for this signature in database
6 changed files with 173 additions and 41 deletions

1
Cargo.lock generated
View file

@ -1191,6 +1191,7 @@ name = "rustc-logviz"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"crossterm",
"itertools", "itertools",
"jiff", "jiff",
"nix 0.31.1", "nix 0.31.1",

View file

@ -19,3 +19,4 @@ thiserror = "2"
itertools = "0.14" itertools = "0.14"
nix = {version = "0.31", features = ["process", "signal"]} nix = {version = "0.31", features = ["process", "signal"]}
regex = "1" regex = "1"
crossterm = "*"

View file

@ -145,7 +145,12 @@ pub struct LogViewer {
filters: Vec<Rc<Filter>>, filters: Vec<Rc<Filter>>,
pub root_stream: Box<dyn LogStream>, pub root_stream: Box<dyn LogStream>,
pub last_height: usize, pub last_height: usize,
pub last_offset: usize,
pub last_fields_offset: usize,
pub last_fields_height: usize,
pub footer_list: ListState, pub footer_list: ListState,
pub input_state: InputState, pub input_state: InputState,
@ -162,7 +167,12 @@ impl LogViewer {
root_stream: stream.clone(), root_stream: stream.clone(),
cache: HashMap::new(), cache: HashMap::new(),
footer_list: ListState::default(), footer_list: ListState::default(),
last_height: 0, last_height: 0,
last_offset: 0,
last_fields_offset: 0,
last_fields_height: 0,
filters: Vec::new(), filters: Vec::new(),
input_state: InputState::None, input_state: InputState::None,
} }
@ -326,35 +336,61 @@ impl LogViewer {
Some((res, self.curr.selection_offset)) 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<LogEntry>, usize)> { pub fn selected(&self) -> Option<(Rc<LogEntry>, usize)> {
self.curr.selected() self.curr.selected()
} }
pub fn prev(&mut self) { pub fn prev(&mut self) {
match self.input_state { 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 { 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.input_state = InputState::None;
} }
InputState::Target(InputTarget::Fields(None)) => {
self.footer_list.previous();
}
_ => {}
} }
} }
pub fn next(&mut self) { pub fn next(&mut self) {
match self.input_state { match self.input_state {
InputState::None => { InputState::Target(InputTarget::Fields(..)) => {
self.curr.selection_offset += 1;
}
InputState::Target(InputTarget::Fields(None)) => {
self.footer_list.next(); 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) { pub fn home(&mut self) {
match self.input_state { 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; self.curr.selection_offset = 0;
while self.curr.iter.prev().is_some() {} 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; 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( self.stack.push(mem::replace(
&mut self.curr, &mut self.curr,
LogView { LogView {
@ -421,7 +465,8 @@ impl LogViewer {
} }
} }
InputState::Target(InputTarget::Fields(None)) => { InputState::Target(InputTarget::Fields(None)) => {
self.footer_list.next(); self.input_state =
InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo)))
} }
_ => {} _ => {}
} }

View file

@ -1,13 +1,15 @@
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, MouseButton, MouseEventKind},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui_themes::{Theme, ThemeName}; use ratatui_themes::{Theme, ThemeName};
use std::{ use std::{
cell::RefCell,
fs::{self, DirEntry}, fs::{self, DirEntry},
io, io::{self, Stdout},
ops::ControlFlow, ops::ControlFlow,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::exit, process::exit,
rc::Rc, rc::Rc,
time::Instant,
}; };
use tui_widget_list::{ListBuilder, ListView}; use tui_widget_list::{ListBuilder, ListView};
@ -23,11 +25,12 @@ use crate::tui::{
widgets::styled::IntoStyled, widgets::styled::IntoStyled,
}; };
use ratatui::{ use ratatui::{
DefaultTerminal, DefaultTerminal, Terminal,
buffer::Buffer, buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
layout::{Constraint, HorizontalAlignment, Layout, Rect}, layout::{Constraint, HorizontalAlignment, Layout, Rect},
style::{self, Style}, prelude::CrosstermBackend,
style::Style,
text::Line, text::Line,
widgets::{ widgets::{
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
@ -75,11 +78,29 @@ perform action on selected target:
alt-i inline alt-i inline
"; ";
fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
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<CrosstermBackend<Stdout>>) -> 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<PathBuf>, theme: ThemeName) { pub fn run(logs_dir: PathBuf, compiler_root: Option<PathBuf>, theme: ThemeName) {
let terminal = ratatui::init(); let mut terminal = setup_terminal().unwrap();
let theme = Theme::new(theme); let theme = Theme::new(theme);
let app_result = App::new(logs_dir, compiler_root, theme).run(terminal); let app_result = App::new(logs_dir, compiler_root, theme).run(&mut terminal);
ratatui::restore(); teardown_terminal(&mut terminal).unwrap();
if let Err(e) = app_result { if let Err(e) = app_result {
eprintln!("error in tui: {e:?}"); eprintln!("error in tui: {e:?}");
@ -92,6 +113,7 @@ enum Tab {
files: Vec<DirEntry>, files: Vec<DirEntry>,
state: ListState, state: ListState,
last_height: usize, last_height: usize,
last_offset: usize,
}, },
LogViewer(LogViewer), LogViewer(LogViewer),
Empty, Empty,
@ -171,6 +193,7 @@ impl App {
files, files,
state: ListState::default(), state: ListState::default(),
last_height: 0, last_height: 0,
last_offset: 0,
}, },
Err(_) => Tab::Empty, Err(_) => Tab::Empty,
} }
@ -192,6 +215,7 @@ impl App {
files, files,
state, state,
last_height, last_height,
last_offset: _,
} => match key.code { } => match key.code {
KeyCode::Char('j') | KeyCode::Down => state.select_next(), KeyCode::Char('j') | KeyCode::Down => state.select_next(),
KeyCode::Char('k') | KeyCode::Up => state.select_previous(), KeyCode::Char('k') | KeyCode::Up => state.select_previous(),
@ -337,7 +361,7 @@ impl App {
ratatui::restore(); ratatui::restore();
let self_pid = nix::unistd::getpid(); let self_pid = nix::unistd::getpid();
let _ = nix::sys::signal::kill(self_pid, nix::sys::signal::SIGTSTP); let _ = nix::sys::signal::kill(self_pid, nix::sys::signal::SIGTSTP);
*terminal = ratatui::init(); *terminal = setup_terminal().unwrap();
} }
KeyCode::Char('?') => { KeyCode::Char('?') => {
if matches!(self.current_tab(), Tab::Help) { if matches!(self.current_tab(), Tab::Help) {
@ -357,20 +381,62 @@ impl App {
ControlFlow::Continue(()) ControlFlow::Continue(())
} }
fn run(mut self, mut terminal: DefaultTerminal) -> io::Result<()> { fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
loop { loop {
self.last_error.check_expired(); self.last_error.check_expired();
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
if let Event::Key(key) = event::read()? { match event::read()? {
// to initialize, but we then do get manually for borrow reasons Event::Key(key) => {
self.current_tab(); // to initialize, but we then do get manually for borrow reasons
self.current_tab();
// always active // always active
if self.handle_generic_keycode(key, &mut terminal).is_break() { if self.handle_generic_keycode(key, terminal).is_break() {
break Ok(()); 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, files,
state, state,
last_height, last_height,
last_offset,
} => { } => {
let list = List::new(files.iter().map(|file| { let list = List::new(files.iter().map(|file| {
ListItem::new(file.file_name().to_string_lossy().into_owned()) ListItem::new(file.file_name().to_string_lossy().into_owned())
@ -497,10 +564,12 @@ impl Widget for &mut App {
.highlight_style(styles.highlighted); .highlight_style(styles.highlighted);
*last_height = main_area.height as usize; *last_height = main_area.height as usize;
*last_offset = main_area.y as usize;
StatefulWidget::render(list, main_area, buf, state); StatefulWidget::render(list, main_area, buf, state);
} }
Tab::LogViewer(lv) => { Tab::LogViewer(lv) => {
lv.last_offset = main_area.y as usize;
lv.update_num_items(main_area.height as usize); lv.update_num_items(main_area.height as usize);
let (items, selected_offset) = lv let (items, selected_offset) = lv
@ -549,6 +618,9 @@ impl Widget for &mut App {
.render(main_area, buf); .render(main_area, buf);
let items = lv.footer_fields(); let items = lv.footer_fields();
lv.last_fields_offset = footer_area.y as usize;
lv.last_fields_height = items.len();
let width = 20; let width = 20;
let builder = ListBuilder::new(|cx| { let builder = ListBuilder::new(|cx| {
let Some((k, v)) = &items.get(cx.index) else { let Some((k, v)) = &items.get(cx.index) else {

View file

@ -62,6 +62,10 @@ impl LogEntry {
(PathBuf::from(entry.filename.clone()), entry.line_number) (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 { pub fn hash(&self) -> u64 {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
match self { match self {
@ -154,11 +158,17 @@ impl LogEntry {
} => { } => {
if let Some(val) = enter.all_fields().fields.get("name") { if let Some(val) = enter.all_fields().fields.get("name") {
LineText::new( LineText::new(
format!( if sub_entries.is_empty()
"{:4}⭣{:4}⇊ ", || sub_entries.get(0).is_some_and(|i| i.is_return())
sub_entries.len(), {
self.count().wrapping_sub(1) SPACES_BEFORE.to_string()
), } else {
format!(
"{:4}⭣{:4}⇊ ",
sub_entries.len(),
self.count().wrapping_sub(1)
)
},
format!("{val}"), format!("{val}"),
self.message_or_name(), self.message_or_name(),
tree, tree,

View file

@ -138,11 +138,14 @@ impl Widget for Styled<'_, &Items<'_>> {
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
&& let InputState::None | InputState::Target(InputTarget::This) = && let InputState::None
self.input_state | InputState::Target(InputTarget::This)
| InputState::Target(InputTarget::Fields(..)) = self.input_state
{ {
line.highlight(Highlighted::All); 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 && let Some(msg) = &line.message_text
{ {
match s { match s {
@ -169,7 +172,7 @@ impl Widget for Styled<'_, &Items<'_>> {
if let Ok(regex) = regex_cache.get_or_init(|| { if let Ok(regex) = regex_cache.get_or_init(|| {
let regex = Regex::new(r); let regex = Regex::new(r);
if let Err(e) = &regex { if let Err(e) = &regex {
self.last_error.set(format!("{e}")); self.last_error.set(e.to_string());
} }
regex regex
}) && let Some(start_offset) = line.message.find(msg) }) && let Some(start_offset) = line.message.find(msg)