From df5eec13a031d41232e7407794e2f5b9a0a2d608 Mon Sep 17 00:00:00 2001 From: Jonas Maier <> Date: Sat, 7 Mar 2026 20:49:52 +0100 Subject: history with relative time --- src/date.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 26 +++++++++--- src/run/builtin.rs | 18 +++++---- 3 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 src/date.rs 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 { + 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; #[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, + history: Vec, prev_path: BString, builtins: HashMap, vars: HashMap, @@ -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(()) -- cgit v1.2.3