aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonas Maier <>2026-03-07 20:49:52 +0100
committerJonas Maier <>2026-03-07 20:49:52 +0100
commitdf5eec13a031d41232e7407794e2f5b9a0a2d608 (patch)
treea76fba2ed68928d82d9b8352bfd0afc33d118fb0
parent86cdb8c21dec737a3f0a40311782de851c1203d1 (diff)
downloadpish-df5eec13a031d41232e7407794e2f5b9a0a2d608.tar.gz
history with relative time
-rw-r--r--src/date.rs113
-rw-r--r--src/main.rs26
-rw-r--r--src/run/builtin.rs18
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(())