diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 0000000..b126412 --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/home/jana/src/projects/rustc-logviz" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/home/jana/src/projects/rustc-logviz")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/home/jana/src/projects/rustc-logviz" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/home/jana/src/projects/rustc-logviz/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/home/jana/src/projects/rustc-logviz/.envrc" "/home/jana/src/projects/rustc-logviz/.direnv"/*.rc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..44610e5 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake; diff --git a/.gitignore b/.gitignore index ea8c4bf..2d5df85 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5458660 --- /dev/null +++ b/flake.lock @@ -0,0 +1,153 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "naersk", + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1752475459, + "narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=", + "owner": "nix-community", + "repo": "fenix", + "rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "fenix": "fenix", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1769799857, + "narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=", + "owner": "nix-community", + "repo": "naersk", + "rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1752077645, + "narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "be9e214982e20b8310878ac2baa063a961c1bdf6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-mozilla": { + "flake": false, + "locked": { + "lastModified": 1762256096, + "narHash": "sha256-Hzj/d8eRpfRfjHSRX6gGRf0jSg2zIJMXl6S5opuKsHc=", + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "rev": "80c058cf774c198fb838fc3549806b232dd3e320", + "type": "github" + }, + "original": { + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs_2", + "nixpkgs-mozilla": "nixpkgs-mozilla" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1752428706, + "narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "591e3b7624be97e4443ea7b5542c191311aa141d", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c210fde --- /dev/null +++ b/flake.nix @@ -0,0 +1,77 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + # for building rust packages + naersk.url = "github:nix-community/naersk"; + # for eary pre-built toolchains + nixpkgs-mozilla = { + url = "github:mozilla/nixpkgs-mozilla"; + flake = false; + }; + }; + outputs = + { + self, + nixpkgs, + flake-utils, + nixpkgs-mozilla, + naersk, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = (import nixpkgs) { + inherit system; + overlays = [ + (import nixpkgs-mozilla) + ]; + }; + + toolchain = ( + (pkgs.rustChannelOf { + rustToolchain = ./rust-toolchain.toml; + sha256 = "sha256-sqSWJDUxc+zaz1nBWMAJKTAGBuGWP25GCftIOlCEAtA="; + }).rust.override + { + extensions = [ + "rust-src" + ]; + } + ); + in + { + packages = { + backend = pkgs.callPackage ./packages/rawr-backend.nix { + inherit naersk toolchain; + }; + }; + devShells.default = + with pkgs; + mkShell { + nativeBuildInputs = [ + openssl + ]; + buildInputs = [ + pkg-config + clang + llvmPackages_latest.bintools + toolchain + ]; + packages = [ + gdb + ]; + shellHook = '' + export LIBCLANG_PATH="${lib.makeLibraryPath [ llvmPackages_latest.libclang.lib ]}" + export LD_LIBRARY_PATH="'$LD_LIBRARY_PATH:${ + lib.makeLibraryPath [ + openssl + ] + }" + PKG_CONFIG_PATH="${openssl.dev}/lib/pkgconfig"; + ''; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a0e66db --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.92" +components = ["rust-analyzer", "rust-src", "rustfmt"] diff --git a/src/main.rs b/src/main.rs index 84baf98..c6e2207 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,15 @@ enum Preset { } fn default_tempdir() -> PathBuf { - temp_dir().join("rustc-logviz") + let home = std::env::var("HOME").unwrap(); + let t_rs_tempdirs = PathBuf::from(home).join("tempdirs"); + let tempdir = if t_rs_tempdirs.exists() { + t_rs_tempdirs + } else { + temp_dir() + }; + + tempdir.join("rustc-logviz") } #[derive(Parser, Debug)] diff --git a/src/tui/filter.rs b/src/tui/filter.rs index e3ec3e9..3d99275 100644 --- a/src/tui/filter.rs +++ b/src/tui/filter.rs @@ -1,12 +1,13 @@ use crate::tui::model::LogEntry; +#[derive(Clone)] pub enum WipMatcher { Field { name: Option, value: Option, }, Specific { - path: Option>, + hash: Option, }, } @@ -20,9 +21,7 @@ impl WipMatcher { name: name.clone(), value: value.clone(), }), - WipMatcher::Specific { path: Some(path) } => { - Some(Matcher::Specific { path: path.clone() }) - } + WipMatcher::Specific { hash } => Some(Matcher::Specific { hash: (*hash)? }), _ => None, } } @@ -34,7 +33,7 @@ pub enum Matcher { value: serde_json::Value, }, Specific { - path: Vec, + hash: u64, }, } @@ -46,7 +45,7 @@ impl Matcher { .fields .get(name) .is_some_and(|v| v == value), - Matcher::Specific { path } => false, + Matcher::Specific { hash } => entry.hash() == *hash, } } } @@ -62,6 +61,16 @@ pub struct Filter { pub kind: FilterKind, } +impl Filter { + pub fn removes(&self, elem: &LogEntry) -> bool { + match self.kind { + FilterKind::Inline => false, + FilterKind::Remove => self.matcher.matches(elem), + } + } +} + +#[derive(Clone)] pub struct WipFilter { pub matcher: Option, pub kind: Option, diff --git a/src/tui/log_viewer.rs b/src/tui/log_viewer.rs index 418b8a8..5cfee3b 100644 --- a/src/tui/log_viewer.rs +++ b/src/tui/log_viewer.rs @@ -1,88 +1,208 @@ -use std::{collections::HashMap, mem, rc::Rc}; +use std::{collections::HashMap, iter, mem, rc::Rc}; use crate::tui::{ + filter::Filter, model::LogEntry, - reader::{FilterAdapter, LogfileReader}, + processing::{IntoLogStream, LogStream}, }; use tui_widget_list::ListState; -#[derive(Default, Clone)] pub struct LogView { - first_item: usize, - selected: usize, + iter: Box, + selection_offset: usize, +} + +impl LogView { + pub fn selected(&self) -> Option> { + 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(), + } + } } pub struct LogViewer { stack: Vec, curr: LogView, cache: HashMap, LogView>, + filters: Vec>, + pub root_stream: Box, pub last_height: usize, pub footer_selected: bool, pub footer_list: ListState, } impl LogViewer { - pub fn new() -> Self { + pub fn new(stream: impl LogStream) -> Self { Self { stack: Vec::new(), - curr: LogView::default(), + curr: LogView { + iter: stream.clone(), + selection_offset: 0, + }, + root_stream: stream.clone(), cache: HashMap::new(), footer_list: ListState::default(), footer_selected: false, last_height: 0, + filters: Vec::new(), } } + pub fn filtered_root_stream(&self) -> Box { + 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) { + 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, + ) -> Option> { + 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::::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.selected >= self.curr.first_item + num_visible_items { - self.curr.first_item += 1; + 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, file: &mut LogfileReader) -> Vec<(String, serde_json::Value)> { - if let Some(selected) = self.selected(file) { + pub fn footer_fields(&self) -> Vec<(String, serde_json::Value)> { + if let Some(selected) = self.selected() { selected.all_fields().fields.into_iter().collect::>() } else { Vec::new() } } - pub fn items( - &self, - file: &mut LogfileReader, - max: usize, - ) -> Option<(Vec>, usize, usize)> { - let items: Vec<_> = if self.stack.is_empty() { - file.iter_from(self.curr.first_item).take(max).collect() - } else { - let mut stack = self.stack.iter(); - let first = stack.next().unwrap(); - let mut curr_log_entry = file.iter_from(first.selected).next()?; - for elem in stack { - curr_log_entry = curr_log_entry.get(elem.selected)?; - } + pub fn items(&self, max: usize) -> Option<(Vec>, 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); + } - match curr_log_entry.as_ref() { - LogEntry::Single { .. } => return None, - LogEntry::Sub { sub_entries, .. } => { - FilterAdapter::new(file, &sub_entries[self.curr.first_item..]) - .take(max) - .cloned() - .collect() - } - } - }; - - Some((items, self.curr.first_item, self.curr.selected)) + Some((res, self.curr.selection_offset)) } - pub fn selected(&self, file: &mut LogfileReader) -> Option> { - self.items(file, self.curr.selected - self.curr.first_item + 1)? - .0 - .get(self.curr.selected - self.curr.first_item) - .cloned() + pub fn selected(&self) -> Option> { + self.curr.selected() } fn update_footer_select(&mut self) { @@ -93,8 +213,11 @@ impl LogViewer { if self.footer_selected { self.footer_list.previous(); } else { - self.curr.selected = self.curr.selected.saturating_sub(1); - self.curr.first_item = self.curr.first_item.min(self.curr.selected); + if self.curr.selection_offset == 0 { + let _ = self.curr.iter.prev(); + } else { + self.curr.selection_offset -= 1; + } self.update_footer_select(); } } @@ -103,20 +226,25 @@ impl LogViewer { if self.footer_selected { self.footer_list.next(); } else { - self.curr.selected += 1; + self.curr.selection_offset += 1; self.update_footer_select(); } } pub fn page_down(&mut self) { - self.curr.selected += self.last_height; + self.curr.selection_offset += self.last_height; self.footer_selected = false; self.update_footer_select(); } pub fn page_up(&mut self) { - self.curr.selected = self.curr.selected.saturating_sub(self.last_height); - self.curr.first_item = self.curr.first_item.min(self.curr.selected); + for _ in 0..self.last_height { + if self.curr.selection_offset == 0 { + let _ = self.curr.iter.prev(); + } else { + self.curr.selection_offset -= 1; + } + } self.footer_selected = false; self.update_footer_select(); } @@ -125,19 +253,21 @@ impl LogViewer { if self.footer_selected { self.footer_list.select(Some(0)); } else { - self.curr.selected = 0; - self.curr.first_item = 0; + self.curr.selection_offset = 0; + while self.curr.iter.prev().is_some() {} self.update_footer_select(); } } pub fn path(&self) -> Vec { - self.stack.iter().map(|i| i.selected).collect() + self.stack.iter().map(|i| i.selection_offset).collect() } pub fn back(&mut self) { - self.cache.insert(self.path(), self.curr); - self.curr = self.stack.pop().unwrap_or_default(); + self.cache.insert(self.path(), self.curr.clone()); + if let Some(stack) = self.stack.pop() { + self.curr = stack; + } self.footer_selected = false; self.update_footer_select(); } @@ -146,19 +276,24 @@ impl LogViewer { self.footer_selected = !self.footer_selected; } - pub fn enter(&mut self, file: &mut LogfileReader) { + pub fn enter(&mut self) { if !self.footer_selected { - let Some(s) = self.selected(file) else { + let Some(s) = self.selected() else { return; }; - if let LogEntry::Single { .. } = s.as_ref() { + let Some(i) = s.from_start() else { return; - } + }; - self.stack - .push(mem::replace(&mut self.curr, LogView::default())); + 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; + self.curr = cached_view.clone(); } self.update_footer_select(); } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 6df472d..20eb6c0 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -4,6 +4,7 @@ use std::{ io, path::{Path, PathBuf}, process::exit, + rc::Rc, }; use tui_widget_list::{ListBuilder, ListView}; @@ -21,6 +22,7 @@ use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyModifiers}, layout::{Constraint, HorizontalAlignment, Layout, Rect}, style::Style, + text::Span, widgets::{ Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, }, @@ -29,6 +31,7 @@ use ratatui::{ pub mod filter; pub mod log_viewer; pub mod model; +pub mod processing; pub mod reader; pub fn run(logs_dir: PathBuf) { @@ -68,13 +71,9 @@ impl Tab { } } -fn initialize_filter( - lv: &mut LogViewer, - file: &mut LogfileReader, - kind: Option, -) -> WipFilter { +fn initialize_filter(lv: &mut LogViewer, kind: Option) -> WipFilter { let matcher = if lv.footer_selected { - let footer_fields = lv.footer_fields(file); + 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))); @@ -84,7 +83,7 @@ fn initialize_filter( }) } else { Some(WipMatcher::Specific { - path: Some(lv.path().clone()), + hash: lv.selected().map(|i| i.hash()), }) }; @@ -115,7 +114,7 @@ impl App { } fn current_file_path(&self) -> Option { - self.current_file.as_ref().map(|i| i.path.to_path_buf()) + self.current_file.as_ref().map(|i| i.path()) } fn replace_tab(&mut self, tab: Tab) { @@ -244,23 +243,19 @@ impl App { (KeyCode::Left, Tab::CreateFilter { filter }) => { filter.left(); } - (KeyCode::Right, Tab::LogViewer(lv)) => { - if let Some(file) = &mut self.current_file { - lv.enter(file) - } - } + (KeyCode::Right, Tab::LogViewer(lv)) => lv.enter(), (KeyCode::Tab, Tab::LogViewer(lv)) => { lv.switch_focus(); } (KeyCode::Char('r'), Tab::LogViewer(lv)) => { if let Some(file) = &mut self.current_file { - let filter = initialize_filter(lv, file, Some(FilterKind::Remove)); + let filter = initialize_filter(lv, Some(FilterKind::Remove)); self.push_tab(Tab::CreateFilter { filter }); } } (KeyCode::Char('i'), Tab::LogViewer(lv)) => { if let Some(file) = &mut self.current_file { - let filter = initialize_filter(lv, file, Some(FilterKind::Inline)); + let filter = initialize_filter(lv, Some(FilterKind::Inline)); self.push_tab(Tab::CreateFilter { filter }); } } @@ -271,8 +266,8 @@ impl App { { match LogfileReader::new(&selected.path()) { Ok(i) => { - self.current_file = Some(i); - self.replace_tab(Tab::LogViewer(LogViewer::new())); + self.current_file = Some(i.clone()); + self.replace_tab(Tab::LogViewer(LogViewer::new(i.iter()))); } Err(e) => { panic!() @@ -281,22 +276,26 @@ impl App { } } Tab::LogViewer(lv) => { - if let Some(file) = &mut self.current_file { - if lv.footer_selected { - let filter = initialize_filter(lv, file, None); - self.push_tab(Tab::CreateFilter { filter }); - } else { - lv.enter(file) - } + if lv.footer_selected { + let filter = initialize_filter(lv, None); + self.push_tab(Tab::CreateFilter { filter }); + } else { + lv.enter() } } Tab::Empty => {} Tab::CreateFilter { filter } => { - if let FilterSelection::Confirm = filter.selection - && let Some(file) = &mut self.current_file - { - if let Some(filter) = filter.validate() { - file.add_filter(filter); + if let FilterSelection::Confirm = filter.selection { + let filter_clone = filter.clone(); + if let Some(lv) = self.tabs.iter_mut().rev().find_map(|i| { + if let Tab::LogViewer(lv) = i { + Some(lv) + } else { + None + } + }) && let Some(filter) = filter_clone.validate() + { + lv.add_filter(Rc::new(filter)); self.pop_tab(); if let Tab::LogViewer(lv) = self.current_tab() { @@ -419,22 +418,20 @@ impl Widget for &mut App { StatefulWidget::render(list, main_area, buf, state); } Tab::LogViewer(lv) => { - let Some(file) = &mut self.current_file else { - continue; - }; - lv.update_num_items(main_area.height as usize); - let (items, start, selected) = lv - .items(file, main_area.height as usize) - .unwrap_or_else(|| (Vec::new(), 0, 0)); + let (items, selected_offset) = lv + .items(main_area.height as usize) + .unwrap_or_else(|| (Vec::new(), 0)); + + Paragraph::new(selected_offset.to_string()).render(right, buf); let list = List::new(items.into_iter().enumerate().map(|(idx, i)| { let line = i.line_text(false); let mut list_item = ListItem::new(line); - if idx + start == selected { + if idx == selected_offset { list_item = list_item.style(highlighted); } @@ -442,7 +439,7 @@ impl Widget for &mut App { })); Widget::render(list, main_area, buf); - let items = lv.footer_fields(file); + let items = lv.footer_fields(); let width = 20; let builder = ListBuilder::new(|cx| { let Some((k, v)) = &items.get(cx.index) else { diff --git a/src/tui/model.rs b/src/tui/model.rs index f4fdd84..cb6e55e 100644 --- a/src/tui/model.rs +++ b/src/tui/model.rs @@ -1,10 +1,15 @@ -use std::{collections::BTreeMap, rc::Rc, sync::OnceLock}; +use std::{ + collections::BTreeMap, + hash::{DefaultHasher, Hash, Hasher}, + rc::Rc, + sync::OnceLock, +}; use jiff::Timestamp; use ratatui::text::Line; use serde::Deserialize; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Hash)] pub enum Level { #[serde(rename = "TRACE")] Trace, @@ -33,6 +38,20 @@ pub enum LogEntry { } impl LogEntry { + pub fn hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + match self { + LogEntry::Single { raw } => { + raw.hash(&mut hasher); + hasher.finish() + } + LogEntry::Sub { enter, exit, .. } => { + (enter, exit).hash(&mut hasher); + hasher.finish() + } + } + } + pub fn get(&self, index: usize) -> Option> { match self { LogEntry::Single { .. } => None, @@ -89,7 +108,7 @@ impl LogEntry { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Hash)] pub struct LogFields { #[serde(flatten)] pub fields: BTreeMap, @@ -112,7 +131,7 @@ impl LogFields { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Hash)] pub struct RawLogEntry { pub timestamp: Timestamp, pub level: Level, diff --git a/src/tui/processing.rs b/src/tui/processing.rs new file mode 100644 index 0000000..bdada40 --- /dev/null +++ b/src/tui/processing.rs @@ -0,0 +1,170 @@ +use std::rc::Rc; + +use crate::tui::{ + filter::{Filter, FilterKind}, + model::LogEntry, +}; + +pub trait IntoLogStream { + type Stream: LogStream; + + fn from_end(self) -> Option; + fn from_start(self) -> Option; +} + +pub struct LogEntryStream { + inner: Rc, + curr: usize, +} + +impl LogStream for LogEntryStream { + fn next(&mut self) -> Option> { + let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else { + return None; + }; + + let res = sub_entries.get(self.curr)?; + self.curr += 1; + Some(Rc::clone(res)) + } + + fn prev(&mut self) -> Option> { + let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else { + return None; + }; + + self.curr -= 1; + let res = sub_entries.get(self.curr)?; + Some(Rc::clone(res)) + } + + fn clone(&self) -> Box { + Box::new(Self { + inner: Rc::clone(&self.inner), + curr: self.curr, + }) + } +} + +impl IntoLogStream for &Rc { + type Stream = LogEntryStream; + + fn from_end(self) -> Option { + if let LogEntry::Sub { sub_entries, .. } = self.as_ref() { + Some(LogEntryStream { + inner: Rc::clone(&self), + curr: sub_entries.len(), + }) + } else { + None + } + } + + fn from_start(self) -> Option { + if let LogEntry::Sub { .. } = self.as_ref() { + Some(LogEntryStream { + inner: Rc::clone(&self), + curr: 0, + }) + } else { + None + } + } +} + +pub trait LogStream { + fn next(&mut self) -> Option>; + fn prev(&mut self) -> Option>; + + fn clone(&self) -> Box; + + fn filter(&self, filter: Rc) -> FilteredLogStream { + FilteredLogStream { + filter: filter, + stack: vec![self.clone()], + } + } +} + +pub struct FilteredLogStream { + filter: Rc, + stack: Vec>, +} + +macro_rules! generate_candidate { + ($_self: tt, $iter_method: ident) => { + loop { + let top = $_self.stack.last_mut().unwrap(); + if let Some(top) = top.$iter_method() { + // if we can find it in the top of stack iterator, neat! + return Some(top); + } else if $_self.stack.len() > 1 { + // Otherwise, try popping the stack once and try again + $_self.stack.pop(); + } else { + // However, never pop the stack empty. + // If that'd happen, just return None. + return None; + } + } + }; +} + +macro_rules! generate_filter { + ($_self: tt, $candidate: ident, $into_iter: ident) => { + loop { + let elem = $_self.$candidate()?; + let Filter { matcher, kind } = $_self.filter.as_ref(); + + if matcher.matches(&elem) { + match kind { + FilterKind::Inline => { + // When we inline, add this item to the stack + // so we continue iterating inside it. + if let Some(iter) = elem.$into_iter() { + $_self.stack.push(Box::new(iter)); + } + // Continue so we actually return a nested item. + continue; + } + FilterKind::Remove => { + // continue past removed items + continue; + } + } + } else { + return Some(elem); + } + } + }; +} + +impl FilteredLogStream { + fn next_candidate(&mut self) -> Option> { + generate_candidate!(self, next) + } + fn prev_candidate(&mut self) -> Option> { + generate_candidate!(self, prev) + } +} + +impl LogStream for FilteredLogStream { + fn next(&mut self) -> Option> { + generate_filter!(self, next_candidate, from_start) + } + + fn prev(&mut self) -> Option> { + generate_filter!(self, prev_candidate, from_end) + } + + fn clone(&self) -> Box { + Box::new(Self { + filter: Rc::clone(&self.filter), + stack: self + .stack + .iter() + .map(|i| LogStream::clone(i.as_ref())) + .collect(), + }) + } +} diff --git a/src/tui/reader.rs b/src/tui/reader.rs index 6f4796d..a5efaff 100644 --- a/src/tui/reader.rs +++ b/src/tui/reader.rs @@ -1,4 +1,5 @@ use std::{ + cell::RefCell, fs::File, io::{self, BufRead, BufReader}, mem, @@ -8,35 +9,36 @@ use std::{ }; use crate::tui::{ - filter::{Filter, FilterKind}, model::{LogEntry, RawLogEntry}, + processing::LogStream, }; -pub struct LogfileReader { +struct Inner { pub path: PathBuf, - file: BufReader, - entries: Vec>, - filters: Vec>, + file: BufReader, + cached_entries: Vec>, } +#[derive(Clone)] +pub struct LogfileReader(Rc>); + impl LogfileReader { pub fn new(p: &Path) -> io::Result { - Ok(Self { + Ok(Self(Rc::new(RefCell::new(Inner { file: BufReader::new(File::open(p)?), path: p.to_path_buf(), - entries: Vec::new(), - filters: Vec::new(), - }) + cached_entries: Vec::new(), + })))) } - pub fn add_filter(&mut self, filter: Filter) { - self.filters.push(Rc::new(filter)); + pub fn path(&self) -> PathBuf { + self.0.borrow().path.clone() } fn next_line(&mut self) -> Option { let mut res = String::new(); - match self.file.read_line(&mut res) { + match self.0.borrow_mut().file.read_line(&mut res) { Err(e) => { eprintln!("error: {e:?}"); None @@ -93,71 +95,51 @@ impl LogfileReader { } } - pub fn iter_from(&mut self, start: usize) -> FilterAdapter> { - FilterAdapter { - filters: self.filters.clone(), - inner: EntryIterator { - curr: start, - reader: self, - }, + fn fill_buf_to_access_index(&mut self, n: usize) -> Option> { + while self.0.borrow().cached_entries.len() <= n { + let entry = self.next_entry()?; + self.0.borrow_mut().cached_entries.push(entry); } + + Some(Rc::clone(&self.0.borrow().cached_entries[n])) } - fn add_next_entry(&mut self) -> Option<()> { - let entry = self.next_entry()?; - self.entries.push(entry); - Some(()) + pub fn iter(&self) -> LogFileReaderStream { + LogFileReaderStream { + reader: self.clone(), + curr: 0, + } } } -pub struct EntryIterator<'a> { +pub struct LogFileReaderStream { + reader: LogfileReader, curr: usize, - reader: &'a mut LogfileReader, } -impl<'a> Iterator for EntryIterator<'a> { - type Item = Rc; - - fn next(&mut self) -> Option { - while self.reader.entries.len() <= self.curr { - self.reader.add_next_entry()?; - } - - let res = Rc::clone(&self.reader.entries[self.curr]); +impl LogStream for LogFileReaderStream { + fn next(&mut self) -> Option> { + let entry = self.reader.fill_buf_to_access_index(self.curr)?; self.curr += 1; - Some(res) + + Some(entry) } -} -pub struct FilterAdapter { - filters: Vec>, - inner: I, -} - -impl FilterAdapter { - pub fn new(file: &LogfileReader, inner: I) -> FilterAdapter { - Self { - filters: file.filters.clone(), - inner: inner.into_iter(), - } - } -} - -impl Iterator for FilterAdapter { - type Item = I::Item; - - fn next(&mut self) -> Option { - 'next_entry: loop { - let res = self.inner.next()?; - - for filter in &self.reader.filters { - if let FilterKind::Remove = filter.kind - && filter.matcher.matches(&res) - { - continue 'next_entry; - } - } - break Some(res); + fn prev(&mut self) -> Option> { + if self.curr == 0 { + return None; } + + let entry = self.reader.fill_buf_to_access_index(self.curr)?; + self.curr -= 1; + + Some(entry) + } + + fn clone(&self) -> Box { + Box::new(Self { + reader: self.reader.clone(), + curr: self.curr, + }) } }