diff options
| author | Jonas Maier <> | 2026-03-07 20:49:52 +0100 |
|---|---|---|
| committer | Jonas Maier <> | 2026-03-07 20:49:52 +0100 |
| commit | df5eec13a031d41232e7407794e2f5b9a0a2d608 (patch) | |
| tree | a76fba2ed68928d82d9b8352bfd0afc33d118fb0 | |
| parent | 86cdb8c21dec737a3f0a40311782de851c1203d1 (diff) | |
| download | pish-df5eec13a031d41232e7407794e2f5b9a0a2d608.tar.gz | |
history with relative time
| -rw-r--r-- | src/date.rs | 113 | ||||
| -rw-r--r-- | src/main.rs | 26 | ||||
| -rw-r--r-- | src/run/builtin.rs | 18 |
3 files changed, 144 insertions, 13 deletions
diff --git a/src/date.rs b/src/date.rs new file mode 100644 index 0000000..149dc79 --- /dev/null +++ b/src/date.rs @@ -0,0 +1,113 @@ +use core::fmt; +use std::{ + process::Command, + time::{Duration, SystemTime}, +}; + +use crate::BString; + +#[derive(Clone)] +pub struct DateTime { + sys: SystemTime, +} + +impl DateTime { + pub fn from_unix(unix: u64) -> Self { + Self { + sys: SystemTime::UNIX_EPOCH + Duration::from_secs(unix), + } + } + pub fn now() -> Self { + Self { + sys: SystemTime::now(), + } + } + fn format(&self) -> std::io::Result<BString> { + let mut out = Command::new("date") + .arg("-d") + .arg(format!( + "@{}", + self.sys + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + )) + .output()? + .stdout; + while let Some(x) = out.last() + && x.is_ascii_whitespace() + { + out.pop(); + } + Ok(out) + } + + fn unix(&self) -> u64 { + self.sys + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + } + + pub fn relative_to(&self, other: &Self) -> String { + let a = self.unix(); + let b = other.unix(); + + let (pre, post, mut diff) = if a < b { + ("in ", "", b - a) + } else { + ("", " ago", a - b) + }; + + let unit = if diff < 60 { + "s" + } else if { + diff /= 60; + diff < 60 + } { + "m" + } else if { + diff /= 60; + diff < 24 + } { + "h" + } else if { + diff /= 24; + diff < 365 + } { + "d" + } else { + diff /= 365; + "y" + }; + + format!("{pre}{diff}{unit}{post}") + } + + pub const fn longest_reasonable_delta() -> usize { + 7 + } +} + +#[test] +fn long_delta() { + assert_eq!( + DateTime::now() + .relative_to(&DateTime { + // 30 years ago + sys: SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 365 * 30) + }) + .len(), + DateTime::longest_reasonable_delta() + ); +} + +impl fmt::Display for DateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + String::from_utf8_lossy(&self.format().unwrap_or_else(|_| Vec::new())) + ) + } +} diff --git a/src/main.rs b/src/main.rs index 5e29a6e..9a4e692 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ pub mod parse; pub mod raw; pub mod reload; pub mod run; +pub mod date; use linebuf::LineBuf; use raw::*; @@ -57,10 +58,25 @@ type BString = Vec<u8>; #[allow(non_camel_case_types)] type bstr = [u8]; +#[derive(Clone)] +struct HistoryEntry { + pub time: date::DateTime, + pub cmd: BString, +} + +impl HistoryEntry { + pub fn new(cmd: BString) -> Self { + Self { + time: date::DateTime::now(), + cmd, + } + } +} + pub struct Session { raw: ScopedRawMode, line: LineBuf, - history: Vec<BString>, + history: Vec<HistoryEntry>, prev_path: BString, builtins: HashMap<BString, &'static dyn run::Builtin>, vars: HashMap<BString, BString>, @@ -131,7 +147,7 @@ impl Session { let new = if self.history_visit == 0 { Vec::new() } else { - self.history[self.history.len() - self.history_visit].clone() + self.history[self.history.len() - self.history_visit].cmd.clone() }; io::stdout().write_all(&new).unwrap(); io::stdout().flush().unwrap(); @@ -311,7 +327,7 @@ fn event_loop() { let line = se.line.dump(); if !line.is_empty() { print!("\r\n"); - se.history.push(line.clone()); + se.history.push(HistoryEntry::new(line.clone())); se.history_visit = 0; drop(se); run::run(session.clone(), line); @@ -322,7 +338,7 @@ fn event_loop() { 127 => { if se.line.is_empty() && !se.line.is_dirty() && !se.history.is_empty() { // take previous command for editing - let cmd = se.history[se.history.len() - 1].clone(); + let cmd = se.history[se.history.len() - 1].cmd.clone(); se.type_bytes(&cmd); } else { se.del_left(); @@ -455,7 +471,7 @@ fn event_loop() { } b'|' if se.line.is_empty() && !se.history.is_empty() => { - let mut cmd = se.history[se.history.len() - 1].clone(); + let mut cmd = se.history[se.history.len() - 1].cmd.clone(); cmd.extend_from_slice(b" | "); io::stdout().write_all(&cmd).unwrap(); io::stdout().flush().unwrap(); diff --git a/src/run/builtin.rs b/src/run/builtin.rs index b791861..c9456cd 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -27,12 +27,8 @@ impl Builtin for cd { std::mem::swap(&mut dir, &mut se.lock().unwrap().prev_path); let target_path: BString = match args.get(0).map(|v| &v[..]) { - Some(b"-") => { - dir - } - Some(path) => { - path.to_vec() - } + Some(b"-") => dir, + Some(path) => path.to_vec(), None => { if let Some(home) = std::env::var_os("HOME") { home.into_encoded_bytes() @@ -223,10 +219,16 @@ impl Builtin for history { _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { - // TODO: better history querying let hist = session.lock().unwrap().history.clone(); + let now = crate::date::DateTime::now(); for entry in hist { - stdout.write_all(&entry)?; + let delta = now.relative_to(&entry.time); + for _ in 0..crate::date::DateTime::longest_reasonable_delta() - delta.len() { + stdout.write_all(b" ")?; + } + stdout.write_all(delta.as_bytes())?; + stdout.write_all(b" ")?; + stdout.write_all(&entry.cmd)?; stdout.write_all(b"\n")?; } Ok(()) |
