undo/redo
This commit is contained in:
parent
8a4df3307d
commit
8cfe1a0b65
8 changed files with 247 additions and 154 deletions
|
|
@ -1,13 +1,34 @@
|
|||
use regex::bytes::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::tui::{
|
||||
log_viewer::{FieldMatcher, InputTarget, LogViewer},
|
||||
log_viewer::{
|
||||
LogViewer,
|
||||
input::{FieldMatcher, InputTarget},
|
||||
},
|
||||
model::LogEntry,
|
||||
};
|
||||
|
||||
mod serialize_regex {
|
||||
use regex::bytes::Regex;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
|
||||
|
||||
pub fn serialize<S: Serializer>(r: &Regex, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
r.as_str().serialize(serializer)
|
||||
}
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Regex::new(&s).map_err(|e| D::Error::custom(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum MatcherValue {
|
||||
Exact(String),
|
||||
Regex(Regex),
|
||||
Regex(#[serde(with = "serialize_regex")] Regex),
|
||||
Prefix(String),
|
||||
Contains(String),
|
||||
}
|
||||
|
|
@ -32,6 +53,7 @@ impl MatcherValue {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Matcher {
|
||||
Field { name: String, value: MatcherValue },
|
||||
Message { value: MatcherValue },
|
||||
|
|
@ -76,12 +98,13 @@ impl Matcher {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum FilterKind {
|
||||
Inline,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Filter {
|
||||
pub matcher: Matcher,
|
||||
pub kind: FilterKind,
|
||||
|
|
|
|||
41
src/tui/log_viewer/filters.rs
Normal file
41
src/tui/log_viewer/filters.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::tui::filter::Filter;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Filters {
|
||||
filters: Vec<Rc<Filter>>,
|
||||
undo_pos: usize,
|
||||
}
|
||||
|
||||
impl Filters {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
filters: Vec::new(),
|
||||
undo_pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> &[Rc<Filter>] {
|
||||
&self.filters[0..self.undo_pos]
|
||||
}
|
||||
|
||||
pub fn push(&mut self, filter: Rc<Filter>) {
|
||||
self.filters.truncate(self.undo_pos);
|
||||
self.filters.push(filter);
|
||||
self.undo_pos = self.filters.len();
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
self.undo_pos += 1;
|
||||
if self.undo_pos > self.filters.len() {
|
||||
self.undo_pos = self.filters.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) {
|
||||
self.undo_pos = self.undo_pos.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
109
src/tui/log_viewer/input.rs
Normal file
109
src/tui/log_viewer/input.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use std::mem;
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
|
||||
use crate::tui::widgets::styled::Styled;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum FieldMatcher {
|
||||
EqualTo,
|
||||
Prefix(String),
|
||||
Regex(String),
|
||||
Contains(String),
|
||||
}
|
||||
|
||||
impl FieldMatcher {
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
FieldMatcher::EqualTo => "equal to selected value".to_string(),
|
||||
Self::Prefix(s) => format!("with a prefix of `{s}`"),
|
||||
Self::Regex(s) => format!("matching /{s}/"),
|
||||
Self::Contains(s) => format!("containing `{s}`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InputTarget {
|
||||
Fields(Option<FieldMatcher>),
|
||||
Text(FieldMatcher),
|
||||
This,
|
||||
Surround,
|
||||
}
|
||||
|
||||
impl InputTarget {
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
Self::Fields(None) => "logs with a field...".to_string(),
|
||||
Self::Fields(Some(fm)) => format!("logs with the selected field {}", fm.show()),
|
||||
Self::Text(fm) => format!("logs {}", fm.show()),
|
||||
Self::This => format!("this log"),
|
||||
Self::Surround => format!("the log surrounding the current view"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InputState {
|
||||
None,
|
||||
Target(InputTarget),
|
||||
}
|
||||
|
||||
impl Widget for Styled<'_, &InputState> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Line::from(self.inner.show())
|
||||
.style(self.styles.default)
|
||||
.render(area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn capture_string(&mut self) -> Option<&mut String> {
|
||||
match self {
|
||||
InputState::None => None,
|
||||
InputState::Target(InputTarget::This) => None,
|
||||
InputState::Target(InputTarget::Fields(None)) => None,
|
||||
InputState::Target(InputTarget::Surround) => None,
|
||||
|
||||
// require arbitrary text input
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Contains(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Contains(s))) => Some(s),
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Prefix(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Prefix(s))) => Some(s),
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Regex(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Regex(s))) => Some(s),
|
||||
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))) => None,
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::EqualTo)) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn captures_input(&mut self) -> bool {
|
||||
self.capture_string().is_some()
|
||||
}
|
||||
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
InputState::None => "".to_string(),
|
||||
InputState::Target(input_target) => input_target.show(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::None;
|
||||
}
|
||||
|
||||
pub fn target(&mut self, target: InputTarget) {
|
||||
if let Self::Target(t) = self
|
||||
&& mem::discriminant(t) == mem::discriminant(&target)
|
||||
&& !self.captures_input()
|
||||
{
|
||||
self.reset();
|
||||
} else {
|
||||
*self = Self::Target(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,147 +2,24 @@ use std::{collections::HashMap, iter, mem, rc::Rc};
|
|||
|
||||
use crate::tui::{
|
||||
filter::Filter,
|
||||
log_viewer::{
|
||||
filters::Filters,
|
||||
input::{FieldMatcher, InputState, InputTarget},
|
||||
view::LogView,
|
||||
},
|
||||
model::LogEntry,
|
||||
processing::{IntoLogStream, LogStream},
|
||||
widgets::styled::Styled,
|
||||
};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
use tui_widget_list::ListState;
|
||||
|
||||
pub struct LogView {
|
||||
iter: Box<dyn LogStream>,
|
||||
selection_offset: usize,
|
||||
}
|
||||
|
||||
impl LogView {
|
||||
pub fn selected(&self) -> Option<(Rc<LogEntry>, usize)> {
|
||||
let mut temp_iter = self.iter.clone();
|
||||
for _ in 0..self.selection_offset {
|
||||
let _ = temp_iter.next()?;
|
||||
}
|
||||
|
||||
temp_iter.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LogView {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
iter: self.iter.clone(),
|
||||
selection_offset: self.selection_offset.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum FieldMatcher {
|
||||
EqualTo,
|
||||
Prefix(String),
|
||||
Regex(String),
|
||||
Contains(String),
|
||||
}
|
||||
|
||||
impl FieldMatcher {
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
FieldMatcher::EqualTo => "equal to selected value".to_string(),
|
||||
Self::Prefix(s) => format!("with a prefix of `{s}`"),
|
||||
Self::Regex(s) => format!("matching /{s}/"),
|
||||
Self::Contains(s) => format!("containing `{s}`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InputTarget {
|
||||
Fields(Option<FieldMatcher>),
|
||||
Text(FieldMatcher),
|
||||
This,
|
||||
Surround,
|
||||
}
|
||||
|
||||
impl InputTarget {
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
Self::Fields(None) => "logs with a field...".to_string(),
|
||||
Self::Fields(Some(fm)) => format!("logs with the selected field {}", fm.show()),
|
||||
Self::Text(fm) => format!("logs {}", fm.show()),
|
||||
Self::This => format!("this log"),
|
||||
Self::Surround => format!("the log surrounding the current view"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InputState {
|
||||
None,
|
||||
Target(InputTarget),
|
||||
}
|
||||
|
||||
impl Widget for Styled<'_, &InputState> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Line::from(self.inner.show())
|
||||
.style(self.styles.default)
|
||||
.render(area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn capture_string(&mut self) -> Option<&mut String> {
|
||||
match self {
|
||||
InputState::None => None,
|
||||
InputState::Target(InputTarget::This) => None,
|
||||
InputState::Target(InputTarget::Fields(None)) => None,
|
||||
InputState::Target(InputTarget::Surround) => None,
|
||||
|
||||
// require arbitrary text input
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Contains(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Contains(s))) => Some(s),
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Prefix(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Prefix(s))) => Some(s),
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::Regex(s)))) => Some(s),
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::Regex(s))) => Some(s),
|
||||
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))) => None,
|
||||
InputState::Target(InputTarget::Text(FieldMatcher::EqualTo)) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn captures_input(&mut self) -> bool {
|
||||
self.capture_string().is_some()
|
||||
}
|
||||
|
||||
pub fn show(&self) -> String {
|
||||
match self {
|
||||
InputState::None => "".to_string(),
|
||||
InputState::Target(input_target) => input_target.show(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::None;
|
||||
}
|
||||
|
||||
pub fn target(&mut self, target: InputTarget) {
|
||||
if let Self::Target(t) = self
|
||||
&& mem::discriminant(t) == mem::discriminant(&target)
|
||||
&& !self.captures_input()
|
||||
{
|
||||
self.reset();
|
||||
} else {
|
||||
*self = Self::Target(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod filters;
|
||||
pub mod input;
|
||||
pub mod view;
|
||||
|
||||
pub struct LogViewer {
|
||||
pub stack: Vec<LogView>,
|
||||
curr: LogView,
|
||||
cache: HashMap<Vec<usize>, LogView>,
|
||||
filters: Vec<Rc<Filter>>,
|
||||
|
||||
pub root_stream: Box<dyn LogStream>,
|
||||
|
||||
|
|
@ -152,7 +29,7 @@ pub struct LogViewer {
|
|||
pub last_fields_height: usize,
|
||||
|
||||
pub footer_list: ListState,
|
||||
|
||||
filters: Filters,
|
||||
pub input_state: InputState,
|
||||
}
|
||||
|
||||
|
|
@ -173,22 +50,21 @@ impl LogViewer {
|
|||
last_fields_offset: 0,
|
||||
last_fields_height: 0,
|
||||
|
||||
filters: Vec::new(),
|
||||
filters: Filters::new(),
|
||||
input_state: InputState::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filtered_root_stream(&self) -> Box<dyn LogStream> {
|
||||
let mut curr = self.root_stream.clone();
|
||||
for filter in &self.filters {
|
||||
for filter in self.filters.get() {
|
||||
curr = Box::new(curr.filter(Rc::clone(filter)));
|
||||
}
|
||||
|
||||
curr
|
||||
}
|
||||
|
||||
pub fn add_filter(&mut self, filter: Rc<Filter>) {
|
||||
self.filters.push(Rc::clone(&filter));
|
||||
fn update_filters(&mut self) {
|
||||
self.cache.clear();
|
||||
let offsets_list: Vec<_> = self
|
||||
.stack
|
||||
|
|
@ -230,9 +106,9 @@ impl LogViewer {
|
|||
|
||||
// If the value we're looking for is removed by the filter,
|
||||
// we'll have a hard time finding it so quit
|
||||
if filter.removes(&elem) {
|
||||
break;
|
||||
}
|
||||
// if filter.removes(&elem) {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// find the nearest stream in which this element can be found
|
||||
let mut curr = current_stream.as_ref();
|
||||
|
|
@ -286,6 +162,11 @@ impl LogViewer {
|
|||
self.stack = new_stack;
|
||||
}
|
||||
|
||||
pub fn add_filter(&mut self, filter: Rc<Filter>) {
|
||||
self.filters.push(Rc::clone(&filter));
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
pub fn update_num_items(&mut self, num_visible_items: usize) {
|
||||
while self.curr.selection_offset >= num_visible_items {
|
||||
if self.curr.selection_offset == 0 {
|
||||
|
|
@ -436,6 +317,16 @@ impl LogViewer {
|
|||
self.input_state.reset();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) {
|
||||
self.filters.undo();
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
self.filters.redo();
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
pub fn enter(&mut self) {
|
||||
match self.input_state {
|
||||
InputState::None => {
|
||||
28
src/tui/log_viewer/view.rs
Normal file
28
src/tui/log_viewer/view.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::tui::{model::LogEntry, processing::LogStream};
|
||||
|
||||
pub struct LogView {
|
||||
pub iter: Box<dyn LogStream>,
|
||||
pub selection_offset: usize,
|
||||
}
|
||||
|
||||
impl LogView {
|
||||
pub fn selected(&self) -> Option<(Rc<LogEntry>, usize)> {
|
||||
let mut temp_iter = self.iter.clone();
|
||||
for _ in 0..self.selection_offset {
|
||||
let _ = temp_iter.next()?;
|
||||
}
|
||||
|
||||
temp_iter.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LogView {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
iter: self.iter.clone(),
|
||||
selection_offset: self.selection_offset.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,15 +14,13 @@ use std::{
|
|||
use tui_widget_list::{ListBuilder, ListView};
|
||||
|
||||
use crate::tui::{
|
||||
filter::FilterKind,
|
||||
log_viewer::{InputState, InputTarget, LogViewer},
|
||||
widgets::{hyperlink::Hyperlink, items::Items, last_error::LastError},
|
||||
};
|
||||
use crate::tui::{
|
||||
filter::{Filter, Matcher},
|
||||
log_viewer::FieldMatcher,
|
||||
filter::{Filter, FilterKind, Matcher},
|
||||
log_viewer::{
|
||||
LogViewer,
|
||||
input::{FieldMatcher, InputState, InputTarget},
|
||||
},
|
||||
reader::LogfileReader,
|
||||
widgets::styled::IntoStyled,
|
||||
widgets::{hyperlink::Hyperlink, items::Items, last_error::LastError, styled::IntoStyled},
|
||||
};
|
||||
use ratatui::{
|
||||
DefaultTerminal, Terminal,
|
||||
|
|
@ -67,7 +65,6 @@ targeting logs:
|
|||
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
|
||||
|
||||
|
|
@ -298,6 +295,10 @@ impl App {
|
|||
KeyCode::Backspace | KeyCode::Left => lv.back(),
|
||||
KeyCode::Right => lv.enter(),
|
||||
KeyCode::Enter => lv.enter(),
|
||||
|
||||
KeyCode::Char('u') => lv.undo(),
|
||||
KeyCode::Char('r') => lv.redo(),
|
||||
|
||||
KeyCode::Char('f') => {
|
||||
lv.input_state.target(InputTarget::Fields(None));
|
||||
lv.footer_list.select(Some(0));
|
||||
|
|
@ -309,7 +310,7 @@ impl App {
|
|||
KeyCode::Char('t') => {
|
||||
lv.input_state.target(InputTarget::This);
|
||||
}
|
||||
KeyCode::Char('r') | KeyCode::Char('/') => {
|
||||
KeyCode::Char('/') => {
|
||||
let v = FieldMatcher::Regex(String::new());
|
||||
if let InputState::Target(InputTarget::Fields(f @ None)) = &mut lv.input_state {
|
||||
*f = Some(v);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use ratatui::widgets::{List, ListItem, Widget};
|
|||
use regex::Regex;
|
||||
|
||||
use crate::tui::{
|
||||
log_viewer::{FieldMatcher, InputState, InputTarget},
|
||||
log_viewer::input::{FieldMatcher, InputState, InputTarget},
|
||||
model::LogEntry,
|
||||
widgets::{
|
||||
last_error::LastError,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue