mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
add filtering
This commit is contained in:
parent
a664daa606
commit
ec19270ecd
9 changed files with 307 additions and 62 deletions
28
src/app.rs
28
src/app.rs
|
|
@ -2,7 +2,7 @@ use crate::logfile::LogFile;
|
||||||
use crate::logline::LogLine;
|
use crate::logline::LogLine;
|
||||||
use crate::matcher::MatchResult;
|
use crate::matcher::MatchResult;
|
||||||
use crate::timegraph::TimeGraph;
|
use crate::timegraph::TimeGraph;
|
||||||
use logsmash_data::StatementList;
|
use logsmash_data::{LoggingStatementWithPathPrefix, StatementList};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
pub struct App<'a> {
|
pub struct App<'a> {
|
||||||
|
|
@ -62,6 +62,24 @@ impl LogMatch {
|
||||||
pub fn row_count(&self) -> usize {
|
pub fn row_count(&self) -> usize {
|
||||||
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
|
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn statements<'a>(
|
||||||
|
&'a self,
|
||||||
|
app: &'a App,
|
||||||
|
) -> impl Iterator<Item = LoggingStatementWithPathPrefix> + 'a {
|
||||||
|
self.result
|
||||||
|
.iter()
|
||||||
|
.flat_map(|res| res.iter())
|
||||||
|
.filter_map(|index| app.log_statements.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, app: &App, filter: &str) -> bool {
|
||||||
|
if filter.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.statements(app)
|
||||||
|
.any(|statement| statement.pattern.contains(filter))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogMatch {
|
impl LogMatch {
|
||||||
|
|
@ -111,4 +129,12 @@ impl GroupedLines {
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.lines.len()
|
self.lines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, app: &App, filter: &str) -> bool {
|
||||||
|
if filter.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let line = &app.lines[self.lines[0]];
|
||||||
|
line.message.contains(filter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,10 @@ impl<R: Read + Seek> ArchiveEntry for ZipEntry<'_, R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Read + Seek> Archive for ZipArchive<R> {
|
impl<R: Read + Seek> Archive for ZipArchive<R> {
|
||||||
type Entry<'a> = ZipEntry<'a, R> where R: 'a;
|
type Entry<'a>
|
||||||
|
= ZipEntry<'a, R>
|
||||||
|
where
|
||||||
|
R: 'a;
|
||||||
|
|
||||||
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
|
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
|
||||||
let names = self
|
let names = self
|
||||||
|
|
@ -114,7 +117,10 @@ impl ArchiveEntry for TarEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Read> Archive for TarArchive<R> {
|
impl<R: Read> Archive for TarArchive<R> {
|
||||||
type Entry<'a> = TarEntry where R: 'a;
|
type Entry<'a>
|
||||||
|
= TarEntry
|
||||||
|
where
|
||||||
|
R: 'a;
|
||||||
|
|
||||||
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
|
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
|
||||||
match self.0.entries() {
|
match self.0.entries() {
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,14 @@ impl<'a> LogLine<'a> {
|
||||||
Cow::Borrowed(&self.message)
|
Cow::Borrowed(&self.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, filter: &str) -> bool {
|
||||||
|
if filter.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// todo: reqid, more?
|
||||||
|
self.app.contains(filter) || self.message.contains(filter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Hash)]
|
#[derive(Deserialize, Debug, Hash)]
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,18 @@ use ratatui::style::palette::tailwind;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Row, Table};
|
use ratatui::widgets::{Row, Table};
|
||||||
|
|
||||||
pub fn footer<'a>(app: &App<'a>, page: UiPage) -> Table<'a> {
|
pub enum FooterParams<'a> {
|
||||||
|
Normal(UiPage),
|
||||||
|
FilterInput(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> {
|
||||||
let footer_style = Style::default()
|
let footer_style = Style::default()
|
||||||
.bg(tailwind::BLACK)
|
.bg(tailwind::BLACK)
|
||||||
.fg(tailwind::GREEN.c600);
|
.fg(tailwind::GREEN.c600);
|
||||||
|
|
||||||
|
match params {
|
||||||
|
FooterParams::Normal(page) => {
|
||||||
let widths = [
|
let widths = [
|
||||||
Constraint::Percentage(100),
|
Constraint::Percentage(100),
|
||||||
Constraint::Min(25),
|
Constraint::Min(25),
|
||||||
|
|
@ -27,12 +34,30 @@ pub fn footer<'a>(app: &App<'a>, page: UiPage) -> Table<'a> {
|
||||||
)
|
)
|
||||||
.style(footer_style)
|
.style(footer_style)
|
||||||
}
|
}
|
||||||
|
FooterParams::FilterInput(filter_input) => {
|
||||||
|
let help = "«Esc» Clear - «Left» Back";
|
||||||
|
let widths = [
|
||||||
|
Constraint::Min(u16::try_from(help.chars().count()).unwrap()),
|
||||||
|
Constraint::Percentage(100),
|
||||||
|
];
|
||||||
|
|
||||||
|
Table::new(
|
||||||
|
[Row::new([
|
||||||
|
Text::from(help),
|
||||||
|
Text::from(format!("- Filter: {}█", filter_input)),
|
||||||
|
])],
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
.style(footer_style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn help(page: UiPage) -> &'static str {
|
fn help(page: UiPage) -> &'static str {
|
||||||
match page {
|
match page {
|
||||||
UiPage::MatchList => "«Q» Exit - «Enter» Select - «E» Show parse errors",
|
UiPage::MatchList => "«Q» Exit - «Enter» Select - «F4» Filter - «E» Show parse errors",
|
||||||
UiPage::Match => "«Q» Exit - «Enter» Select - «Esc» Back",
|
UiPage::Match => "«Q» Exit - «Enter» Select - «F4» Filter - «Esc» Back",
|
||||||
UiPage::Logs => "«Q» Exit - «Esc» Back - «C» Copy log line",
|
UiPage::Logs => "«Q» Exit - «F4» Filter - «Esc» Back - «C» Copy log line",
|
||||||
UiPage::Log => "«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line",
|
UiPage::Log => "«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line",
|
||||||
UiPage::Errors => "«Q» Exit - «Esc» Back - «C» Copy log line",
|
UiPage::Errors => "«Q» Exit - «Esc» Back - «C» Copy log line",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use ratatui::widgets::{Cell, Row};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::iter::{empty, once};
|
use std::iter::{empty, once};
|
||||||
|
|
||||||
pub fn match_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
|
pub fn match_list<'a>(app: &'a App<'a>, filter: &str) -> ScrollbarTable<'a> {
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Statement"),
|
Text::from("Statement"),
|
||||||
Text::from("File"),
|
Text::from("File"),
|
||||||
|
|
@ -38,7 +38,12 @@ pub fn match_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
|
||||||
|
|
||||||
ScrollbarTable::new(
|
ScrollbarTable::new(
|
||||||
once(all)
|
once(all)
|
||||||
.chain(app.matches.iter().map(|result| log_row(result, app, "")))
|
.chain(
|
||||||
|
app.matches
|
||||||
|
.iter()
|
||||||
|
.filter(|result| result.matches(app, filter))
|
||||||
|
.map(|result| log_row(result, app, "")),
|
||||||
|
)
|
||||||
.chain(unmatched),
|
.chain(unmatched),
|
||||||
widths,
|
widths,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::ui::raw_logs::raw_logs;
|
||||||
use crate::ui::single_log::single_log;
|
use crate::ui::single_log::single_log;
|
||||||
use crate::ui::single_match::grouped_lines;
|
use crate::ui::single_match::grouped_lines;
|
||||||
use crate::ui::state::{
|
use crate::ui::state::{
|
||||||
ErrorState, LogState, LogsState, MatchListState, MatchState, UiEvent, UiPage, UiState,
|
ErrorState, LogState, LogsState, MatchListState, MatchState, Mode, UiEvent, UiPage, UiState,
|
||||||
};
|
};
|
||||||
use ratatui::crossterm::event::{
|
use ratatui::crossterm::event::{
|
||||||
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
|
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
|
||||||
|
|
@ -84,22 +84,31 @@ fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEvent>
|
||||||
if event::poll(Duration::from_millis(50))? {
|
if event::poll(Duration::from_millis(50))? {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
|
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
|
||||||
return Ok(match key.code {
|
return Ok(match (ui_state.mode(), key.code) {
|
||||||
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
|
(_, KeyCode::Char('c')) if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
Some(UiEvent::Quit)
|
Some(UiEvent::Quit)
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => Some(UiEvent::Quit),
|
(Mode::Normal, KeyCode::Esc) => Some(UiEvent::Back),
|
||||||
KeyCode::Esc => Some(UiEvent::Back),
|
|
||||||
KeyCode::Char('e') if page == UiPage::MatchList => Some(UiEvent::Errors),
|
(Mode::Normal, KeyCode::Char('q')) => Some(UiEvent::Quit),
|
||||||
KeyCode::Left if page != UiPage::MatchList => Some(UiEvent::Back),
|
(Mode::Normal, KeyCode::Char('e')) if page == UiPage::MatchList => {
|
||||||
KeyCode::Down => Some(UiEvent::Down(1, true)),
|
Some(UiEvent::Errors)
|
||||||
KeyCode::Up => Some(UiEvent::Up(1, true)),
|
}
|
||||||
KeyCode::PageDown => Some(UiEvent::Down(10, false)),
|
(_, KeyCode::Left) if page != UiPage::MatchList => Some(UiEvent::Back),
|
||||||
KeyCode::PageUp => Some(UiEvent::Up(10, false)),
|
(_, KeyCode::Down) => Some(UiEvent::Down(1, true)),
|
||||||
KeyCode::End => Some(UiEvent::Down(usize::MAX, false)),
|
(_, KeyCode::Up) => Some(UiEvent::Up(1, true)),
|
||||||
KeyCode::Home => Some(UiEvent::Up(usize::MAX, false)),
|
(_, KeyCode::PageDown) => Some(UiEvent::Down(10, false)),
|
||||||
KeyCode::Enter | KeyCode::Right => Some(UiEvent::Select),
|
(_, KeyCode::PageUp) => Some(UiEvent::Up(10, false)),
|
||||||
KeyCode::Char('c') => Some(UiEvent::Copy),
|
(_, KeyCode::End) => Some(UiEvent::Down(usize::MAX, false)),
|
||||||
|
(_, KeyCode::Home) => Some(UiEvent::Up(usize::MAX, false)),
|
||||||
|
(_, KeyCode::Enter | KeyCode::Right) => Some(UiEvent::Select),
|
||||||
|
(Mode::Normal, KeyCode::Char('c')) => Some(UiEvent::Copy),
|
||||||
|
(Mode::Normal, KeyCode::F(4)) => Some(UiEvent::EnterFilterMode),
|
||||||
|
|
||||||
|
(Mode::FilterInput, KeyCode::Esc) => Some(UiEvent::ClearFilter),
|
||||||
|
(Mode::FilterInput, KeyCode::F(4)) => Some(UiEvent::Back),
|
||||||
|
(Mode::FilterInput, KeyCode::Backspace) => Some(UiEvent::Backspace),
|
||||||
|
(Mode::FilterInput, KeyCode::Char(c)) => Some(UiEvent::Text(c)),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +144,6 @@ fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> {
|
||||||
const UI_HEADER_SIZE: u16 = 5;
|
const UI_HEADER_SIZE: u16 = 5;
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
let page = state.page();
|
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(vec![
|
.constraints(vec![
|
||||||
|
|
@ -147,7 +155,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
UiState::Quit => {}
|
UiState::Quit => {}
|
||||||
UiState::MatchList(MatchListState { table_state, .. }) => {
|
UiState::MatchList(MatchListState {
|
||||||
|
table_state,
|
||||||
|
filter,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
let selected = table_state.selected();
|
let selected = table_state.selected();
|
||||||
let histogram = if selected == 0 {
|
let histogram = if selected == 0 {
|
||||||
&app.all.histogram
|
&app.all.histogram
|
||||||
|
|
@ -159,29 +171,37 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(UiHistogram::new(histogram), layout[0]);
|
frame.render_widget(UiHistogram::new(histogram), layout[0]);
|
||||||
frame.render_stateful_widget(match_list(app), layout[1], table_state);
|
frame.render_stateful_widget(match_list(app, filter), layout[1], table_state);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Match(MatchState {
|
UiState::Match(MatchState {
|
||||||
result,
|
result,
|
||||||
table_state,
|
table_state,
|
||||||
|
filter,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let selected_group = &result.grouped[table_state.selected()];
|
let selected_group = &result.grouped[table_state.selected()];
|
||||||
|
|
||||||
frame.render_widget(UiHistogram::new(&selected_group.histogram), layout[0]);
|
frame.render_widget(UiHistogram::new(&selected_group.histogram), layout[0]);
|
||||||
frame.render_stateful_widget(grouped_lines(app, result), layout[1], table_state);
|
frame.render_stateful_widget(
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
grouped_lines(app, result, filter),
|
||||||
|
layout[1],
|
||||||
|
table_state,
|
||||||
|
);
|
||||||
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Logs(LogsState {
|
UiState::Logs(LogsState {
|
||||||
lines, table_state, ..
|
lines,
|
||||||
|
table_state,
|
||||||
|
filter,
|
||||||
|
..
|
||||||
}) => {
|
}) => {
|
||||||
frame.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
raw_logs(app, lines),
|
raw_logs(app, lines, filter),
|
||||||
layout[0].union(layout[1]),
|
layout[0].union(layout[1]),
|
||||||
table_state,
|
table_state,
|
||||||
);
|
);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Log(LogState {
|
UiState::Log(LogState {
|
||||||
table_state,
|
table_state,
|
||||||
|
|
@ -193,11 +213,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
layout[0].union(layout[1]),
|
layout[0].union(layout[1]),
|
||||||
table_state,
|
table_state,
|
||||||
);
|
);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Errors(ErrorState { table_state, .. }) => {
|
UiState::Errors(ErrorState { table_state, .. }) => {
|
||||||
frame.render_stateful_widget(error_list(app), layout[0].union(layout[1]), table_state);
|
frame.render_stateful_widget(error_list(app), layout[0].union(layout[1]), table_state);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use ratatui::layout::{Alignment, Constraint};
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
|
||||||
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize]) -> ScrollbarTable<'a> {
|
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize], filter: &str) -> ScrollbarTable<'a> {
|
||||||
let lines = lines.iter().copied().map(|i| &app.lines[i]);
|
let lines = lines.iter().copied().map(|i| &app.lines[i]);
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Level"),
|
Text::from("Level"),
|
||||||
|
|
@ -26,7 +26,11 @@ pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize]) -> ScrollbarTable<'a> {
|
||||||
Constraint::Percentage(100),
|
Constraint::Percentage(100),
|
||||||
Constraint::Length(27),
|
Constraint::Length(27),
|
||||||
];
|
];
|
||||||
ScrollbarTable::new(lines.map(log_row), widths).header(header)
|
ScrollbarTable::new(
|
||||||
|
lines.filter(|line| line.matches(filter)).map(log_row),
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
.header(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {
|
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@ use ratatui::layout::Constraint;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
|
||||||
pub fn grouped_lines<'a>(app: &'a App<'a>, log_match: &'a LogMatch) -> ScrollbarTable<'a> {
|
pub fn grouped_lines<'a>(
|
||||||
|
app: &'a App<'a>,
|
||||||
|
log_match: &'a LogMatch,
|
||||||
|
filter: &str,
|
||||||
|
) -> ScrollbarTable<'a> {
|
||||||
let grouped = &log_match.grouped;
|
let grouped = &log_match.grouped;
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Level"),
|
Text::from("Level"),
|
||||||
|
|
@ -27,7 +31,14 @@ pub fn grouped_lines<'a>(app: &'a App<'a>, log_match: &'a LogMatch) -> Scrollbar
|
||||||
Constraint::Length(10),
|
Constraint::Length(10),
|
||||||
Constraint::Min(10),
|
Constraint::Min(10),
|
||||||
];
|
];
|
||||||
ScrollbarTable::new(grouped.iter().map(|group| group_row(app, group)), widths).header(header)
|
ScrollbarTable::new(
|
||||||
|
grouped
|
||||||
|
.iter()
|
||||||
|
.filter(|group| group.matches(app, filter))
|
||||||
|
.map(|group| group_row(app, group)),
|
||||||
|
widths,
|
||||||
|
)
|
||||||
|
.header(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_row<'a>(app: &'a App, group: &'a GroupedLines) -> Row<'a> {
|
fn group_row<'a>(app: &'a App, group: &'a GroupedLines) -> Row<'a> {
|
||||||
|
|
|
||||||
156
src/ui/state.rs
156
src/ui/state.rs
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::app::{App, LogMatch};
|
use crate::app::{App, LogMatch};
|
||||||
use crate::logline::{FullLogLine, LogLine};
|
use crate::logline::{FullLogLine, LogLine};
|
||||||
|
use crate::ui::footer::FooterParams;
|
||||||
use crate::ui::table::ScrollbarTableState;
|
use crate::ui::table::ScrollbarTableState;
|
||||||
use crate::ui::UI_HEADER_SIZE;
|
use crate::ui::UI_HEADER_SIZE;
|
||||||
use crate::{copy_osc, parse_line_full};
|
use crate::{copy_osc, parse_line_full};
|
||||||
|
|
@ -17,10 +18,18 @@ pub enum UiState<'a> {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Mode {
|
||||||
|
Normal,
|
||||||
|
FilterInput,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MatchListState<'a> {
|
pub struct MatchListState<'a> {
|
||||||
app: &'a App<'a>,
|
app: &'a App<'a>,
|
||||||
pub table_state: ScrollbarTableState,
|
pub table_state: ScrollbarTableState,
|
||||||
|
pub filter: String,
|
||||||
|
mode: Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MatchListState<'a> {
|
impl<'a> MatchListState<'a> {
|
||||||
|
|
@ -32,7 +41,15 @@ impl<'a> MatchListState<'a> {
|
||||||
let result = if selected == 0 {
|
let result = if selected == 0 {
|
||||||
&app.all
|
&app.all
|
||||||
} else if selected <= app.matches.len() {
|
} else if selected <= app.matches.len() {
|
||||||
|
if self.filter.is_empty() {
|
||||||
&app.matches[selected - 1]
|
&app.matches[selected - 1]
|
||||||
|
} else {
|
||||||
|
app.matches
|
||||||
|
.iter()
|
||||||
|
.filter(|log_match| log_match.matches(app, &self.filter))
|
||||||
|
.nth(selected - 1)
|
||||||
|
.expect("filtered select out of bounds")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
&app.unmatched
|
&app.unmatched
|
||||||
};
|
};
|
||||||
|
|
@ -41,6 +58,8 @@ impl<'a> MatchListState<'a> {
|
||||||
result,
|
result,
|
||||||
table_state,
|
table_state,
|
||||||
previous: Box::new(self.into()),
|
previous: Box::new(self.into()),
|
||||||
|
filter: String::new(),
|
||||||
|
mode: Mode::Normal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +75,8 @@ pub struct MatchState<'a> {
|
||||||
pub result: &'a LogMatch,
|
pub result: &'a LogMatch,
|
||||||
pub table_state: ScrollbarTableState,
|
pub table_state: ScrollbarTableState,
|
||||||
pub previous: Box<UiState<'a>>,
|
pub previous: Box<UiState<'a>>,
|
||||||
|
pub filter: String,
|
||||||
|
mode: Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MatchState<'a> {
|
impl<'a> MatchState<'a> {
|
||||||
|
|
@ -63,16 +84,28 @@ impl<'a> MatchState<'a> {
|
||||||
self.table_state.selected()
|
self.table_state.selected()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter(self, selected: usize) -> UiState<'a> {
|
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
|
||||||
let mut table_state = TableState::default();
|
let mut table_state = TableState::default();
|
||||||
table_state.select(Some(0));
|
table_state.select(Some(0));
|
||||||
|
|
||||||
let lines = self.result.grouped[selected].lines.as_slice();
|
let selected_line = if self.filter.is_empty() {
|
||||||
|
&self.result.grouped[selected]
|
||||||
|
} else {
|
||||||
|
self.result
|
||||||
|
.grouped
|
||||||
|
.iter()
|
||||||
|
.filter(|grouped| grouped.matches(app, &self.filter))
|
||||||
|
.nth(selected)
|
||||||
|
.expect("filtered select out of bounds")
|
||||||
|
};
|
||||||
|
let lines = selected_line.lines.as_slice();
|
||||||
let table_state = ScrollbarTableState::new(lines.len());
|
let table_state = ScrollbarTableState::new(lines.len());
|
||||||
UiState::Logs(LogsState {
|
UiState::Logs(LogsState {
|
||||||
lines,
|
lines,
|
||||||
table_state,
|
table_state,
|
||||||
previous: Box::new(self.into()),
|
previous: Box::new(self.into()),
|
||||||
|
filter: String::new(),
|
||||||
|
mode: Mode::Normal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +121,8 @@ pub struct LogsState<'a> {
|
||||||
pub lines: &'a [usize],
|
pub lines: &'a [usize],
|
||||||
pub table_state: ScrollbarTableState,
|
pub table_state: ScrollbarTableState,
|
||||||
pub previous: Box<UiState<'a>>,
|
pub previous: Box<UiState<'a>>,
|
||||||
|
pub filter: String,
|
||||||
|
mode: Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LogsState<'a> {
|
impl<'a> LogsState<'a> {
|
||||||
|
|
@ -96,7 +131,17 @@ impl<'a> LogsState<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
|
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
|
||||||
let line = self.lines[selected];
|
let line = if self.filter.is_empty() {
|
||||||
|
self.lines[selected]
|
||||||
|
} else {
|
||||||
|
self.lines
|
||||||
|
.iter()
|
||||||
|
.map(|index| &app.lines[*index])
|
||||||
|
.filter(|line| line.matches(&self.filter))
|
||||||
|
.nth(selected)
|
||||||
|
.map(|line| line.index)
|
||||||
|
.expect("filtered select out of bounds")
|
||||||
|
};
|
||||||
let log = &app.lines[line];
|
let log = &app.lines[line];
|
||||||
let raw_line = app.get_line(log.index).unwrap();
|
let raw_line = app.get_line(log.index).unwrap();
|
||||||
let full_line = parse_line_full(raw_line).unwrap();
|
let full_line = parse_line_full(raw_line).unwrap();
|
||||||
|
|
@ -155,6 +200,8 @@ impl<'a> UiState<'a> {
|
||||||
UiState::MatchList(MatchListState {
|
UiState::MatchList(MatchListState {
|
||||||
app,
|
app,
|
||||||
table_state: ScrollbarTableState::new(app.match_lines()),
|
table_state: ScrollbarTableState::new(app.match_lines()),
|
||||||
|
filter: String::new(),
|
||||||
|
mode: Mode::Normal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +215,42 @@ impl<'a> UiState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mode(&self) -> Mode {
|
||||||
|
match self {
|
||||||
|
UiState::MatchList(state) => state.mode,
|
||||||
|
UiState::Match(state) => state.mode,
|
||||||
|
UiState::Logs(state) => state.mode,
|
||||||
|
_ => Mode::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(&mut self, mode: Mode) {
|
||||||
|
match self {
|
||||||
|
UiState::MatchList(state) => state.mode = mode,
|
||||||
|
UiState::Match(state) => state.mode = mode,
|
||||||
|
UiState::Logs(state) => state.mode = mode,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
UiState::MatchList(state) => Some(&state.filter),
|
||||||
|
UiState::Match(state) => Some(&state.filter),
|
||||||
|
UiState::Logs(state) => Some(&state.filter),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter_mut(&mut self) -> Option<&mut String> {
|
||||||
|
match self {
|
||||||
|
UiState::MatchList(state) => Some(&mut state.filter),
|
||||||
|
UiState::Match(state) => Some(&mut state.filter),
|
||||||
|
UiState::Logs(state) => Some(&mut state.filter),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn table_state(&self) -> Option<&ScrollbarTableState> {
|
fn table_state(&self) -> Option<&ScrollbarTableState> {
|
||||||
match self {
|
match self {
|
||||||
UiState::MatchList(state) => Some(&state.table_state),
|
UiState::MatchList(state) => Some(&state.table_state),
|
||||||
|
|
@ -208,9 +291,13 @@ impl<'a> UiState<'a> {
|
||||||
|
|
||||||
pub fn index_for_row(&self, row: usize) -> usize {
|
pub fn index_for_row(&self, row: usize) -> usize {
|
||||||
match self {
|
match self {
|
||||||
UiState::MatchList(MatchListState { app, .. }) => {
|
UiState::MatchList(MatchListState { app, filter, .. }) => {
|
||||||
let mut total_height = 0;
|
let mut total_height = 0;
|
||||||
let match_row_counts = app.matches.iter().map(|m| m.row_count());
|
let match_row_counts = app
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.matches(app, filter))
|
||||||
|
.map(|m| m.row_count());
|
||||||
for (index, row_count) in once(1)
|
for (index, row_count) in once(1)
|
||||||
.chain(match_row_counts)
|
.chain(match_row_counts)
|
||||||
.chain(once(1))
|
.chain(once(1))
|
||||||
|
|
@ -248,7 +335,12 @@ impl<'a> UiState<'a> {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(UiState::Quit, _) => (true, UiState::Quit),
|
(UiState::Quit, _) => (true, UiState::Quit),
|
||||||
(_, UiEvent::Quit) => (true, UiState::Quit),
|
(_, UiEvent::Quit) => (true, UiState::Quit),
|
||||||
(UiState::MatchList(_), UiEvent::Back) => (true, UiState::Quit),
|
(
|
||||||
|
UiState::MatchList(MatchListState {
|
||||||
|
mode: Mode::Normal, ..
|
||||||
|
}),
|
||||||
|
UiEvent::Back,
|
||||||
|
) => (true, UiState::Quit),
|
||||||
(mut state, UiEvent::Down(step, rollover)) => {
|
(mut state, UiEvent::Down(step, rollover)) => {
|
||||||
if let Some(table_state) = state.table_state_mut() {
|
if let Some(table_state) = state.table_state_mut() {
|
||||||
table_state.down(step, rollover);
|
table_state.down(step, rollover);
|
||||||
|
|
@ -292,9 +384,9 @@ impl<'a> UiState<'a> {
|
||||||
}
|
}
|
||||||
(UiState::Match(state), UiEvent::Select) => {
|
(UiState::Match(state), UiEvent::Select) => {
|
||||||
let selected = state.selected();
|
let selected = state.selected();
|
||||||
(true, state.enter(selected))
|
(true, state.enter(selected, app))
|
||||||
}
|
}
|
||||||
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
|
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)),
|
||||||
(UiState::Logs(state), UiEvent::Select) => {
|
(UiState::Logs(state), UiEvent::Select) => {
|
||||||
let selected = state.selected();
|
let selected = state.selected();
|
||||||
(true, state.enter(selected, app))
|
(true, state.enter(selected, app))
|
||||||
|
|
@ -324,6 +416,43 @@ impl<'a> UiState<'a> {
|
||||||
copy_osc(raw);
|
copy_osc(raw);
|
||||||
(false, UiState::Errors(state))
|
(false, UiState::Errors(state))
|
||||||
}
|
}
|
||||||
|
(mut ui, UiEvent::EnterFilterMode) if ui.mode() != Mode::FilterInput => {
|
||||||
|
ui.set_mode(Mode::FilterInput);
|
||||||
|
(true, ui)
|
||||||
|
}
|
||||||
|
(mut ui, UiEvent::Text(c)) if ui.mode() == Mode::FilterInput => {
|
||||||
|
if let Some(filter) = ui.filter_mut() {
|
||||||
|
filter.push(c);
|
||||||
|
}
|
||||||
|
(true, ui)
|
||||||
|
}
|
||||||
|
(mut ui, UiEvent::Backspace) if ui.mode() == Mode::FilterInput => {
|
||||||
|
if let Some(filter) = ui.filter_mut() {
|
||||||
|
filter.pop();
|
||||||
|
}
|
||||||
|
(true, ui)
|
||||||
|
}
|
||||||
|
(mut ui, UiEvent::ClearFilter) if ui.mode() != Mode::Normal => {
|
||||||
|
if let Some(filter) = ui.filter_mut() {
|
||||||
|
filter.clear();
|
||||||
|
}
|
||||||
|
ui.set_mode(Mode::Normal);
|
||||||
|
(true, ui)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
mut ui @ UiState::MatchList(MatchListState {
|
||||||
|
mode: Mode::FilterInput,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
UiEvent::Back,
|
||||||
|
) => {
|
||||||
|
if let Some(filter) = ui.filter_mut() {
|
||||||
|
filter.clear();
|
||||||
|
}
|
||||||
|
ui.set_mode(Mode::Normal);
|
||||||
|
(true, ui)
|
||||||
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
UiState::Match(MatchState { previous, .. })
|
UiState::Match(MatchState { previous, .. })
|
||||||
| UiState::Logs(LogsState { previous, .. })
|
| UiState::Logs(LogsState { previous, .. })
|
||||||
|
|
@ -334,6 +463,13 @@ impl<'a> UiState<'a> {
|
||||||
(state, _) => (false, state),
|
(state, _) => (false, state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn footer_params(&self) -> FooterParams {
|
||||||
|
match self.mode() {
|
||||||
|
Mode::Normal => FooterParams::Normal(self.page()),
|
||||||
|
Mode::FilterInput => FooterParams::FilterInput(self.filter().unwrap_or_default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum UiEvent {
|
pub enum UiEvent {
|
||||||
|
|
@ -348,6 +484,10 @@ pub enum UiEvent {
|
||||||
SelectAt(usize),
|
SelectAt(usize),
|
||||||
Enter(usize),
|
Enter(usize),
|
||||||
Copy,
|
Copy,
|
||||||
|
EnterFilterMode,
|
||||||
|
ClearFilter,
|
||||||
|
Text(char),
|
||||||
|
Backspace,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue