new streams
This commit is contained in:
parent
3963fc50c3
commit
c73be7166f
13 changed files with 748 additions and 174 deletions
19
.direnv/bin/nix-direnv-reload
Executable file
19
.direnv/bin/nix-direnv-reload
Executable file
|
|
@ -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
|
||||||
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake;
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
.direnv
|
||||||
|
|
|
||||||
153
flake.lock
generated
Normal file
153
flake.lock
generated
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
77
flake.nix
Normal file
77
flake.nix
Normal file
|
|
@ -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";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.92"
|
||||||
|
components = ["rust-analyzer", "rust-src", "rustfmt"]
|
||||||
10
src/main.rs
10
src/main.rs
|
|
@ -27,7 +27,15 @@ enum Preset {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tempdir() -> PathBuf {
|
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)]
|
#[derive(Parser, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use crate::tui::model::LogEntry;
|
use crate::tui::model::LogEntry;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum WipMatcher {
|
pub enum WipMatcher {
|
||||||
Field {
|
Field {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
value: Option<serde_json::Value>,
|
value: Option<serde_json::Value>,
|
||||||
},
|
},
|
||||||
Specific {
|
Specific {
|
||||||
path: Option<Vec<usize>>,
|
hash: Option<u64>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,9 +21,7 @@ impl WipMatcher {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
value: value.clone(),
|
value: value.clone(),
|
||||||
}),
|
}),
|
||||||
WipMatcher::Specific { path: Some(path) } => {
|
WipMatcher::Specific { hash } => Some(Matcher::Specific { hash: (*hash)? }),
|
||||||
Some(Matcher::Specific { path: path.clone() })
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +33,7 @@ pub enum Matcher {
|
||||||
value: serde_json::Value,
|
value: serde_json::Value,
|
||||||
},
|
},
|
||||||
Specific {
|
Specific {
|
||||||
path: Vec<usize>,
|
hash: u64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +45,7 @@ impl Matcher {
|
||||||
.fields
|
.fields
|
||||||
.get(name)
|
.get(name)
|
||||||
.is_some_and(|v| v == value),
|
.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,
|
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 struct WipFilter {
|
||||||
pub matcher: Option<WipMatcher>,
|
pub matcher: Option<WipMatcher>,
|
||||||
pub kind: Option<FilterKind>,
|
pub kind: Option<FilterKind>,
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,208 @@
|
||||||
use std::{collections::HashMap, mem, rc::Rc};
|
use std::{collections::HashMap, iter, mem, rc::Rc};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
|
filter::Filter,
|
||||||
model::LogEntry,
|
model::LogEntry,
|
||||||
reader::{FilterAdapter, LogfileReader},
|
processing::{IntoLogStream, LogStream},
|
||||||
};
|
};
|
||||||
use tui_widget_list::ListState;
|
use tui_widget_list::ListState;
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub struct LogView {
|
pub struct LogView {
|
||||||
first_item: usize,
|
iter: Box<dyn LogStream>,
|
||||||
selected: usize,
|
selection_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogView {
|
||||||
|
pub fn selected(&self) -> Option<Rc<LogEntry>> {
|
||||||
|
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 {
|
pub struct LogViewer {
|
||||||
stack: Vec<LogView>,
|
stack: Vec<LogView>,
|
||||||
curr: LogView,
|
curr: LogView,
|
||||||
cache: HashMap<Vec<usize>, LogView>,
|
cache: HashMap<Vec<usize>, LogView>,
|
||||||
|
filters: Vec<Rc<Filter>>,
|
||||||
|
|
||||||
|
pub root_stream: Box<dyn LogStream>,
|
||||||
pub last_height: usize,
|
pub last_height: usize,
|
||||||
pub footer_selected: bool,
|
pub footer_selected: bool,
|
||||||
pub footer_list: ListState,
|
pub footer_list: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogViewer {
|
impl LogViewer {
|
||||||
pub fn new() -> Self {
|
pub fn new(stream: impl LogStream) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stack: Vec::new(),
|
stack: Vec::new(),
|
||||||
curr: LogView::default(),
|
curr: LogView {
|
||||||
|
iter: stream.clone(),
|
||||||
|
selection_offset: 0,
|
||||||
|
},
|
||||||
|
root_stream: stream.clone(),
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
footer_list: ListState::default(),
|
footer_list: ListState::default(),
|
||||||
footer_selected: false,
|
footer_selected: false,
|
||||||
last_height: 0,
|
last_height: 0,
|
||||||
|
filters: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
pub fn update_num_items(&mut self, num_visible_items: usize) {
|
||||||
while self.curr.selected >= self.curr.first_item + num_visible_items {
|
while self.curr.selection_offset >= num_visible_items {
|
||||||
self.curr.first_item += 1;
|
if self.curr.selection_offset == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.curr.iter.next();
|
||||||
|
self.curr.selection_offset -= 1;
|
||||||
}
|
}
|
||||||
self.last_height = num_visible_items;
|
self.last_height = num_visible_items;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn footer_fields(&self, file: &mut LogfileReader) -> Vec<(String, serde_json::Value)> {
|
pub fn footer_fields(&self) -> Vec<(String, serde_json::Value)> {
|
||||||
if let Some(selected) = self.selected(file) {
|
if let Some(selected) = self.selected() {
|
||||||
selected.all_fields().fields.into_iter().collect::<Vec<_>>()
|
selected.all_fields().fields.into_iter().collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn items(
|
pub fn items(&self, max: usize) -> Option<(Vec<Rc<LogEntry>>, usize)> {
|
||||||
&self,
|
let mut temp_iter = self.curr.iter.clone();
|
||||||
file: &mut LogfileReader,
|
let mut res = Vec::new();
|
||||||
max: usize,
|
for _ in 0..max {
|
||||||
) -> Option<(Vec<Rc<LogEntry>>, usize, usize)> {
|
let Some(i) = temp_iter.next() else {
|
||||||
let items: Vec<_> = if self.stack.is_empty() {
|
break;
|
||||||
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)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
res.push(i);
|
||||||
Some((items, self.curr.first_item, self.curr.selected))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected(&self, file: &mut LogfileReader) -> Option<Rc<LogEntry>> {
|
Some((res, self.curr.selection_offset))
|
||||||
self.items(file, self.curr.selected - self.curr.first_item + 1)?
|
}
|
||||||
.0
|
|
||||||
.get(self.curr.selected - self.curr.first_item)
|
pub fn selected(&self) -> Option<Rc<LogEntry>> {
|
||||||
.cloned()
|
self.curr.selected()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_footer_select(&mut self) {
|
fn update_footer_select(&mut self) {
|
||||||
|
|
@ -93,8 +213,11 @@ impl LogViewer {
|
||||||
if self.footer_selected {
|
if self.footer_selected {
|
||||||
self.footer_list.previous();
|
self.footer_list.previous();
|
||||||
} else {
|
} else {
|
||||||
self.curr.selected = self.curr.selected.saturating_sub(1);
|
if self.curr.selection_offset == 0 {
|
||||||
self.curr.first_item = self.curr.first_item.min(self.curr.selected);
|
let _ = self.curr.iter.prev();
|
||||||
|
} else {
|
||||||
|
self.curr.selection_offset -= 1;
|
||||||
|
}
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,20 +226,25 @@ impl LogViewer {
|
||||||
if self.footer_selected {
|
if self.footer_selected {
|
||||||
self.footer_list.next();
|
self.footer_list.next();
|
||||||
} else {
|
} else {
|
||||||
self.curr.selected += 1;
|
self.curr.selection_offset += 1;
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_down(&mut self) {
|
pub fn page_down(&mut self) {
|
||||||
self.curr.selected += self.last_height;
|
self.curr.selection_offset += self.last_height;
|
||||||
self.footer_selected = false;
|
self.footer_selected = false;
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_up(&mut self) {
|
pub fn page_up(&mut self) {
|
||||||
self.curr.selected = self.curr.selected.saturating_sub(self.last_height);
|
for _ in 0..self.last_height {
|
||||||
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.footer_selected = false;
|
self.footer_selected = false;
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
|
|
@ -125,19 +253,21 @@ impl LogViewer {
|
||||||
if self.footer_selected {
|
if self.footer_selected {
|
||||||
self.footer_list.select(Some(0));
|
self.footer_list.select(Some(0));
|
||||||
} else {
|
} else {
|
||||||
self.curr.selected = 0;
|
self.curr.selection_offset = 0;
|
||||||
self.curr.first_item = 0;
|
while self.curr.iter.prev().is_some() {}
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> Vec<usize> {
|
pub fn path(&self) -> Vec<usize> {
|
||||||
self.stack.iter().map(|i| i.selected).collect()
|
self.stack.iter().map(|i| i.selection_offset).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn back(&mut self) {
|
pub fn back(&mut self) {
|
||||||
self.cache.insert(self.path(), self.curr);
|
self.cache.insert(self.path(), self.curr.clone());
|
||||||
self.curr = self.stack.pop().unwrap_or_default();
|
if let Some(stack) = self.stack.pop() {
|
||||||
|
self.curr = stack;
|
||||||
|
}
|
||||||
self.footer_selected = false;
|
self.footer_selected = false;
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
|
|
@ -146,19 +276,24 @@ impl LogViewer {
|
||||||
self.footer_selected = !self.footer_selected;
|
self.footer_selected = !self.footer_selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enter(&mut self, file: &mut LogfileReader) {
|
pub fn enter(&mut self) {
|
||||||
if !self.footer_selected {
|
if !self.footer_selected {
|
||||||
let Some(s) = self.selected(file) else {
|
let Some(s) = self.selected() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let LogEntry::Single { .. } = s.as_ref() {
|
let Some(i) = s.from_start() else {
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
self.stack
|
self.stack.push(mem::replace(
|
||||||
.push(mem::replace(&mut self.curr, LogView::default()));
|
&mut self.curr,
|
||||||
|
LogView {
|
||||||
|
iter: Box::new(i),
|
||||||
|
selection_offset: 0,
|
||||||
|
},
|
||||||
|
));
|
||||||
if let Some(cached_view) = self.cache.get(&self.path()) {
|
if let Some(cached_view) = self.cache.get(&self.path()) {
|
||||||
self.curr = *cached_view;
|
self.curr = cached_view.clone();
|
||||||
}
|
}
|
||||||
self.update_footer_select();
|
self.update_footer_select();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::{
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::exit,
|
process::exit,
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
use tui_widget_list::{ListBuilder, ListView};
|
use tui_widget_list::{ListBuilder, ListView};
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ use ratatui::{
|
||||||
crossterm::event::{self, Event, KeyCode, KeyModifiers},
|
crossterm::event::{self, Event, KeyCode, KeyModifiers},
|
||||||
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
layout::{Constraint, HorizontalAlignment, Layout, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
|
text::Span,
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
|
||||||
},
|
},
|
||||||
|
|
@ -29,6 +31,7 @@ use ratatui::{
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod log_viewer;
|
pub mod log_viewer;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
pub mod processing;
|
||||||
pub mod reader;
|
pub mod reader;
|
||||||
|
|
||||||
pub fn run(logs_dir: PathBuf) {
|
pub fn run(logs_dir: PathBuf) {
|
||||||
|
|
@ -68,13 +71,9 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_filter(
|
fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter {
|
||||||
lv: &mut LogViewer,
|
|
||||||
file: &mut LogfileReader,
|
|
||||||
kind: Option<FilterKind>,
|
|
||||||
) -> WipFilter {
|
|
||||||
let matcher = if lv.footer_selected {
|
let matcher = if lv.footer_selected {
|
||||||
let footer_fields = lv.footer_fields(file);
|
let footer_fields = lv.footer_fields();
|
||||||
let (key, value) = footer_fields
|
let (key, value) = footer_fields
|
||||||
.get(lv.footer_list.selected.unwrap_or(0))
|
.get(lv.footer_list.selected.unwrap_or(0))
|
||||||
.map_or((None, None), |(k, v)| (Some(k), Some(v)));
|
.map_or((None, None), |(k, v)| (Some(k), Some(v)));
|
||||||
|
|
@ -84,7 +83,7 @@ fn initialize_filter(
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Some(WipMatcher::Specific {
|
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<PathBuf> {
|
fn current_file_path(&self) -> Option<PathBuf> {
|
||||||
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) {
|
fn replace_tab(&mut self, tab: Tab) {
|
||||||
|
|
@ -244,23 +243,19 @@ impl App {
|
||||||
(KeyCode::Left, Tab::CreateFilter { filter }) => {
|
(KeyCode::Left, Tab::CreateFilter { filter }) => {
|
||||||
filter.left();
|
filter.left();
|
||||||
}
|
}
|
||||||
(KeyCode::Right, Tab::LogViewer(lv)) => {
|
(KeyCode::Right, Tab::LogViewer(lv)) => lv.enter(),
|
||||||
if let Some(file) = &mut self.current_file {
|
|
||||||
lv.enter(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(KeyCode::Tab, Tab::LogViewer(lv)) => {
|
(KeyCode::Tab, Tab::LogViewer(lv)) => {
|
||||||
lv.switch_focus();
|
lv.switch_focus();
|
||||||
}
|
}
|
||||||
(KeyCode::Char('r'), Tab::LogViewer(lv)) => {
|
(KeyCode::Char('r'), Tab::LogViewer(lv)) => {
|
||||||
if let Some(file) = &mut self.current_file {
|
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 });
|
self.push_tab(Tab::CreateFilter { filter });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Char('i'), Tab::LogViewer(lv)) => {
|
(KeyCode::Char('i'), Tab::LogViewer(lv)) => {
|
||||||
if let Some(file) = &mut self.current_file {
|
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 });
|
self.push_tab(Tab::CreateFilter { filter });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,8 +266,8 @@ impl App {
|
||||||
{
|
{
|
||||||
match LogfileReader::new(&selected.path()) {
|
match LogfileReader::new(&selected.path()) {
|
||||||
Ok(i) => {
|
Ok(i) => {
|
||||||
self.current_file = Some(i);
|
self.current_file = Some(i.clone());
|
||||||
self.replace_tab(Tab::LogViewer(LogViewer::new()));
|
self.replace_tab(Tab::LogViewer(LogViewer::new(i.iter())));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!()
|
panic!()
|
||||||
|
|
@ -281,22 +276,26 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tab::LogViewer(lv) => {
|
Tab::LogViewer(lv) => {
|
||||||
if let Some(file) = &mut self.current_file {
|
|
||||||
if lv.footer_selected {
|
if lv.footer_selected {
|
||||||
let filter = initialize_filter(lv, file, None);
|
let filter = initialize_filter(lv, None);
|
||||||
self.push_tab(Tab::CreateFilter { filter });
|
self.push_tab(Tab::CreateFilter { filter });
|
||||||
} else {
|
} else {
|
||||||
lv.enter(file)
|
lv.enter()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tab::Empty => {}
|
Tab::Empty => {}
|
||||||
Tab::CreateFilter { filter } => {
|
Tab::CreateFilter { filter } => {
|
||||||
if let FilterSelection::Confirm = filter.selection
|
if let FilterSelection::Confirm = filter.selection {
|
||||||
&& let Some(file) = &mut self.current_file
|
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()
|
||||||
{
|
{
|
||||||
if let Some(filter) = filter.validate() {
|
lv.add_filter(Rc::new(filter));
|
||||||
file.add_filter(filter);
|
|
||||||
self.pop_tab();
|
self.pop_tab();
|
||||||
|
|
||||||
if let Tab::LogViewer(lv) = self.current_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);
|
StatefulWidget::render(list, main_area, buf, state);
|
||||||
}
|
}
|
||||||
Tab::LogViewer(lv) => {
|
Tab::LogViewer(lv) => {
|
||||||
let Some(file) = &mut self.current_file else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
lv.update_num_items(main_area.height as usize);
|
lv.update_num_items(main_area.height as usize);
|
||||||
|
|
||||||
let (items, start, selected) = lv
|
let (items, selected_offset) = lv
|
||||||
.items(file, main_area.height as usize)
|
.items(main_area.height as usize)
|
||||||
.unwrap_or_else(|| (Vec::new(), 0, 0));
|
.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 list = List::new(items.into_iter().enumerate().map(|(idx, i)| {
|
||||||
let line = i.line_text(false);
|
let line = i.line_text(false);
|
||||||
|
|
||||||
let mut list_item = ListItem::new(line);
|
let mut list_item = ListItem::new(line);
|
||||||
|
|
||||||
if idx + start == selected {
|
if idx == selected_offset {
|
||||||
list_item = list_item.style(highlighted);
|
list_item = list_item.style(highlighted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,7 +439,7 @@ impl Widget for &mut App {
|
||||||
}));
|
}));
|
||||||
Widget::render(list, main_area, buf);
|
Widget::render(list, main_area, buf);
|
||||||
|
|
||||||
let items = lv.footer_fields(file);
|
let items = lv.footer_fields();
|
||||||
let width = 20;
|
let width = 20;
|
||||||
let builder = ListBuilder::new(|cx| {
|
let builder = ListBuilder::new(|cx| {
|
||||||
let Some((k, v)) = &items.get(cx.index) else {
|
let Some((k, v)) = &items.get(cx.index) else {
|
||||||
|
|
|
||||||
|
|
@ -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 jiff::Timestamp;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Hash)]
|
||||||
pub enum Level {
|
pub enum Level {
|
||||||
#[serde(rename = "TRACE")]
|
#[serde(rename = "TRACE")]
|
||||||
Trace,
|
Trace,
|
||||||
|
|
@ -33,6 +38,20 @@ pub enum LogEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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<Rc<LogEntry>> {
|
pub fn get(&self, index: usize) -> Option<Rc<LogEntry>> {
|
||||||
match self {
|
match self {
|
||||||
LogEntry::Single { .. } => None,
|
LogEntry::Single { .. } => None,
|
||||||
|
|
@ -89,7 +108,7 @@ impl LogEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone, Hash)]
|
||||||
pub struct LogFields {
|
pub struct LogFields {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub fields: BTreeMap<String, serde_json::Value>,
|
pub fields: BTreeMap<String, serde_json::Value>,
|
||||||
|
|
@ -112,7 +131,7 @@ impl LogFields {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Hash)]
|
||||||
pub struct RawLogEntry {
|
pub struct RawLogEntry {
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
pub level: Level,
|
pub level: Level,
|
||||||
|
|
|
||||||
170
src/tui/processing.rs
Normal file
170
src/tui/processing.rs
Normal file
|
|
@ -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<Self::Stream>;
|
||||||
|
fn from_start(self) -> Option<Self::Stream>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogEntryStream {
|
||||||
|
inner: Rc<LogEntry>,
|
||||||
|
curr: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogStream for LogEntryStream {
|
||||||
|
fn next(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
|
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<Rc<LogEntry>> {
|
||||||
|
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<dyn LogStream> {
|
||||||
|
Box::new(Self {
|
||||||
|
inner: Rc::clone(&self.inner),
|
||||||
|
curr: self.curr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoLogStream for &Rc<LogEntry> {
|
||||||
|
type Stream = LogEntryStream;
|
||||||
|
|
||||||
|
fn from_end(self) -> Option<Self::Stream> {
|
||||||
|
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<Self::Stream> {
|
||||||
|
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<Rc<LogEntry>>;
|
||||||
|
fn prev(&mut self) -> Option<Rc<LogEntry>>;
|
||||||
|
|
||||||
|
fn clone(&self) -> Box<dyn LogStream>;
|
||||||
|
|
||||||
|
fn filter(&self, filter: Rc<Filter>) -> FilteredLogStream {
|
||||||
|
FilteredLogStream {
|
||||||
|
filter: filter,
|
||||||
|
stack: vec![self.clone()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FilteredLogStream {
|
||||||
|
filter: Rc<Filter>,
|
||||||
|
stack: Vec<Box<dyn LogStream>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Rc<LogEntry>> {
|
||||||
|
generate_candidate!(self, next)
|
||||||
|
}
|
||||||
|
fn prev_candidate(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
|
generate_candidate!(self, prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogStream for FilteredLogStream {
|
||||||
|
fn next(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
|
generate_filter!(self, next_candidate, from_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
|
generate_filter!(self, prev_candidate, from_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone(&self) -> Box<dyn LogStream> {
|
||||||
|
Box::new(Self {
|
||||||
|
filter: Rc::clone(&self.filter),
|
||||||
|
stack: self
|
||||||
|
.stack
|
||||||
|
.iter()
|
||||||
|
.map(|i| LogStream::clone(i.as_ref()))
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, BufRead, BufReader},
|
io::{self, BufRead, BufReader},
|
||||||
mem,
|
mem,
|
||||||
|
|
@ -8,35 +9,36 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
filter::{Filter, FilterKind},
|
|
||||||
model::{LogEntry, RawLogEntry},
|
model::{LogEntry, RawLogEntry},
|
||||||
|
processing::LogStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct LogfileReader {
|
struct Inner {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
file: BufReader<File>,
|
|
||||||
|
|
||||||
entries: Vec<Rc<LogEntry>>,
|
file: BufReader<File>,
|
||||||
filters: Vec<Rc<Filter>>,
|
cached_entries: Vec<Rc<LogEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LogfileReader(Rc<RefCell<Inner>>);
|
||||||
|
|
||||||
impl LogfileReader {
|
impl LogfileReader {
|
||||||
pub fn new(p: &Path) -> io::Result<Self> {
|
pub fn new(p: &Path) -> io::Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self(Rc::new(RefCell::new(Inner {
|
||||||
file: BufReader::new(File::open(p)?),
|
file: BufReader::new(File::open(p)?),
|
||||||
path: p.to_path_buf(),
|
path: p.to_path_buf(),
|
||||||
entries: Vec::new(),
|
cached_entries: Vec::new(),
|
||||||
filters: Vec::new(),
|
}))))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_filter(&mut self, filter: Filter) {
|
pub fn path(&self) -> PathBuf {
|
||||||
self.filters.push(Rc::new(filter));
|
self.0.borrow().path.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_line(&mut self) -> Option<String> {
|
fn next_line(&mut self) -> Option<String> {
|
||||||
let mut res = String::new();
|
let mut res = String::new();
|
||||||
match self.file.read_line(&mut res) {
|
match self.0.borrow_mut().file.read_line(&mut res) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: {e:?}");
|
eprintln!("error: {e:?}");
|
||||||
None
|
None
|
||||||
|
|
@ -93,71 +95,51 @@ impl LogfileReader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_from(&mut self, start: usize) -> FilterAdapter<EntryIterator<'_>> {
|
fn fill_buf_to_access_index(&mut self, n: usize) -> Option<Rc<LogEntry>> {
|
||||||
FilterAdapter {
|
while self.0.borrow().cached_entries.len() <= n {
|
||||||
filters: self.filters.clone(),
|
|
||||||
inner: EntryIterator {
|
|
||||||
curr: start,
|
|
||||||
reader: self,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_next_entry(&mut self) -> Option<()> {
|
|
||||||
let entry = self.next_entry()?;
|
let entry = self.next_entry()?;
|
||||||
self.entries.push(entry);
|
self.0.borrow_mut().cached_entries.push(entry);
|
||||||
Some(())
|
}
|
||||||
|
|
||||||
|
Some(Rc::clone(&self.0.borrow().cached_entries[n]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> LogFileReaderStream {
|
||||||
|
LogFileReaderStream {
|
||||||
|
reader: self.clone(),
|
||||||
|
curr: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EntryIterator<'a> {
|
pub struct LogFileReaderStream {
|
||||||
|
reader: LogfileReader,
|
||||||
curr: usize,
|
curr: usize,
|
||||||
reader: &'a mut LogfileReader,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for EntryIterator<'a> {
|
impl LogStream for LogFileReaderStream {
|
||||||
type Item = Rc<LogEntry>;
|
fn next(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
|
let entry = self.reader.fill_buf_to_access_index(self.curr)?;
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
while self.reader.entries.len() <= self.curr {
|
|
||||||
self.reader.add_next_entry()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = Rc::clone(&self.reader.entries[self.curr]);
|
|
||||||
self.curr += 1;
|
self.curr += 1;
|
||||||
Some(res)
|
|
||||||
}
|
Some(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FilterAdapter<I> {
|
fn prev(&mut self) -> Option<Rc<LogEntry>> {
|
||||||
filters: Vec<Rc<Filter>>,
|
if self.curr == 0 {
|
||||||
inner: I,
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: IntoIterator> FilterAdapter<I> {
|
let entry = self.reader.fill_buf_to_access_index(self.curr)?;
|
||||||
pub fn new(file: &LogfileReader, inner: I) -> FilterAdapter<I::IntoIter> {
|
self.curr -= 1;
|
||||||
Self {
|
|
||||||
filters: file.filters.clone(),
|
Some(entry)
|
||||||
inner: inner.into_iter(),
|
}
|
||||||
}
|
|
||||||
}
|
fn clone(&self) -> Box<dyn LogStream> {
|
||||||
}
|
Box::new(Self {
|
||||||
|
reader: self.reader.clone(),
|
||||||
impl<I: Iterator> Iterator for FilterAdapter<I> {
|
curr: self.curr,
|
||||||
type Item = I::Item;
|
})
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue