don't show empty elems and mouse support
This commit is contained in:
parent
4a7817a239
commit
0457d63bd0
6 changed files with 173 additions and 41 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1191,6 +1191,7 @@ name = "rustc-logviz"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"crossterm",
|
||||
"itertools",
|
||||
"jiff",
|
||||
"nix 0.31.1",
|
||||
|
|
|
|||
|
|
@ -19,3 +19,4 @@ thiserror = "2"
|
|||
itertools = "0.14"
|
||||
nix = {version = "0.31", features = ["process", "signal"]}
|
||||
regex = "1"
|
||||
crossterm = "*"
|
||||
|
|
|
|||
|
|
@ -145,7 +145,12 @@ pub struct LogViewer {
|
|||
filters: Vec<Rc<Filter>>,
|
||||
|
||||
pub root_stream: Box<dyn LogStream>,
|
||||
|
||||
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<LogEntry>, 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)))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
104
src/tui/mod.rs
104
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<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) {
|
||||
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<DirEntry>,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue