add copy keybind

This commit is contained in:
Robin Appelman 2024-07-27 14:57:00 +02:00
commit 5b86279b46
11 changed files with 231 additions and 101 deletions

22
Cargo.lock generated
View file

@ -329,6 +329,26 @@ dependencies = [
"syn 2.0.71",
]
[[package]]
name = "derive_more"
version = "1.0.0-beta.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7abbfc297053be59290e3152f8cbcd52c8642e0728b69ee187d991d4c1af08d"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0-beta.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.71",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -532,7 +552,9 @@ name = "logsmash"
version = "0.1.0"
dependencies = [
"ahash",
"base64",
"clap",
"derive_more",
"hdrhistogram",
"itertools",
"log",

View file

@ -20,6 +20,8 @@ tinystr = { version = "0.7.6", features = ["serde"] }
time = { version = "0.3.36", features = ["serde", "serde-well-known"] }
hdrhistogram = "7.5.4"
ahash = "0.8.11"
base64 = "0.21.7"
derive_more = { version = "1.0.0-beta.6", features = ["from"] }
[profile.dev.package."*"]
opt-level = 3

View file

@ -1,8 +1,10 @@
use crate::logfile::LogFile;
use crate::logline::LogLine;
use crate::matcher::MatchResult;
use crate::timegraph::TimeGraph;
use logsmash_data::StatementList;
use std::collections::BTreeMap;
use std::sync::Mutex;
use time::OffsetDateTime;
pub struct App {
@ -14,6 +16,7 @@ pub struct App {
pub error_count: usize,
pub all: LogMatch,
pub unmatched: LogMatch,
pub log_file: Mutex<LogFile>,
}
impl App {
@ -25,6 +28,10 @@ impl App {
};
self.matches.len() + 1 + unmatched_line_count
}
pub fn get_line(&self, index: usize) -> Option<String> {
self.log_file.lock().unwrap().nth(index)
}
}
pub struct LogMatch {
@ -63,7 +70,7 @@ fn group_lines<I: Iterator<Item = usize>>(all_lines: &[LogLine], indices: I) ->
let mut map: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
for (i, line) in indices.map(|i| (i, &all_lines[i])) {
map.entry(line.index()).or_default().push(i);
map.entry(line.identity()).or_default().push(i);
}
let mut list: Vec<_> = map

View file

@ -1,7 +1,7 @@
use crate::error::ReadError;
use itertools::Either;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Seek};
use zip::ZipArchive;
pub enum LogFile {
@ -37,4 +37,22 @@ impl LogFile {
}
}
}
pub fn nth(&mut self, index: usize) -> Option<String> {
match self {
LogFile::Plain(file) => {
file.rewind().unwrap();
file.lines().nth(index).transpose().ok().flatten()
}
LogFile::Zip(zip) => {
let file = zip.by_index(0).expect("failed to open zip content again");
BufReader::new(file)
.lines()
.nth(index)
.transpose()
.ok()
.flatten()
}
}
}
}

View file

@ -7,6 +7,8 @@ use tinystr::TinyAsciiStr;
#[derive(Deserialize)]
pub struct LogLine {
#[serde(default)]
pub index: usize,
pub version: TinyAsciiStr<16>,
pub level: LogLevel,
pub message: String,
@ -17,20 +19,11 @@ pub struct LogLine {
}
impl LogLine {
pub fn index(&self) -> u64 {
pub fn identity(&self) -> u64 {
let mut hasher = AHasher::default();
self.message.hash(&mut hasher);
self.level.hash(&mut hasher);
self.exception
.as_ref()
.map(|e| e.exception.as_str())
.hash(&mut hasher);
self.exception
.as_ref()
.map(|e| e.file.as_str())
.hash(&mut hasher);
self.app.hash(&mut hasher);
self.exception.as_ref().map(|e| e.line).hash(&mut hasher);
self.exception.hash(&mut hasher);
self.app.hash(&mut hasher);
hasher.finish()
}
@ -71,3 +64,12 @@ pub struct Exception {
pub line: usize,
pub previous: Option<Box<Exception>>,
}
impl Hash for Exception {
fn hash<H: Hasher>(&self, state: &mut H) {
self.message.hash(state);
self.exception.hash(state);
self.file.hash(state);
self.line.hash(state);
}
}

View file

@ -4,12 +4,14 @@ use crate::logfile::LogFile;
use crate::logline::LogLine;
use crate::matcher::{MatchResult, Matcher};
use crate::ui::run_ui;
use base64::prelude::*;
use clap::Parser;
use logsmash_data::{default_apps, get_statements, SourceDefinition};
use main_error::MainResult;
use std::borrow::Cow;
use std::collections::HashMap;
use std::iter::once;
use std::sync::Mutex;
mod app;
mod error;
@ -58,25 +60,26 @@ fn main() -> MainResult {
let mut unmatched_counts: HashMap<String, Vec<usize>> = HashMap::new();
let mut parsed_lines = Vec::with_capacity(1024);
let mut unmatched_lines = Vec::with_capacity(256);
let mut i = 0;
for line in lines {
let mut parsed_index = 0;
for (index, line) in lines.enumerate() {
if line.starts_with('{') {
let parsed = match serde_json::from_str::<LogLine>(&line) {
let mut parsed = match serde_json::from_str::<LogLine>(&line) {
Ok(parsed) => parsed,
Err(_) => {
error_count += 1;
continue;
}
};
parsed.index = index;
if let Some(index) = matcher.match_log(&parsed) {
counts.entry(index).or_default().push(i);
counts.entry(index).or_default().push(parsed_index);
} else if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_str()) {
entry.push(i)
entry.push(parsed_index)
} else {
unmatched_lines.push(i);
unmatched_lines.push(parsed_index);
}
parsed_lines.push(parsed);
i += 1;
parsed_index += 1;
}
}
@ -105,9 +108,14 @@ fn main() -> MainResult {
unmatched,
all,
error_count,
log_file: Mutex::new(log_file),
};
run_ui(app)?;
Ok(())
}
fn copy_osc(text: &str) {
print!("\x1B]52;c;{}\x07", BASE64_STANDARD.encode(text))
}

View file

@ -163,6 +163,7 @@ fn test_matcher() {
message: "Not allowed to rename a shared album".into(),
exception: None,
time: OffsetDateTime::now_utc(),
index: 0,
})
);
assert_eq!(
@ -174,6 +175,7 @@ fn test_matcher() {
message: "Not allowed to rename an album".into(),
exception: None,
time: OffsetDateTime::now_utc(),
index: 0,
})
);
assert_eq!(
@ -185,6 +187,7 @@ fn test_matcher() {
message: "You are not allowed to edit link shares that you don't own".into(),
exception: None,
time: OffsetDateTime::now_utc(),
index: 0,
})
);
assert_eq!(
@ -196,6 +199,7 @@ fn test_matcher() {
message: "You are not allowed to edit link shares that you don't own".into(),
exception: None,
time: OffsetDateTime::now_utc(),
index: 0,
})
);
assert_eq!(
@ -208,6 +212,7 @@ fn test_matcher() {
message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
exception: None,
time: OffsetDateTime::now_utc(),
index: 0,
}
)
);
@ -227,6 +232,7 @@ fn test_matcher() {
previous: None,
}),
time: OffsetDateTime::now_utc(),
index: 0,
}
)
);

View file

@ -31,6 +31,7 @@ fn help(page: UiPage) -> &'static str {
match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select",
UiPage::Match => "«Q» Exit - «Enter» Select - «Esc» Back",
UiPage::Logs => "«Q» Exit - «Esc» Back",
UiPage::Logs => "«Q» Exit - «Esc» Back - «C» Copy log line",
UiPage::Log => "«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line",
}
}

View file

@ -4,8 +4,9 @@ use crate::ui::footer::footer;
use crate::ui::histogram::UiHistogram;
use crate::ui::match_list::match_list;
use crate::ui::raw_logs::raw_logs;
use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines;
use crate::ui::state::{UiEvent, UiState};
use crate::ui::state::{LogState, LogsState, MatchListState, MatchState, UiEvent, UiState};
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
use ratatui::crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
@ -21,6 +22,7 @@ mod footer;
mod histogram;
mod match_list;
mod raw_logs;
mod single_log;
mod single_match;
mod state;
pub mod style;
@ -58,7 +60,10 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
KeyCode::Up => Some(UiEvent::Up(1)),
KeyCode::PageDown => Some(UiEvent::Down(10)),
KeyCode::PageUp => Some(UiEvent::Up(10)),
KeyCode::End => Some(UiEvent::Down(usize::MAX)),
KeyCode::Home => Some(UiEvent::Up(usize::MAX)),
KeyCode::Enter | KeyCode::Right => Some(UiEvent::Select),
KeyCode::Char('c') => Some(UiEvent::Copy),
_ => None,
});
}
@ -80,10 +85,10 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state {
UiState::Quit => {}
UiState::MatchList {
UiState::MatchList(MatchListState {
table_state,
scroll_state,
} => {
}) => {
let selected = table_state.selected().unwrap_or(0);
let histogram = if selected == 0 {
&app.all.histogram
@ -113,12 +118,12 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
);
frame.render_widget(footer(app, page), layout[2]);
}
UiState::Match {
UiState::Match(MatchState {
result,
table_state,
scroll_state,
..
} => {
}) => {
let selected_group = &result.grouped[table_state.selected().unwrap_or_default()];
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
@ -140,12 +145,12 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
);
frame.render_widget(footer(app, page), layout[2]);
}
UiState::Logs {
UiState::Logs(LogsState {
lines,
table_state,
scroll_state,
..
} => {
}) => {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
@ -164,5 +169,9 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
);
frame.render_widget(footer(app, page), layout[2]);
}
UiState::Log(LogState { log, .. }) => {
frame.render_widget(single_log(app, log), layout[0].union(layout[1]));
frame.render_widget(footer(app, page), layout[2]);
}
}
}

7
src/ui/single_log.rs Normal file
View file

@ -0,0 +1,7 @@
use crate::app::App;
use crate::logline::LogLine;
use ratatui::widgets::{Paragraph, Wrap};
pub fn single_log<'a>(_app: &App, line: &'a LogLine) -> Paragraph<'a> {
Paragraph::new(line.display()).wrap(Wrap::default())
}

View file

@ -1,70 +1,108 @@
use crate::app::{App, LogMatch};
use crate::copy_osc;
use crate::logline::LogLine;
use derive_more::From;
use ratatui::widgets::{ScrollbarState, TableState};
use table_state::TableStateExt;
#[derive(Clone)]
#[derive(Clone, From)]
pub enum UiState<'a> {
MatchList {
table_state: TableState,
scroll_state: ScrollbarState,
},
Match {
result: &'a LogMatch,
table_state: TableState,
scroll_state: ScrollbarState,
previous: Box<UiState<'a>>,
},
Logs {
lines: &'a [usize],
table_state: TableState,
scroll_state: ScrollbarState,
previous: Box<UiState<'a>>,
},
MatchList(MatchListState),
Match(MatchState<'a>),
Logs(LogsState<'a>),
Log(LogState<'a>),
Quit,
}
#[derive(Clone)]
pub struct MatchListState {
pub table_state: TableState,
pub scroll_state: ScrollbarState,
}
impl MatchListState {
fn selected(&self) -> usize {
self.table_state.selected().unwrap()
}
}
#[derive(Clone)]
pub struct MatchState<'a> {
pub result: &'a LogMatch,
pub table_state: TableState,
pub scroll_state: ScrollbarState,
pub previous: Box<UiState<'a>>,
}
impl<'a> MatchState<'a> {
fn selected(&self) -> usize {
self.table_state.selected().unwrap()
}
}
#[derive(Clone)]
pub struct LogsState<'a> {
pub lines: &'a [usize],
pub table_state: TableState,
pub scroll_state: ScrollbarState,
pub previous: Box<UiState<'a>>,
}
impl<'a> LogsState<'a> {
fn selected(&self) -> usize {
self.table_state.selected().unwrap()
}
}
#[derive(Clone)]
pub struct LogState<'a> {
pub log: &'a LogLine,
pub previous: Box<UiState<'a>>,
}
impl<'a> UiState<'a> {
pub fn new(app: &App) -> Self {
let mut table_state = TableState::default();
table_state.select(Some(0));
UiState::MatchList {
UiState::MatchList(MatchListState {
table_state,
scroll_state: ScrollbarState::new(app.match_lines()),
}
})
}
pub fn page(&self) -> UiPage {
match self {
UiState::Quit | UiState::MatchList { .. } => UiPage::MatchList,
UiState::Match { .. } => UiPage::Match,
UiState::Logs { .. } => UiPage::Logs,
UiState::Quit | UiState::MatchList(_) => UiPage::MatchList,
UiState::Match(_) => UiPage::Match,
UiState::Logs(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log,
}
}
fn table_state(&mut self) -> Option<&mut TableState> {
match self {
UiState::MatchList { table_state, .. } => Some(table_state),
UiState::Match { table_state, .. } => Some(table_state),
UiState::Logs { table_state, .. } => Some(table_state),
UiState::Quit => None,
UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state),
UiState::Logs(state) => Some(&mut state.table_state),
_ => None,
}
}
fn scroll_state(&mut self) -> Option<&mut ScrollbarState> {
match self {
UiState::MatchList { scroll_state, .. } => Some(scroll_state),
UiState::Match { scroll_state, .. } => Some(scroll_state),
UiState::Logs { scroll_state, .. } => Some(scroll_state),
UiState::Quit => None,
UiState::MatchList(state) => Some(&mut state.scroll_state),
UiState::Match(state) => Some(&mut state.scroll_state),
UiState::Logs(state) => Some(&mut state.scroll_state),
_ => None,
}
}
fn table_count(&self, app: &App) -> usize {
fn row_count(&self, app: &App) -> usize {
match self {
UiState::MatchList { .. } => app.match_lines(),
UiState::Match { result, .. } => result.grouped.len(),
UiState::Logs { lines, .. } => lines.len(),
UiState::Quit => 0,
UiState::MatchList(_) => app.match_lines(),
UiState::Match(state) => state.result.grouped.len(),
UiState::Logs(state) => state.lines.len(),
_ => 0,
}
}
@ -72,9 +110,9 @@ impl<'a> UiState<'a> {
match (self, event) {
(UiState::Quit, _) => UiState::Quit,
(_, UiEvent::Quit) => UiState::Quit,
(UiState::MatchList { .. }, UiEvent::Back) => UiState::Quit,
(UiState::MatchList(_), UiEvent::Back) => UiState::Quit,
(mut state, UiEvent::Down(step)) => {
let count = state.table_count(app);
let count = state.row_count(app);
if let Some(table_state) = state.table_state() {
let pos = table_state.down(count, step);
let scroll_state = state.scroll_state().unwrap();
@ -83,7 +121,7 @@ impl<'a> UiState<'a> {
state
}
(mut state, UiEvent::Up(step)) => {
let count = state.table_count(app);
let count = state.row_count(app);
if let Some(table_state) = state.table_state() {
let pos = table_state.up(count, step);
let scroll_state = state.scroll_state().unwrap();
@ -91,14 +129,8 @@ impl<'a> UiState<'a> {
}
state
}
(
UiState::MatchList {
table_state: prev_state,
scroll_state: prev_scroll,
},
UiEvent::Select,
) => {
let selected = prev_state.selected().unwrap_or(0);
(UiState::MatchList(state), UiEvent::Select) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
@ -109,45 +141,59 @@ impl<'a> UiState<'a> {
} else {
&app.matches[selected - 1]
};
UiState::Match {
UiState::Match(MatchState {
result,
table_state,
scroll_state: ScrollbarState::new(result.count()),
previous: Box::new(UiState::MatchList {
table_state: prev_state,
scroll_state: prev_scroll,
}),
}
previous: Box::new(state.into()),
})
}
(
UiState::Match {
table_state: prev_state,
scroll_state: prev_scroll,
previous,
result,
},
UiEvent::Select,
) => {
let selected = prev_state.selected().unwrap_or(0);
(UiState::Match(state), UiEvent::Select) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let lines = result.grouped[selected].lines.as_slice();
UiState::Logs {
let lines = state.result.grouped[selected].lines.as_slice();
UiState::Logs(LogsState {
lines,
table_state,
scroll_state: ScrollbarState::new(lines.len()),
previous: Box::new(UiState::Match {
table_state: prev_state,
scroll_state: prev_scroll,
previous,
result,
}),
}
previous: Box::new(state.into()),
})
}
(UiState::Match { previous, .. } | UiState::Logs { previous, .. }, UiEvent::Back) => {
*previous
(UiState::Logs(state), UiEvent::Select) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let line = state.lines[selected];
let log = &app.lines[line];
UiState::Log(LogState {
log,
previous: Box::new(state.into()),
})
}
(UiState::Logs(state), UiEvent::Copy) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
let line = &app.lines[state.lines[selected]];
let raw = app.get_line(line.index).unwrap_or_default();
copy_osc(&raw);
UiState::Logs(state)
}
(UiState::Log(state), UiEvent::Copy) => {
let raw = app.get_line(state.log.index).unwrap_or_default();
copy_osc(&raw);
UiState::Log(state)
}
(
UiState::Match(MatchState { previous, .. })
| UiState::Logs(LogsState { previous, .. })
| UiState::Log(LogState { previous, .. }),
UiEvent::Back,
) => *previous,
(state, _) => state,
}
}
@ -159,12 +205,14 @@ pub enum UiEvent {
Up(usize),
Down(usize),
Select,
Copy,
}
pub enum UiPage {
MatchList,
Match,
Logs,
Log,
}
mod table_state {