refactor logviewer
This commit is contained in:
parent
7ea9d84228
commit
79639be9da
9 changed files with 425 additions and 239 deletions
279
src/tui/mod.rs
279
src/tui/mod.rs
|
|
@ -1,4 +1,3 @@
|
|||
use itertools::Itertools;
|
||||
use ratatui_themes::{Theme, ThemeName};
|
||||
use regex::bytes::Regex;
|
||||
use std::{
|
||||
|
|
@ -15,11 +14,13 @@ use crate::tui::{
|
|||
filter::FilterKind,
|
||||
log_viewer::{InputState, InputTarget, LogViewer},
|
||||
model::pretty_print_value,
|
||||
widgets::{hyperlink::Hyperlink, items::Items, line_text::Highlighted},
|
||||
};
|
||||
use crate::tui::{
|
||||
filter::{Filter, Matcher},
|
||||
log_viewer::FieldMatcher,
|
||||
reader::LogfileReader,
|
||||
widgets::styled::IntoStyled,
|
||||
};
|
||||
use ratatui::{
|
||||
DefaultTerminal,
|
||||
|
|
@ -27,7 +28,7 @@ use ratatui::{
|
|||
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
||||
style::Style,
|
||||
text::{Line, Span, Text},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
||||
},
|
||||
|
|
@ -38,6 +39,7 @@ pub mod log_viewer;
|
|||
pub mod model;
|
||||
pub mod processing;
|
||||
pub mod reader;
|
||||
pub mod widgets;
|
||||
|
||||
const HELP_TEXT: &str = "Generic:
|
||||
? show help
|
||||
|
|
@ -367,6 +369,42 @@ impl App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn styles(&self) -> Styles {
|
||||
let palette = self.theme.palette();
|
||||
let default = Style::new().fg(palette.fg).bg(palette.bg);
|
||||
let highlighted = Style::new().fg(palette.accent).bg(palette.selection);
|
||||
let border = Style::new().fg(palette.fg).bg(palette.bg);
|
||||
let border_highlighted = Style::new().fg(palette.secondary).bg(palette.bg);
|
||||
|
||||
Styles {
|
||||
default,
|
||||
highlighted,
|
||||
border,
|
||||
border_highlighted,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_around(&self, area: Rect, buf: &mut Buffer, selected: bool) -> Rect {
|
||||
let styles = self.styles();
|
||||
let block = Block::bordered()
|
||||
.style(styles.default)
|
||||
.border_style(if selected {
|
||||
styles.border
|
||||
} else {
|
||||
styles.border_highlighted
|
||||
});
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
struct Styles {
|
||||
default: Style,
|
||||
highlighted: Style,
|
||||
border: Style,
|
||||
border_highlighted: Style,
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
|
|
@ -374,12 +412,7 @@ impl Widget for &mut App {
|
|||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let palette = self.theme.palette();
|
||||
let default = Style::new().fg(palette.fg).bg(palette.bg);
|
||||
let highlighted = Style::new().fg(palette.accent).bg(palette.selection);
|
||||
let border = Style::new().fg(palette.fg).bg(palette.bg);
|
||||
let border_selected = Style::new().fg(palette.secondary).bg(palette.bg);
|
||||
|
||||
let styles = self.styles();
|
||||
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Fill(1),
|
||||
|
|
@ -411,32 +444,8 @@ impl Widget for &mut App {
|
|||
Tab::Empty => (false, false),
|
||||
};
|
||||
|
||||
let main_area = {
|
||||
let block = Block::bordered()
|
||||
.style(default)
|
||||
.border_style(if header_focused {
|
||||
border_selected
|
||||
} else {
|
||||
border
|
||||
});
|
||||
let inner = block.inner(main_area);
|
||||
block.render(main_area, buf);
|
||||
inner
|
||||
};
|
||||
|
||||
let footer_area = {
|
||||
let block = Block::bordered()
|
||||
.style(default)
|
||||
.border_style(if footer_focused {
|
||||
border_selected
|
||||
} else {
|
||||
border
|
||||
});
|
||||
|
||||
let inner = block.inner(footer_area);
|
||||
block.render(footer_area, buf);
|
||||
inner
|
||||
};
|
||||
let main_area = self.block_around(main_area, buf, header_focused);
|
||||
let footer_area = self.block_around(footer_area, buf, footer_focused);
|
||||
|
||||
let [left, middle, right] = Layout::horizontal([
|
||||
Constraint::Ratio(1, 3),
|
||||
|
|
@ -453,16 +462,16 @@ impl Widget for &mut App {
|
|||
.join("►");
|
||||
Paragraph::new(breadcrumbs)
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(default)
|
||||
.style(styles.default)
|
||||
.render(left, buf);
|
||||
|
||||
Paragraph::new(self.current_tab().name(current_file_path.as_deref()))
|
||||
.alignment(HorizontalAlignment::Center)
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(default)
|
||||
.style(styles.default)
|
||||
.render(middle, buf);
|
||||
|
||||
Line::from("-").style(default).render(right, buf);
|
||||
Paragraph::new("").style(styles.default).render(right, buf);
|
||||
|
||||
for tab in &mut self.tabs {
|
||||
match tab {
|
||||
|
|
@ -474,8 +483,8 @@ impl Widget for &mut App {
|
|||
let list = List::new(files.iter().map(|file| {
|
||||
ListItem::new(file.file_name().to_string_lossy().into_owned())
|
||||
}))
|
||||
.style(default)
|
||||
.highlight_style(highlighted);
|
||||
.style(styles.default)
|
||||
.highlight_style(styles.highlighted);
|
||||
|
||||
*last_height = main_area.height as usize;
|
||||
|
||||
|
|
@ -488,129 +497,7 @@ impl Widget for &mut App {
|
|||
.items(main_area.height as usize)
|
||||
.unwrap_or_else(|| (Vec::new(), 0));
|
||||
|
||||
Line::from(lv.input_state.show())
|
||||
.style(default)
|
||||
.render(right, buf);
|
||||
|
||||
let list = List::new(items.into_iter().enumerate().map(
|
||||
|(idx, (i, inline_depth))| {
|
||||
let line_text = i.line_text(false, inline_depth);
|
||||
|
||||
let mut line = Line::from(line_text.clone());
|
||||
if idx == selected_offset
|
||||
&& let InputState::None | InputState::Target(InputTarget::This) =
|
||||
lv.input_state
|
||||
{
|
||||
line = Line::from(line_text).style(highlighted);
|
||||
} else if let InputState::Target(InputTarget::Text(s)) = &lv.input_state
|
||||
&& let Some(msg) = i.message_or_name()
|
||||
{
|
||||
match s {
|
||||
FieldMatcher::EqualTo => {
|
||||
if lv
|
||||
.selected()
|
||||
.and_then(|(i, _)| i.message_or_name())
|
||||
.is_some_and(|m| m == msg)
|
||||
{
|
||||
line = line.style(highlighted);
|
||||
}
|
||||
}
|
||||
FieldMatcher::Prefix(p) => {
|
||||
if msg.starts_with(p)
|
||||
&& let Some(offset) = line_text.find(&msg)
|
||||
{
|
||||
let spans = vec![
|
||||
Span::from(line_text[..offset].to_string()),
|
||||
Span::from(
|
||||
line_text[offset..(offset + p.len())]
|
||||
.to_string(),
|
||||
)
|
||||
.style(highlighted),
|
||||
Span::from(
|
||||
line_text[(offset + p.len())..].to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
line = Line::from(spans);
|
||||
}
|
||||
}
|
||||
FieldMatcher::Regex(r) => {
|
||||
if let Ok(regex) = Regex::new(r)
|
||||
&& let Some(start_offset) = line_text.find(&msg)
|
||||
&& let Some(m) = regex.find(msg.as_bytes())
|
||||
{
|
||||
let spans = vec![
|
||||
Span::from(
|
||||
line_text[..start_offset + m.start()]
|
||||
.to_string(),
|
||||
),
|
||||
Span::from(
|
||||
line_text[start_offset + m.start()
|
||||
..start_offset + m.end()]
|
||||
.to_string(),
|
||||
)
|
||||
.style(highlighted),
|
||||
Span::from(
|
||||
line_text[start_offset + m.end()..].to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
line = Line::from(spans);
|
||||
}
|
||||
}
|
||||
FieldMatcher::Contains(c) => {
|
||||
if msg.contains(c)
|
||||
&& let Some(start_offset) = line_text.find(&msg)
|
||||
&& let Some(contains_offset) =
|
||||
line_text[start_offset..].find(c)
|
||||
{
|
||||
let start = start_offset + contains_offset;
|
||||
let spans = vec![
|
||||
Span::from(line_text[..start].to_string()),
|
||||
Span::from(
|
||||
line_text[start..(start + c.len())].to_string(),
|
||||
)
|
||||
.style(highlighted),
|
||||
Span::from(
|
||||
line_text[(start + c.len())..].to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
line = Line::from(spans);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let InputState::Target(InputTarget::Fields(Some(f))) =
|
||||
&lv.input_state
|
||||
&& let Some(selected_field_offset) = lv.footer_list.selected
|
||||
&& let Some((name, value)) =
|
||||
lv.footer_fields().get(selected_field_offset)
|
||||
&& let Some(current_log_value) = i.all_fields().fields.get(name)
|
||||
{
|
||||
let matches = match f {
|
||||
FieldMatcher::EqualTo => value == current_log_value,
|
||||
FieldMatcher::Prefix(v) => {
|
||||
current_log_value.to_string().starts_with(v)
|
||||
}
|
||||
FieldMatcher::Regex(r) => Regex::new(r).is_ok_and(|i| {
|
||||
i.is_match(current_log_value.to_string().as_bytes())
|
||||
}),
|
||||
FieldMatcher::Contains(c) => {
|
||||
current_log_value.to_string().contains(c)
|
||||
}
|
||||
};
|
||||
|
||||
if matches {
|
||||
line = line.style(highlighted);
|
||||
}
|
||||
}
|
||||
|
||||
let list_item = ListItem::new(line);
|
||||
|
||||
list_item
|
||||
},
|
||||
));
|
||||
Widget::render(list, main_area, buf);
|
||||
lv.input_state.styled_ref(&styles).render(right, buf);
|
||||
|
||||
Clear.render(footer_area, buf);
|
||||
let [first_line, footer_area] =
|
||||
|
|
@ -625,37 +512,50 @@ impl Widget for &mut App {
|
|||
{
|
||||
let full_file_path = canonical_rustc_root.join(&file);
|
||||
Hyperlink::new(
|
||||
Line::from(format!("In file: {}", file.display())).style(default),
|
||||
Line::from(format!("In file: {}", file.display()))
|
||||
.style(styles.default),
|
||||
format!("file://{}:{line}", full_file_path.display()),
|
||||
)
|
||||
.render(first_line, buf);
|
||||
} else {
|
||||
Line::from(format!("In file: {}:{line}", file.display()))
|
||||
.style(default)
|
||||
.style(styles.default)
|
||||
.render(first_line, buf);
|
||||
}
|
||||
}
|
||||
|
||||
Items::new(
|
||||
items,
|
||||
selected_offset,
|
||||
&lv.input_state,
|
||||
lv.footer_list.selected.and_then(|idx| {
|
||||
lv.footer_fields()
|
||||
.get(idx)
|
||||
.map(|(a, b)| (a.clone(), b.clone()))
|
||||
}),
|
||||
)
|
||||
.styled_ref(&styles)
|
||||
.render(main_area, buf);
|
||||
|
||||
let items = lv.footer_fields();
|
||||
let width = 20;
|
||||
let builder = ListBuilder::new(|cx| {
|
||||
let Some((k, v)) = &items.get(cx.index) else {
|
||||
return (Paragraph::new(""), 1);
|
||||
};
|
||||
let contents = pretty_print_value(&v);
|
||||
|
||||
let mut res = Paragraph::new(format!("{k:width$} {contents}"))
|
||||
.wrap(Wrap { trim: false });
|
||||
let mut res =
|
||||
Paragraph::new(format!("{k:width$} {v}")).wrap(Wrap { trim: false });
|
||||
|
||||
if cx.is_selected {
|
||||
res = res.style(highlighted);
|
||||
res = res.style(styles.highlighted);
|
||||
}
|
||||
|
||||
let height = res.line_count(footer_area.width) as u16;
|
||||
(res, height)
|
||||
});
|
||||
|
||||
let list = ListView::new(builder, items.len()).style(default);
|
||||
let list = ListView::new(builder, items.len()).style(styles.default);
|
||||
StatefulWidget::render(list, footer_area, buf, &mut lv.footer_list);
|
||||
}
|
||||
Tab::Empty => {}
|
||||
|
|
@ -664,9 +564,9 @@ impl Widget for &mut App {
|
|||
let popup_area = {
|
||||
let block = Block::bordered()
|
||||
.title_top("help")
|
||||
.style(default)
|
||||
.style(styles.default)
|
||||
.padding(Padding::symmetric(3, 1))
|
||||
.border_style(border_selected);
|
||||
.border_style(styles.border_highlighted);
|
||||
let inner = block.inner(popup_area);
|
||||
block.render(popup_area, buf);
|
||||
inner
|
||||
|
|
@ -678,40 +578,3 @@ impl Widget for &mut App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Hyperlink<'content> {
|
||||
text: Text<'content>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl<'content> Hyperlink<'content> {
|
||||
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
url: url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Hyperlink<'_> {
|
||||
fn render(self, area: Rect, buffer: &mut Buffer) {
|
||||
(&self.text).render(area, buffer);
|
||||
|
||||
// this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
|
||||
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
|
||||
// works by rendering the hyperlink as a series of 2-character chunks, which is the
|
||||
// calculated width of the hyperlink text.
|
||||
for (i, two_chars) in self
|
||||
.text
|
||||
.to_string()
|
||||
.chars()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let text = two_chars.collect::<String>();
|
||||
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
|
||||
buffer[(area.x + i as u16 * 2, area.y)].set_symbol(hyperlink.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue