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", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -532,7 +552,9 @@ name = "logsmash"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ahash", "ahash",
"base64",
"clap", "clap",
"derive_more",
"hdrhistogram", "hdrhistogram",
"itertools", "itertools",
"log", "log",

View file

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

View file

@ -1,8 +1,10 @@
use crate::logfile::LogFile;
use crate::logline::LogLine; use crate::logline::LogLine;
use crate::matcher::MatchResult; use crate::matcher::MatchResult;
use crate::timegraph::TimeGraph; use crate::timegraph::TimeGraph;
use logsmash_data::StatementList; use logsmash_data::StatementList;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Mutex;
use time::OffsetDateTime; use time::OffsetDateTime;
pub struct App { pub struct App {
@ -14,6 +16,7 @@ pub struct App {
pub error_count: usize, pub error_count: usize,
pub all: LogMatch, pub all: LogMatch,
pub unmatched: LogMatch, pub unmatched: LogMatch,
pub log_file: Mutex<LogFile>,
} }
impl App { impl App {
@ -25,6 +28,10 @@ impl App {
}; };
self.matches.len() + 1 + unmatched_line_count 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 { 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(); let mut map: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
for (i, line) in indices.map(|i| (i, &all_lines[i])) { 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 let mut list: Vec<_> = map

View file

@ -1,7 +1,7 @@
use crate::error::ReadError; use crate::error::ReadError;
use itertools::Either; use itertools::Either;
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader, Seek};
use zip::ZipArchive; use zip::ZipArchive;
pub enum LogFile { 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)] #[derive(Deserialize)]
pub struct LogLine { pub struct LogLine {
#[serde(default)]
pub index: usize,
pub version: TinyAsciiStr<16>, pub version: TinyAsciiStr<16>,
pub level: LogLevel, pub level: LogLevel,
pub message: String, pub message: String,
@ -17,20 +19,11 @@ pub struct LogLine {
} }
impl LogLine { impl LogLine {
pub fn index(&self) -> u64 { pub fn identity(&self) -> u64 {
let mut hasher = AHasher::default(); let mut hasher = AHasher::default();
self.message.hash(&mut hasher); self.message.hash(&mut hasher);
self.level.hash(&mut hasher); self.level.hash(&mut hasher);
self.exception self.exception.hash(&mut hasher);
.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.app.hash(&mut hasher); self.app.hash(&mut hasher);
hasher.finish() hasher.finish()
} }
@ -71,3 +64,12 @@ pub struct Exception {
pub line: usize, pub line: usize,
pub previous: Option<Box<Exception>>, 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::logline::LogLine;
use crate::matcher::{MatchResult, Matcher}; use crate::matcher::{MatchResult, Matcher};
use crate::ui::run_ui; use crate::ui::run_ui;
use base64::prelude::*;
use clap::Parser; use clap::Parser;
use logsmash_data::{default_apps, get_statements, SourceDefinition}; use logsmash_data::{default_apps, get_statements, SourceDefinition};
use main_error::MainResult; use main_error::MainResult;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::iter::once; use std::iter::once;
use std::sync::Mutex;
mod app; mod app;
mod error; mod error;
@ -58,25 +60,26 @@ fn main() -> MainResult {
let mut unmatched_counts: HashMap<String, Vec<usize>> = HashMap::new(); let mut unmatched_counts: HashMap<String, Vec<usize>> = HashMap::new();
let mut parsed_lines = Vec::with_capacity(1024); let mut parsed_lines = Vec::with_capacity(1024);
let mut unmatched_lines = Vec::with_capacity(256); let mut unmatched_lines = Vec::with_capacity(256);
let mut i = 0; let mut parsed_index = 0;
for line in lines { for (index, line) in lines.enumerate() {
if line.starts_with('{') { 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, Ok(parsed) => parsed,
Err(_) => { Err(_) => {
error_count += 1; error_count += 1;
continue; continue;
} }
}; };
parsed.index = index;
if let Some(index) = matcher.match_log(&parsed) { 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()) { } else if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_str()) {
entry.push(i) entry.push(parsed_index)
} else { } else {
unmatched_lines.push(i); unmatched_lines.push(parsed_index);
} }
parsed_lines.push(parsed); parsed_lines.push(parsed);
i += 1; parsed_index += 1;
} }
} }
@ -105,9 +108,14 @@ fn main() -> MainResult {
unmatched, unmatched,
all, all,
error_count, error_count,
log_file: Mutex::new(log_file),
}; };
run_ui(app)?; run_ui(app)?;
Ok(()) 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(), message: "Not allowed to rename a shared album".into(),
exception: None, exception: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
}) })
); );
assert_eq!( assert_eq!(
@ -174,6 +175,7 @@ fn test_matcher() {
message: "Not allowed to rename an album".into(), message: "Not allowed to rename an album".into(),
exception: None, exception: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
}) })
); );
assert_eq!( assert_eq!(
@ -185,6 +187,7 @@ fn test_matcher() {
message: "You are not allowed to edit link shares that you don't own".into(), message: "You are not allowed to edit link shares that you don't own".into(),
exception: None, exception: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
}) })
); );
assert_eq!( assert_eq!(
@ -196,6 +199,7 @@ fn test_matcher() {
message: "You are not allowed to edit link shares that you don't own".into(), message: "You are not allowed to edit link shares that you don't own".into(),
exception: None, exception: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
}) })
); );
assert_eq!( 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(), message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
exception: None, exception: None,
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
} }
) )
); );
@ -227,6 +232,7 @@ fn test_matcher() {
previous: None, previous: None,
}), }),
time: OffsetDateTime::now_utc(), time: OffsetDateTime::now_utc(),
index: 0,
} }
) )
); );

View file

@ -31,6 +31,7 @@ fn help(page: UiPage) -> &'static str {
match page { match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select", UiPage::MatchList => "«Q» Exit - «Enter» Select",
UiPage::Match => "«Q» Exit - «Enter» Select - «Esc» Back", 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::histogram::UiHistogram;
use crate::ui::match_list::match_list; use crate::ui::match_list::match_list;
use crate::ui::raw_logs::raw_logs; use crate::ui::raw_logs::raw_logs;
use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines; 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::event::{Event, KeyCode, KeyModifiers};
use ratatui::crossterm::terminal::{ use ratatui::crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
@ -21,6 +22,7 @@ mod footer;
mod histogram; mod histogram;
mod match_list; mod match_list;
mod raw_logs; mod raw_logs;
mod single_log;
mod single_match; mod single_match;
mod state; mod state;
pub mod style; pub mod style;
@ -58,7 +60,10 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
KeyCode::Up => Some(UiEvent::Up(1)), KeyCode::Up => Some(UiEvent::Up(1)),
KeyCode::PageDown => Some(UiEvent::Down(10)), KeyCode::PageDown => Some(UiEvent::Down(10)),
KeyCode::PageUp => Some(UiEvent::Up(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::Enter | KeyCode::Right => Some(UiEvent::Select),
KeyCode::Char('c') => Some(UiEvent::Copy),
_ => None, _ => None,
}); });
} }
@ -80,10 +85,10 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state { match state {
UiState::Quit => {} UiState::Quit => {}
UiState::MatchList { UiState::MatchList(MatchListState {
table_state, table_state,
scroll_state, scroll_state,
} => { }) => {
let selected = table_state.selected().unwrap_or(0); let selected = table_state.selected().unwrap_or(0);
let histogram = if selected == 0 { let histogram = if selected == 0 {
&app.all.histogram &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]); frame.render_widget(footer(app, page), layout[2]);
} }
UiState::Match { UiState::Match(MatchState {
result, result,
table_state, table_state,
scroll_state, scroll_state,
.. ..
} => { }) => {
let selected_group = &result.grouped[table_state.selected().unwrap_or_default()]; let selected_group = &result.grouped[table_state.selected().unwrap_or_default()];
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("")) .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]); frame.render_widget(footer(app, page), layout[2]);
} }
UiState::Logs { UiState::Logs(LogsState {
lines, lines,
table_state, table_state,
scroll_state, scroll_state,
.. ..
} => { }) => {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("")) .begin_symbol(Some(""))
.end_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]); 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::app::{App, LogMatch};
use crate::copy_osc;
use crate::logline::LogLine;
use derive_more::From;
use ratatui::widgets::{ScrollbarState, TableState}; use ratatui::widgets::{ScrollbarState, TableState};
use table_state::TableStateExt; use table_state::TableStateExt;
#[derive(Clone)] #[derive(Clone, From)]
pub enum UiState<'a> { pub enum UiState<'a> {
MatchList { MatchList(MatchListState),
table_state: TableState, Match(MatchState<'a>),
scroll_state: ScrollbarState, Logs(LogsState<'a>),
}, Log(LogState<'a>),
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>>,
},
Quit, 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> { impl<'a> UiState<'a> {
pub fn new(app: &App) -> Self { pub fn new(app: &App) -> Self {
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
UiState::MatchList { UiState::MatchList(MatchListState {
table_state, table_state,
scroll_state: ScrollbarState::new(app.match_lines()), scroll_state: ScrollbarState::new(app.match_lines()),
} })
} }
pub fn page(&self) -> UiPage { pub fn page(&self) -> UiPage {
match self { match self {
UiState::Quit | UiState::MatchList { .. } => UiPage::MatchList, UiState::Quit | UiState::MatchList(_) => UiPage::MatchList,
UiState::Match { .. } => UiPage::Match, UiState::Match(_) => UiPage::Match,
UiState::Logs { .. } => UiPage::Logs, UiState::Logs(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log,
} }
} }
fn table_state(&mut self) -> Option<&mut TableState> { fn table_state(&mut self) -> Option<&mut TableState> {
match self { match self {
UiState::MatchList { table_state, .. } => Some(table_state), UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match { table_state, .. } => Some(table_state), UiState::Match(state) => Some(&mut state.table_state),
UiState::Logs { table_state, .. } => Some(table_state), UiState::Logs(state) => Some(&mut state.table_state),
UiState::Quit => None, _ => None,
} }
} }
fn scroll_state(&mut self) -> Option<&mut ScrollbarState> { fn scroll_state(&mut self) -> Option<&mut ScrollbarState> {
match self { match self {
UiState::MatchList { scroll_state, .. } => Some(scroll_state), UiState::MatchList(state) => Some(&mut state.scroll_state),
UiState::Match { scroll_state, .. } => Some(scroll_state), UiState::Match(state) => Some(&mut state.scroll_state),
UiState::Logs { scroll_state, .. } => Some(scroll_state), UiState::Logs(state) => Some(&mut state.scroll_state),
UiState::Quit => None, _ => None,
} }
} }
fn table_count(&self, app: &App) -> usize { fn row_count(&self, app: &App) -> usize {
match self { match self {
UiState::MatchList { .. } => app.match_lines(), UiState::MatchList(_) => app.match_lines(),
UiState::Match { result, .. } => result.grouped.len(), UiState::Match(state) => state.result.grouped.len(),
UiState::Logs { lines, .. } => lines.len(), UiState::Logs(state) => state.lines.len(),
UiState::Quit => 0, _ => 0,
} }
} }
@ -72,9 +110,9 @@ impl<'a> UiState<'a> {
match (self, event) { match (self, event) {
(UiState::Quit, _) => UiState::Quit, (UiState::Quit, _) => UiState::Quit,
(_, UiEvent::Quit) => UiState::Quit, (_, UiEvent::Quit) => UiState::Quit,
(UiState::MatchList { .. }, UiEvent::Back) => UiState::Quit, (UiState::MatchList(_), UiEvent::Back) => UiState::Quit,
(mut state, UiEvent::Down(step)) => { (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() { if let Some(table_state) = state.table_state() {
let pos = table_state.down(count, step); let pos = table_state.down(count, step);
let scroll_state = state.scroll_state().unwrap(); let scroll_state = state.scroll_state().unwrap();
@ -83,7 +121,7 @@ impl<'a> UiState<'a> {
state state
} }
(mut state, UiEvent::Up(step)) => { (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() { if let Some(table_state) = state.table_state() {
let pos = table_state.up(count, step); let pos = table_state.up(count, step);
let scroll_state = state.scroll_state().unwrap(); let scroll_state = state.scroll_state().unwrap();
@ -91,14 +129,8 @@ impl<'a> UiState<'a> {
} }
state state
} }
( (UiState::MatchList(state), UiEvent::Select) => {
UiState::MatchList { let selected = state.selected();
table_state: prev_state,
scroll_state: prev_scroll,
},
UiEvent::Select,
) => {
let selected = prev_state.selected().unwrap_or(0);
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
@ -109,45 +141,59 @@ impl<'a> UiState<'a> {
} else { } else {
&app.matches[selected - 1] &app.matches[selected - 1]
}; };
UiState::Match { UiState::Match(MatchState {
result, result,
table_state, table_state,
scroll_state: ScrollbarState::new(result.count()), scroll_state: ScrollbarState::new(result.count()),
previous: Box::new(UiState::MatchList { previous: Box::new(state.into()),
table_state: prev_state, })
scroll_state: prev_scroll,
}),
} }
} (UiState::Match(state), UiEvent::Select) => {
( let selected = state.selected();
UiState::Match {
table_state: prev_state,
scroll_state: prev_scroll,
previous,
result,
},
UiEvent::Select,
) => {
let selected = prev_state.selected().unwrap_or(0);
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
let lines = result.grouped[selected].lines.as_slice(); let lines = state.result.grouped[selected].lines.as_slice();
UiState::Logs { UiState::Logs(LogsState {
lines, lines,
table_state, table_state,
scroll_state: ScrollbarState::new(lines.len()), scroll_state: ScrollbarState::new(lines.len()),
previous: Box::new(UiState::Match { previous: Box::new(state.into()),
table_state: prev_state, })
scroll_state: prev_scroll,
previous,
result,
}),
} }
(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::Match { previous, .. } | UiState::Logs { previous, .. }, UiEvent::Back) => { (UiState::Logs(state), UiEvent::Copy) => {
*previous 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, (state, _) => state,
} }
} }
@ -159,12 +205,14 @@ pub enum UiEvent {
Up(usize), Up(usize),
Down(usize), Down(usize),
Select, Select,
Copy,
} }
pub enum UiPage { pub enum UiPage {
MatchList, MatchList,
Match, Match,
Logs, Logs,
Log,
} }
mod table_state { mod table_state {