From 3a082edd58ecdf4db31702c6c8487c42227049a3 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Tue, 8 Mar 2022 19:58:53 -0500 Subject: parse a history file if available doesn't do anything with it yet --- Cargo.lock | 7 ++ Cargo.toml | 5 +- src/dirs.rs | 17 ++++- src/history.pest | 5 ++ src/shell/mod.rs | 3 + src/shell/old_history.rs | 185 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 src/history.pest create mode 100644 src/shell/old_history.rs diff --git a/Cargo.lock b/Cargo.lock index 2137155..dbd3a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1472,8 +1472,15 @@ dependencies = [ "itoa", "libc", "num_threads", + "time-macros", ] +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/Cargo.toml b/Cargo.toml index 3a2f0d9..89b201e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ terminal_size = "0.1.17" textmode = { version = "0.3.0", features = ["async"] } time = { version = "0.3.7", features = ["formatting", "parsing"] } tokio = { version = "1.17.0", features = ["full"] } -tokio-stream = "0.1.8" +tokio-stream = { version = "0.1.8", features = ["io-util"] } tokio-util = { version = "0.7.0", features = ["io"] } toml = "0.5.8" unicode-width = "0.1.9" @@ -42,3 +42,6 @@ nix = { git = "https://github.com/nix-rust/nix" } notify = { git = "https://github.com/notify-rs/notify" } pty-process = { git = "https://github.com/doy/pty-process" } textmode = { git = "https://github.com/doy/textmode" } + +[dev-dependencies] +time = { version = "0.3.7", features = ["macros"] } diff --git a/src/dirs.rs b/src/dirs.rs index 45674e3..2ffbb33 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -1,9 +1,20 @@ +static PROJECT_DIRS: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| { + directories::ProjectDirs::from("", "", "nbsh").unwrap() + }); + pub fn config_file() -> std::path::PathBuf { config_dir().join("config.toml") } +pub fn history_file() -> std::path::PathBuf { + data_dir().join("history") +} + fn config_dir() -> std::path::PathBuf { - let project_dirs = - directories::ProjectDirs::from("", "", "nbsh").unwrap(); - project_dirs.config_dir().to_path_buf() + PROJECT_DIRS.config_dir().to_path_buf() +} + +fn data_dir() -> std::path::PathBuf { + PROJECT_DIRS.data_dir().to_path_buf() } diff --git a/src/history.pest b/src/history.pest new file mode 100644 index 0000000..67597d1 --- /dev/null +++ b/src/history.pest @@ -0,0 +1,5 @@ +time = @{ ASCII_DIGIT+ } +duration = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? } +command = @{ ANY* } + +line = ${ SOI ~ (": " ~ time ~ ":" ~ duration ~ ";")? ~ command ~ "\n"? ~ EOI } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 9befd99..4a5c5d2 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -5,6 +5,7 @@ use textmode::Textmode as _; mod event; mod history; mod inputs; +mod old_history; mod prelude; mod readline; @@ -75,6 +76,7 @@ pub enum Action { pub struct Shell { readline: readline::Readline, history: history::History, + old_history: old_history::History, env: Env, git: Option, focus: Focus, @@ -92,6 +94,7 @@ impl Shell { Ok(Self { readline: readline::Readline::new(), history: history::History::new(), + old_history: old_history::History::new(), env, git: None, focus: Focus::Readline, diff --git a/src/shell/old_history.rs b/src/shell/old_history.rs new file mode 100644 index 0000000..49fd1c2 --- /dev/null +++ b/src/shell/old_history.rs @@ -0,0 +1,185 @@ +use crate::shell::prelude::*; + +use tokio::io::AsyncBufReadExt as _; + +use pest::Parser as _; + +#[derive(pest_derive::Parser)] +#[grammar = "history.pest"] +struct HistoryLine; + +pub struct History { + entries: std::sync::Arc>>, +} + +impl History { + pub fn new() -> Self { + let entries = std::sync::Arc::new(std::sync::Mutex::new(vec![])); + tokio::spawn(Self::task(std::sync::Arc::clone(&entries))); + Self { entries } + } + + pub fn entry_count(&self) -> usize { + self.entries.lock().unwrap().len() + } + + async fn task(entries: std::sync::Arc>>) { + // TODO: we should actually read this in reverse order, because we + // want to populate the most recent entries first + let mut stream = tokio_stream::wrappers::LinesStream::new( + tokio::io::BufReader::new( + tokio::fs::File::open(crate::dirs::history_file()) + .await + .unwrap(), + ) + .lines(), + ); + while let Some(line) = stream.next().await { + let line = if let Ok(line) = line { + line + } else { + continue; + }; + let entry = if let Ok(entry) = line.parse() { + entry + } else { + continue; + }; + entries.lock().unwrap().push(entry); + } + } +} + +pub struct Entry { + cmdline: String, + start_time: Option, + duration: Option, +} + +impl Entry { + pub fn render( + &self, + out: &mut impl textmode::Textmode, + offset: time::UtcOffset, + ) { + let size = out.screen().size(); + let mut time = "".to_string(); + if let Some(duration) = self.duration { + time.push_str(&crate::format::duration(duration)); + } + if let Some(start_time) = self.start_time { + time.push_str(&crate::format::time(start_time.to_offset(offset))); + } + + out.write_str(" $ "); + let start = usize::from(out.screen().cursor_position().1); + let end = usize::from(size.1) - time.len() - 2; + let max_len = end - start; + let cmd = if self.cmdline.len() > max_len { + &self.cmdline[..(max_len - 4)] + } else { + &self.cmdline + }; + out.write_str(cmd); + if self.cmdline.len() > max_len { + out.write_str(" "); + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + } + out.reset_attributes(); + + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + let cur_pos = out.screen().cursor_position(); + out.write_str(&" ".repeat( + usize::from(size.1) - time.len() - 1 - usize::from(cur_pos.1), + )); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + } + + pub fn cmd(&self) -> &str { + &self.cmdline + } +} + +impl std::str::FromStr for Entry { + type Err = anyhow::Error; + + fn from_str(line: &str) -> std::result::Result { + let mut parsed = + HistoryLine::parse(Rule::line, line).map_err(|e| anyhow!(e))?; + let line = parsed.next().unwrap(); + assert!(matches!(line.as_rule(), Rule::line)); + + let mut start_time = None; + let mut duration = None; + let mut cmdline = None; + for part in line.into_inner() { + match part.as_rule() { + Rule::time => { + start_time = + Some(time::OffsetDateTime::from_unix_timestamp( + part.as_str().parse()?, + )?); + } + Rule::duration => { + if part.as_str() == "0" { + continue; + } + let mut dur_parts = part.as_str().split('.'); + let secs: u64 = dur_parts.next().unwrap().parse()?; + let nsec_str = dur_parts.next().unwrap_or("0"); + let nsec_str = &nsec_str[..9.min(nsec_str.len())]; + let nsecs: u64 = nsec_str.parse()?; + duration = Some(std::time::Duration::from_nanos( + secs * 1_000_000_000 + + nsecs + * (10u64.pow( + (9 - nsec_str.len()).try_into().unwrap(), + )), + )); + } + Rule::command => { + cmdline = Some(part.as_str().to_string()); + } + Rule::line => unreachable!(), + Rule::EOI => break, + } + } + + Ok(Self { + cmdline: cmdline.unwrap(), + start_time, + duration, + }) + } +} + +#[test] +fn test_parse() { + let entry: Entry = + ": 1646779848:1234.56;vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!( + entry.duration, + Some(std::time::Duration::from_nanos(1_234_560_000_000)) + ); + assert_eq!( + entry.start_time, + Some(time::macros::datetime!(2022-03-08 22:50:48).assume_utc()) + ); + + let entry: Entry = ": 1646779848:1;vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!(entry.duration, Some(std::time::Duration::from_secs(1))); + assert_eq!( + entry.start_time, + Some(time::macros::datetime!(2022-03-08 22:50:48).assume_utc()) + ); + + let entry: Entry = "vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!(entry.duration, None); + assert_eq!(entry.start_time, None); +} -- cgit v1.2.3-54-g00ecf