summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2022-03-08 19:58:53 -0500
committerJesse Luehrs <doy@tozt.net>2022-03-08 20:00:46 -0500
commit3a082edd58ecdf4db31702c6c8487c42227049a3 (patch)
tree6cdb3a7b635da5b06e82fc91d7ddba618822843a
parent1abb97cca69e0b513f7499cc7db03ba622b38711 (diff)
downloadnbsh-3a082edd58ecdf4db31702c6c8487c42227049a3.tar.gz
nbsh-3a082edd58ecdf4db31702c6c8487c42227049a3.zip
parse a history file if available
doesn't do anything with it yet
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml5
-rw-r--r--src/dirs.rs17
-rw-r--r--src/history.pest5
-rw-r--r--src/shell/mod.rs3
-rw-r--r--src/shell/old_history.rs185
6 files changed, 218 insertions, 4 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2137155..dbd3a93 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1472,9 +1472,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<directories::ProjectDirs> =
+ 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<inputs::GitInfo>,
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<std::sync::Mutex<Vec<Entry>>>,
+}
+
+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<std::sync::Mutex<Vec<Entry>>>) {
+ // 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<time::OffsetDateTime>,
+ duration: Option<std::time::Duration>,
+}
+
+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<Self, Self::Err> {
+ 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);
+}