undo/redo
This commit is contained in:
parent
8a4df3307d
commit
8cfe1a0b65
8 changed files with 247 additions and 154 deletions
|
|
@ -1,474 +0,0 @@
|
|||
use std::{collections::HashMap, iter, mem, rc::Rc};
|
||||
|
||||
use crate::tui::{
|
||||
filter::Filter,
|
||||
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 struct LogViewer {
|
||||
pub stack: Vec<LogView>,
|
||||
curr: LogView,
|
||||
cache: HashMap<Vec<usize>, LogView>,
|
||||
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,
|
||||
}
|
||||
|
||||
impl LogViewer {
|
||||
pub fn new(stream: impl LogStream) -> Self {
|
||||
Self {
|
||||
stack: Vec::new(),
|
||||
curr: LogView {
|
||||
iter: stream.clone(),
|
||||
selection_offset: 0,
|
||||
},
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filtered_root_stream(&self) -> Box<dyn LogStream> {
|
||||
let mut curr = self.root_stream.clone();
|
||||
for filter in &self.filters {
|
||||
curr = Box::new(curr.filter(Rc::clone(filter)));
|
||||
}
|
||||
|
||||
curr
|
||||
}
|
||||
|
||||
pub fn add_filter(&mut self, filter: Rc<Filter>) {
|
||||
self.filters.push(Rc::clone(&filter));
|
||||
self.cache.clear();
|
||||
let offsets_list: Vec<_> = self
|
||||
.stack
|
||||
.iter()
|
||||
.map(|i| (i.selected(), i.selection_offset))
|
||||
.chain(iter::once((
|
||||
self.curr.selected(),
|
||||
self.curr.selection_offset,
|
||||
)))
|
||||
.collect();
|
||||
|
||||
fn find_elem_in_stream(
|
||||
stream: &dyn LogStream,
|
||||
elem: &Rc<LogEntry>,
|
||||
) -> Option<Box<dyn LogStream>> {
|
||||
let mut temp_stream = stream.clone();
|
||||
let mut max = 100usize;
|
||||
while let Some((curr, _)) = temp_stream.next() {
|
||||
if Rc::ptr_eq(&curr, elem) {
|
||||
return Some(temp_stream);
|
||||
}
|
||||
|
||||
if max == 0 {
|
||||
break;
|
||||
}
|
||||
max -= 1;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
let mut current_stream = self.filtered_root_stream();
|
||||
let mut new_stack = Vec::<LogView>::new();
|
||||
|
||||
'outer: for (elem, old_offset) in offsets_list {
|
||||
let Some((elem, _)) = elem else {
|
||||
break;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// find the nearest stream in which this element can be found
|
||||
let mut curr = current_stream.as_ref();
|
||||
let mut parents = new_stack.iter().rev();
|
||||
let mut found_in_toplevel = true;
|
||||
let mut stream = loop {
|
||||
if let Some(stream) = find_elem_in_stream(curr, &elem) {
|
||||
break stream;
|
||||
}
|
||||
found_in_toplevel = false;
|
||||
|
||||
if let Some(parent) = parents.next() {
|
||||
curr = parent.iter.as_ref();
|
||||
continue;
|
||||
}
|
||||
|
||||
break 'outer;
|
||||
};
|
||||
|
||||
let offset = if found_in_toplevel {
|
||||
let mut offset = 0;
|
||||
for _ in 0..old_offset {
|
||||
if stream.prev().is_none() {
|
||||
break;
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
current_stream = stream.clone();
|
||||
new_stack.push(LogView {
|
||||
iter: stream,
|
||||
selection_offset: offset,
|
||||
});
|
||||
}
|
||||
|
||||
// Take the top of the stack as `curr`, unless
|
||||
// the new stack is empty, then just reset the current view.
|
||||
if let Some(curr) = new_stack.pop() {
|
||||
self.curr = curr;
|
||||
} else {
|
||||
self.curr = LogView {
|
||||
iter: self.filtered_root_stream().clone(),
|
||||
selection_offset: 0,
|
||||
};
|
||||
}
|
||||
self.stack = new_stack;
|
||||
}
|
||||
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
self.curr.iter.next();
|
||||
self.curr.selection_offset -= 1;
|
||||
}
|
||||
self.last_height = num_visible_items;
|
||||
}
|
||||
|
||||
pub fn footer_fields(&self) -> Vec<(String, String)> {
|
||||
if let Some((selected, _)) = self.selected() {
|
||||
let ret = match selected.as_ref() {
|
||||
LogEntry::Single { .. } => Default::default(),
|
||||
LogEntry::Sub { sub_entries, .. } => sub_entries.last().and_then(|i| {
|
||||
i.all_fields()
|
||||
.get_key_value("return")
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
}),
|
||||
};
|
||||
|
||||
selected
|
||||
.all_relevant_fields()
|
||||
.fields
|
||||
.into_iter()
|
||||
.chain(ret)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items(&mut self, max: usize) -> Option<(Vec<(Rc<LogEntry>, usize)>, usize)> {
|
||||
let mut temp_iter = self.curr.iter.clone();
|
||||
let mut res = Vec::new();
|
||||
for _ in 0..max {
|
||||
let Some(i) = temp_iter.next() else {
|
||||
break;
|
||||
};
|
||||
res.push(i);
|
||||
}
|
||||
|
||||
if res.len() > 0 && self.curr.selection_offset > res.len() - 1 {
|
||||
self.curr.selection_offset = res.len() - 1;
|
||||
}
|
||||
|
||||
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::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
match self.input_state {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_down(&mut self) {
|
||||
self.curr.selection_offset += self.last_height;
|
||||
self.input_state.reset();
|
||||
}
|
||||
|
||||
pub fn page_up(&mut self) {
|
||||
for _ in 0..self.last_height {
|
||||
if self.curr.selection_offset == 0 {
|
||||
let _ = self.curr.iter.prev();
|
||||
} else {
|
||||
self.curr.selection_offset -= 1;
|
||||
}
|
||||
}
|
||||
self.input_state.reset();
|
||||
}
|
||||
|
||||
pub fn home(&mut self) {
|
||||
match self.input_state {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Vec<usize> {
|
||||
self.stack.iter().map(|i| i.selection_offset).collect()
|
||||
}
|
||||
|
||||
pub fn back(&mut self) {
|
||||
self.cache.insert(self.path(), self.curr.clone());
|
||||
if let Some(stack) = self.stack.pop() {
|
||||
self.curr = stack;
|
||||
}
|
||||
self.input_state.reset();
|
||||
}
|
||||
|
||||
pub fn enter(&mut self) {
|
||||
match self.input_state {
|
||||
InputState::None => {
|
||||
let Some((s, _)) = self.selected() else {
|
||||
return;
|
||||
};
|
||||
let Some(i) = s.from_start(0) else {
|
||||
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 {
|
||||
iter: Box::new(i),
|
||||
selection_offset: 0,
|
||||
},
|
||||
));
|
||||
if let Some(cached_view) = self.cache.get(&self.path()) {
|
||||
self.curr = cached_view.clone();
|
||||
}
|
||||
}
|
||||
InputState::Target(InputTarget::Fields(None)) => {
|
||||
self.input_state =
|
||||
InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo)))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue