Skip to content

Commit

Permalink
forward search is working
Browse files Browse the repository at this point in the history
  • Loading branch information
janstarke committed May 13, 2024
1 parent e3956a1 commit a7f8f03
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 36 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ ratatui = {version="0.26.2", optional=true}
crossterm = {version="^0", optional=true}
quick-xml = {version="^0.31", features=["serialize"], optional=true}
serde_repr = {version="^0", optional=true}
tui-textarea = "0.4.0"

[dev-dependencies]

Expand Down
171 changes: 137 additions & 34 deletions src/bin/evtxview/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{io, time::Duration};
use std::{io, ops::Neg, time::Duration};

use crate::{
cli::Cli,
Expand All @@ -9,11 +9,36 @@ use ratatui::{
prelude::*,
widgets::{block::*, *},
};
use tui_textarea::TextArea;

// (→) next color | (←) previous color
const INFO_TEXT: &str = r#"(Esc) quit | (↑) move up | (↓) move down | (E) Exclude by Event id" | (e) include by Event id | (U) exclude by User | (u) include by User | (R) Reset filter | (o) change Orientation | (+/-) in/decrease table size"#;
const INFO_TEXT: &str = r#"(Esc) quit | (↑) move up | (↓) move down | (E) Exclude by Event id" | (e) include by Event id | (U) exclude by User | (u) include by User | (R) Reset filter | (o) change Orientation | (+/-) in/decrease table size | (/|?) search forard/backward"#;

pub struct App {
#[derive(Clone, Copy)]
enum SearchOrder {
Forward,
Backward,
}

impl Neg for SearchOrder {
type Output = Self;

fn neg(self) -> Self::Output {
match self {
SearchOrder::Forward => SearchOrder::Backward,
SearchOrder::Backward => SearchOrder::Forward,
}
}
}

#[derive(Default, Clone, Copy)]
enum AppMode {
#[default]
Normal,
SearchField(SearchOrder),
}

pub struct App<'t> {
evtx_table: EvtxTable,
exit: bool,
state: TableState,
Expand All @@ -23,18 +48,20 @@ pub struct App {
table_view_port: Rect,
orientation: Direction,
table_percentage: u16,
app_mode: AppMode,
search_field: TextArea<'t>,
search_order: SearchOrder,
}

impl App {
impl<'t> App<'t> {
pub fn new(cli: Cli) -> Self {
let paths: Vec<_> = cli.evtx_file.iter().map(|p| p.path().path()).collect();
let evtx_table = EvtxTable::try_from(paths).unwrap();
let table_len = evtx_table.len();
let table_scroll_state = if table_len == 0 {
0
} else {
table_len - 1
};
let table_scroll_state = if table_len == 0 { 0 } else { table_len - 1 };

let search_field = TextArea::default();

Self {
evtx_table,
exit: Default::default(),
Expand All @@ -45,6 +72,9 @@ impl App {
table_view_port: Rect::new(0, 0, 0, 0),
orientation: Direction::Horizontal,
table_percentage: 50,
app_mode: Default::default(),
search_order: SearchOrder::Forward,
search_field,
}
}
/// runs the application's main loop until the user quits
Expand All @@ -58,11 +88,30 @@ impl App {

fn render_frame(&mut self, frame: &mut Frame) {
let margins = Margin::new(0, 0);
let rects = Layout::vertical([
Constraint::Min(5),
Constraint::Length(3),
])
.split(frame.size());
let contents_line;
let textfield_line;
let help_line;

match self.app_mode {
AppMode::Normal => {
let lines = Layout::vertical([Constraint::Min(5), Constraint::Length(3)])
.split(frame.size());
contents_line = lines[0];
textfield_line = None;
help_line = lines[1];
}
AppMode::SearchField(_) => {
let lines = Layout::vertical([
Constraint::Min(5),
Constraint::Length(3),
Constraint::Length(3),
])
.split(frame.size());
contents_line = lines[0];
textfield_line = Some(lines[1]);
help_line = lines[2];
}
}

let cols = Layout::new(
self.orientation,
Expand All @@ -71,13 +120,13 @@ impl App {
Constraint::Percentage(100 - self.table_percentage),
],
)
.split(rects[0]);
.split(contents_line);

let table_scroll_area = cols[0].inner(&margins);
let table_contents_area = table_scroll_area.inner(&margins);
self.table_view_port = table_contents_area;

frame.render_widget(Clear, rects[0]);
frame.render_widget(Clear, contents_line);
self.render_table(frame, self.table_view_port);
frame.render_stateful_widget(
Scrollbar::default()
Expand All @@ -103,7 +152,10 @@ impl App {
details_scroll_area,
&mut self.details_scroll_state,
);
self.render_footer(frame, rects[1]);
if let Some(textfield_line) = textfield_line {
self.render_textfield(frame, textfield_line);
}
self.render_footer(frame, help_line);
}

fn render_table(&mut self, frame: &mut Frame, area: Rect) {
Expand Down Expand Up @@ -142,6 +194,20 @@ impl App {
frame.render_widget(info_footer, area);
}

fn render_textfield(&mut self, frame: &mut Frame, area: Rect) {
let block = self.bordered_block();
let layout = Layout::horizontal(vec![Constraint::Length(19), Constraint::Min(0)])
.split(block.inner(area));
frame.render_widget(block, area);
let display_text = match self.app_mode {
AppMode::Normal => "",
AppMode::SearchField(SearchOrder::Forward) => "Search (forward):",
AppMode::SearchField(SearchOrder::Backward) => "Search (backward):"
};
frame.render_widget(Text::raw(display_text), layout[0]);
frame.render_widget(self.search_field.widget(), layout[1]);
}

fn handle_events(&mut self) -> io::Result<()> {
self.evtx_table.update();
if event::poll(Duration::from_millis(100))? {
Expand All @@ -158,29 +224,66 @@ impl App {
}
fn handle_key_event(&mut self, key_event: KeyEvent) {
self.evtx_table.update();
match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => self.exit(),
KeyCode::Char('g') => self.set_selected(0),
KeyCode::Char('G') => self.set_selected(usize::max(self.evtx_table.len(), 1) - 1),
KeyCode::Down => self.next(1),
KeyCode::Up => self.previous(1),
KeyCode::PageDown => self.next((self.table_view_port.height / 2).into()),
KeyCode::PageUp => self.previous((self.table_view_port.height / 2).into()),
KeyCode::Char('E') => self.exclude_event_id(),
KeyCode::Char('e') => self.include_event_id(),
KeyCode::Char('U') => self.exclude_user(),
KeyCode::Char('u') => self.include_user(),
KeyCode::Char('R') => self.reset_filter(),
KeyCode::Char('o') => self.change_orientation(),
KeyCode::Char('+') => self.increase_table_size(),
KeyCode::Char('-') => self.decrease_table_size(),
_ => {}

match self.app_mode {
AppMode::Normal => match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => self.exit(),
KeyCode::Char('g') => self.set_selected(0),
KeyCode::Char('G') => self.set_selected(usize::max(self.evtx_table.len(), 1) - 1),
KeyCode::Down => self.next(1),
KeyCode::Up => self.previous(1),
KeyCode::PageDown => self.next((self.table_view_port.height / 2).into()),
KeyCode::PageUp => self.previous((self.table_view_port.height / 2).into()),
KeyCode::Char('E') => self.exclude_event_id(),
KeyCode::Char('e') => self.include_event_id(),
KeyCode::Char('U') => self.exclude_user(),
KeyCode::Char('u') => self.include_user(),
KeyCode::Char('R') => self.reset_filter(),
KeyCode::Char('o') => self.change_orientation(),
KeyCode::Char('+') => self.increase_table_size(),
KeyCode::Char('-') => self.decrease_table_size(),
KeyCode::Char('/') => self.app_mode = AppMode::SearchField(SearchOrder::Forward),
KeyCode::Char('?') => self.app_mode = AppMode::SearchField(SearchOrder::Backward),
KeyCode::Char('n') => self.goto_next_finding(self.search_order),
KeyCode::Char('N') => self.goto_next_finding(self.search_order.neg()),
_ => {}
},
AppMode::SearchField(order) => match key_event.code {
KeyCode::Enter => {
self.app_mode = AppMode::Normal;
self.search_order = order;
self.goto_next_finding(order);
}

KeyCode::Esc => {
self.app_mode = AppMode::Normal;
}

_ => {
self.search_field.input(key_event);
}
},
}
}
fn exit(&mut self) {
self.exit = true;
}

fn goto_next_finding(&mut self, order: SearchOrder) {
if !self.evtx_table.is_empty() {
if let Some(i) = self.state.selected() {
if let Some(search_string) = self.search_field.lines().first() {
if let Some(index) = match order {
SearchOrder::Forward => self.evtx_table.find_next(i, search_string),
SearchOrder::Backward => self.evtx_table.find_previous(i, search_string),
} {
self.set_selected(index);
}
}
}
}
}

fn increase_table_size(&mut self) {
// leave some space
if self.table_percentage < 97 {
Expand Down
31 changes: 29 additions & 2 deletions src/bin/evtxview/tui/evtx_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ impl EventFilter {
match self {
EventFilter::ExcludeByEventId(event_id) => rc.event().system().EventID() != event_id,
EventFilter::IncludeByEventId(event_id) => rc.event().system().EventID() == event_id,
EventFilter::ExcludeByUser(user_id) => rc.event().system().security().user_id().as_ref() != Some(user_id),
EventFilter::IncludeByUser(user_id) => rc.event().system().security().user_id().as_ref() == Some(user_id),
EventFilter::ExcludeByUser(user_id) => {
rc.event().system().security().user_id().as_ref() != Some(user_id)
}
EventFilter::IncludeByUser(user_id) => {
rc.event().system().security().user_id().as_ref() == Some(user_id)
}
}
}
}
Expand Down Expand Up @@ -303,6 +307,29 @@ impl EvtxTable {
pub fn read_status(&self) -> Option<ReadState> {
self.data.lock().ok().map(|data| data.state)
}

pub fn find_next(&self, starting_at: usize, search_string: &str) -> Option<usize> {
if let Ok(data) = self.data.lock() {
data.rows
.iter()
.filter(|rc| self.filter_row(rc))
.enumerate()
.filter(|(idx, _rc)| *idx > starting_at)
.find_map(|(idx, rc)| {
if rc.raw_value().contains(search_string) {
Some(idx)
} else {
None
}
})
} else {
None
}
}

pub fn find_previous(&self, starting_at: usize, search_string: &str) -> Option<usize> {
None
}
}

#[self_referencing]
Expand Down

0 comments on commit a7f8f03

Please sign in to comment.