modal
This commit is contained in:
parent
d989f6e31e
commit
cc4ecf40d7
6 changed files with 402 additions and 331 deletions
433
src/tui/mod.rs
433
src/tui/mod.rs
|
|
@ -1,5 +1,6 @@
|
|||
use itertools::Itertools;
|
||||
use ratatui_themes::{Theme, ThemeName};
|
||||
use regex::bytes::Regex;
|
||||
use std::{
|
||||
fs::{self, DirEntry},
|
||||
io,
|
||||
|
|
@ -11,12 +12,13 @@ use std::{
|
|||
use tui_widget_list::{ListBuilder, ListView};
|
||||
|
||||
use crate::tui::{
|
||||
filter::{FilterKind, WipMatcher},
|
||||
filter::FilterKind,
|
||||
log_viewer::{InputState, InputTarget, LogViewer},
|
||||
model::pretty_print_value,
|
||||
};
|
||||
use crate::tui::{
|
||||
filter::{FilterSelection, WipFilter},
|
||||
filter::{Filter, Matcher},
|
||||
log_viewer::FieldMatcher,
|
||||
reader::LogfileReader,
|
||||
};
|
||||
use ratatui::{
|
||||
|
|
@ -25,7 +27,7 @@ use ratatui::{
|
|||
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
||||
style::Style,
|
||||
text::{Line, Text},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{
|
||||
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
||||
},
|
||||
|
|
@ -37,6 +39,40 @@ pub mod model;
|
|||
pub mod processing;
|
||||
pub mod reader;
|
||||
|
||||
const HELP_TEXT: &str = "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
|
||||
t the selected log
|
||||
s surrounding element
|
||||
|
||||
either a field after `f` or the text of the current log:
|
||||
p ... with a prefix
|
||||
r ... matching a regex
|
||||
/ ... matching a regex
|
||||
e ... equal to selected
|
||||
c ... containing
|
||||
|
||||
───────────────────────────────────────────────────────
|
||||
perform action on selected target:
|
||||
|
||||
control-d delete
|
||||
control-i inline
|
||||
";
|
||||
|
||||
pub fn run(logs_dir: PathBuf, compiler_root: Option<PathBuf>, theme: ThemeName) {
|
||||
let terminal = ratatui::init();
|
||||
let theme = Theme::new(theme);
|
||||
|
|
@ -56,9 +92,6 @@ enum Tab {
|
|||
last_height: usize,
|
||||
},
|
||||
LogViewer(LogViewer),
|
||||
CreateFilter {
|
||||
filter: WipFilter,
|
||||
},
|
||||
Empty,
|
||||
Help,
|
||||
}
|
||||
|
|
@ -70,36 +103,11 @@ impl Tab {
|
|||
(Tab::FileChooser { .. }, _) => "choose a file".to_string(),
|
||||
(Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()),
|
||||
(Tab::LogViewer(_), None) => "logs".to_string(),
|
||||
(Tab::CreateFilter { .. }, _) => "create filter".to_string(),
|
||||
(Tab::Help, _) => "help".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter {
|
||||
todo!()
|
||||
// let matcher = if lv.fields_selected {
|
||||
// let footer_fields = lv.footer_fields();
|
||||
// let (key, value) = footer_fields
|
||||
// .get(lv.footer_list.selected.unwrap_or(0))
|
||||
// .map_or((None, None), |(k, v)| (Some(k), Some(v)));
|
||||
// Some(WipMatcher::Field {
|
||||
// name: key.cloned(),
|
||||
// value: value.cloned(),
|
||||
// })
|
||||
// } else {
|
||||
// Some(WipMatcher::Specific {
|
||||
// hash: lv.selected().map(|(i, _)| i.hash()),
|
||||
// })
|
||||
// };
|
||||
|
||||
// WipFilter {
|
||||
// matcher,
|
||||
// kind,
|
||||
// selection: filter::FilterSelection::Kind,
|
||||
// }
|
||||
}
|
||||
|
||||
struct App {
|
||||
tabs: Vec<Tab>,
|
||||
logs_dir: PathBuf,
|
||||
|
|
@ -204,6 +212,54 @@ impl App {
|
|||
_ => {}
|
||||
},
|
||||
Tab::LogViewer(lv) => match key.code {
|
||||
// delete
|
||||
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
if let InputState::None = lv.input_state {
|
||||
lv.input_state = InputState::Target(InputTarget::This);
|
||||
}
|
||||
|
||||
if let InputState::Target(t) = lv.input_state.clone()
|
||||
&& let Some(m) = Matcher::from_input(t, lv)
|
||||
{
|
||||
lv.add_filter(Rc::new(Filter {
|
||||
matcher: m,
|
||||
kind: FilterKind::Remove,
|
||||
}));
|
||||
lv.input_state.reset();
|
||||
}
|
||||
}
|
||||
// inline
|
||||
KeyCode::Char('i') if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
if let InputState::None = lv.input_state {
|
||||
lv.input_state = InputState::Target(InputTarget::This);
|
||||
}
|
||||
|
||||
if let InputState::Target(t) = lv.input_state.clone()
|
||||
&& let Some(m) = Matcher::from_input(t, lv)
|
||||
{
|
||||
lv.add_filter(Rc::new(Filter {
|
||||
matcher: m,
|
||||
kind: FilterKind::Inline,
|
||||
}));
|
||||
lv.input_state.reset();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if lv.input_state.captures_input() => {
|
||||
lv.input_state.capture_string().unwrap().push(c);
|
||||
}
|
||||
KeyCode::Backspace if lv.input_state.captures_input() => {
|
||||
lv.input_state.capture_string().unwrap().pop();
|
||||
}
|
||||
KeyCode::Esc if lv.input_state.captures_input() => {
|
||||
if let InputState::Target(InputTarget::Fields(s @ Some(_))) =
|
||||
&mut lv.input_state
|
||||
{
|
||||
*s = None
|
||||
} else {
|
||||
lv.input_state.reset();
|
||||
}
|
||||
}
|
||||
_ if lv.input_state.captures_input() => {}
|
||||
KeyCode::Char('j') | KeyCode::Down => lv.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => lv.prev(),
|
||||
KeyCode::PageUp => lv.page_up(),
|
||||
|
|
@ -214,11 +270,50 @@ impl App {
|
|||
KeyCode::Right => lv.enter(),
|
||||
KeyCode::Enter => lv.enter(),
|
||||
KeyCode::Char('f') => {
|
||||
lv.input_state.target(InputTarget::Fields);
|
||||
lv.input_state.target(InputTarget::Fields(None));
|
||||
lv.footer_list.select(Some(0));
|
||||
}
|
||||
KeyCode::Esc => lv.input_state.reset(),
|
||||
KeyCode::Char('s') if !lv.stack.is_empty() => {
|
||||
lv.input_state.target(InputTarget::Surround);
|
||||
}
|
||||
KeyCode::Char('t') => {
|
||||
lv.input_state.target(InputTarget::This);
|
||||
}
|
||||
KeyCode::Char('r') | KeyCode::Char('/') => {
|
||||
let v = FieldMatcher::Regex(String::new());
|
||||
if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state {
|
||||
*f = Some(v);
|
||||
} else {
|
||||
lv.input_state.target(InputTarget::Text(v));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let v = FieldMatcher::Prefix(String::new());
|
||||
if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state {
|
||||
*f = Some(v);
|
||||
} else {
|
||||
lv.input_state.target(InputTarget::Text(v));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
let v = FieldMatcher::Contains(String::new());
|
||||
if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state {
|
||||
*f = Some(v);
|
||||
} else {
|
||||
lv.input_state.target(InputTarget::Text(v));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
let v = FieldMatcher::EqualTo;
|
||||
if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state {
|
||||
*f = Some(v);
|
||||
} else {
|
||||
lv.input_state.target(InputTarget::Text(v));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Tab::CreateFilter { filter } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,11 +442,10 @@ impl Widget for &mut App {
|
|||
Tab::FileChooser { .. } => (false, true),
|
||||
Tab::LogViewer(lv) => {
|
||||
let target_fields =
|
||||
matches!(lv.input_state, InputState::Target(InputTarget::Fields));
|
||||
matches!(lv.input_state, InputState::Target(InputTarget::Fields(..)));
|
||||
(target_fields, !target_fields)
|
||||
}
|
||||
Tab::Empty => (false, false),
|
||||
Tab::CreateFilter { .. } => (false, false),
|
||||
};
|
||||
|
||||
let main_area = {
|
||||
|
|
@ -427,18 +521,123 @@ impl Widget for &mut App {
|
|||
.items(main_area.height as usize)
|
||||
.unwrap_or_else(|| (Vec::new(), 0));
|
||||
|
||||
Paragraph::new(selected_offset.to_string()).render(right, buf);
|
||||
Line::from(lv.input_state.show()).render(right, buf);
|
||||
|
||||
let list = List::new(items.into_iter().enumerate().map(
|
||||
|(idx, (i, inline_depth))| {
|
||||
let line = i.line_text(false, inline_depth);
|
||||
let line_text = i.line_text(false, inline_depth);
|
||||
|
||||
let mut list_item = ListItem::new(line);
|
||||
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(),
|
||||
),
|
||||
];
|
||||
|
||||
if idx == selected_offset {
|
||||
list_item = list_item.style(highlighted);
|
||||
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
|
||||
},
|
||||
));
|
||||
|
|
@ -504,159 +703,7 @@ impl Widget for &mut App {
|
|||
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 } => {
|
||||
Clear.render(popup_area, buf);
|
||||
let popup_area = {
|
||||
let block = Block::bordered()
|
||||
.title_top("create filter")
|
||||
.style(default)
|
||||
.padding(Padding::symmetric(3, 1))
|
||||
.border_style(border_selected);
|
||||
let inner = block.inner(popup_area);
|
||||
block.render(popup_area, buf);
|
||||
inner
|
||||
};
|
||||
|
||||
let [kind, matcher_kind, matcher_area, confirm] = Layout::vertical([
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(popup_area);
|
||||
|
||||
let text = match &filter.kind {
|
||||
None => "<unset>",
|
||||
Some(FilterKind::Inline) => "inline items",
|
||||
Some(FilterKind::Remove) => "remove item and sub-items",
|
||||
};
|
||||
Paragraph::new(format!("⏴ {text} ⏵"))
|
||||
.centered()
|
||||
.style(
|
||||
if matches!(filter.selection, filter::FilterSelection::Kind) {
|
||||
highlighted
|
||||
} else {
|
||||
default
|
||||
},
|
||||
)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title_top("transformation")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(kind, buf);
|
||||
|
||||
let text = match filter.matcher.as_ref() {
|
||||
None => "<unset>",
|
||||
Some(WipMatcher::Field { .. }) => "all logs where field matches",
|
||||
Some(WipMatcher::Specific { .. }) => "this specific log",
|
||||
};
|
||||
Paragraph::new(format!("⏴ {text} ⏵"))
|
||||
.centered()
|
||||
.style(
|
||||
if matches!(filter.selection, filter::FilterSelection::MatcherKind) {
|
||||
highlighted
|
||||
} else {
|
||||
default
|
||||
},
|
||||
)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title_top("matcher")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(matcher_kind, buf);
|
||||
|
||||
match &filter.matcher {
|
||||
Some(WipMatcher::Field { name, value }) => {
|
||||
let [field_area, value_area, _] = Layout::vertical([
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(matcher_area);
|
||||
|
||||
Paragraph::new(format!("{}", name.clone().unwrap_or_default()))
|
||||
.centered()
|
||||
.style(
|
||||
if matches!(filter.selection, filter::FilterSelection::Matcher)
|
||||
{
|
||||
highlighted
|
||||
} else {
|
||||
default
|
||||
},
|
||||
)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title_top("field name")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(field_area, buf);
|
||||
|
||||
Paragraph::new(format!("{}", value.clone().unwrap_or_default()))
|
||||
.centered()
|
||||
.style(
|
||||
if matches!(filter.selection, filter::FilterSelection::Matcher)
|
||||
{
|
||||
highlighted
|
||||
} else {
|
||||
default
|
||||
},
|
||||
)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title_top("value")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(value_area, buf);
|
||||
}
|
||||
Some(WipMatcher::Specific { .. }) => {}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Paragraph::new("confirm")
|
||||
.centered()
|
||||
.style(
|
||||
if matches!(filter.selection, filter::FilterSelection::Confirm) {
|
||||
highlighted
|
||||
} else {
|
||||
default
|
||||
},
|
||||
)
|
||||
.render(confirm, buf);
|
||||
Paragraph::new(HELP_TEXT).render(popup_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue