From 25a3def15ce676f0902971c4d81d16636f2ea933 Mon Sep 17 00:00:00 2001 From: Jonas Maier <> Date: Sun, 8 Mar 2026 13:15:46 +0100 Subject: history :) --- src/history.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 1 - src/run/builtin.rs | 78 +++++++++++++++------------ 3 files changed, 191 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/history.rs b/src/history.rs index 865d949..ea71fa7 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,8 +1,9 @@ -use sqlite::Connection; +use sqlite::{Connection, State}; use crate::BString; use crate::date::DateTime; use std::env::current_dir; +use std::i64; use std::path::PathBuf; fn db_file() -> PathBuf { @@ -21,15 +22,27 @@ pub struct HistoryEntry { pub cmd: BString, } +pub fn canonical_path(mut path: BString) -> BString { + while let Some(b'/') = path.last() { + path.pop(); + } + if path.is_empty() { + path.push(b'/'); + } + path +} + impl HistoryEntry { pub fn new(cmd: BString) -> Self { Self { time: DateTime::now(), - loc: current_dir() - .unwrap() - .as_os_str() - .as_encoded_bytes() - .to_vec(), + loc: canonical_path( + current_dir() + .unwrap() + .as_os_str() + .as_encoded_bytes() + .to_vec(), + ), cmd, } } @@ -74,5 +87,133 @@ fn try_persist(entry: &HistoryEntry) -> sqlite::Result<()> { pub fn persist(entry: &HistoryEntry) { // keep quiet in case db fails + // TODO maybe better behavior? let _ = try_persist(entry); } + +pub struct HistoryQueryer { + db: Connection, +} + +impl HistoryQueryer { + pub fn new() -> sqlite::Result { + Ok(Self { db: try_db()? }) + } + + pub fn query( + &self, + min_time: Option, + max_time: Option, + path_prefix: Option<&[u8]>, + path_strict: bool, + ) -> sqlite::Result> { + let mut query = String::from("SELECT id, ts, loc, cmd FROM history\n"); + let mut has_cond = false; + let mut cond = |c| { + if has_cond { + query += "AND "; + } + query += c; + query += "\n"; + has_cond = true; + }; + + let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec())); + let mut upper = Vec::new(); + + if let Some(path_prefix) = &path_prefix { + if path_strict { + cond("loc = ?"); + } else { + upper = path_prefix.to_vec(); + if let Some(last) = upper.last_mut() { + *last = last.saturating_add(1); + } else { + upper.push(0xFF); + } + cond("loc >= ? AND loc < ?"); + } + } + + if min_time.is_some() { + cond("ts >= ?"); + } + + if max_time.is_some() { + cond("ts <= ?"); + } + + query += "ORDER BY id asc"; + let mut stmt = self.db.prepare(&query)?; + + let mut i = 1; + if let Some(path_prefix) = &path_prefix { + stmt.bind((i, path_prefix.as_slice()))?; + i += 1; + if !path_strict { + stmt.bind((i, upper.as_slice()))?; + i += 1; + } + } + + if let Some(t) = min_time { + stmt.bind((i, t.unix() as i64))?; + i += 1; + } + + if let Some(t) = max_time { + stmt.bind((i, t.unix() as i64))?; + i += 1; + } + + let _ = i; + + Ok(std::iter::from_fn(move || match stmt.next() { + Ok(State::Row) => { + let ts: i64 = stmt.read::(1).ok()?; + let loc: Vec = stmt.read::, _>(2).ok()?; + let cmd: Vec = stmt.read::, _>(3).ok()?; + let time = DateTime::from_unix(ts as u64); + Some(HistoryEntry { time, loc, cmd }) + } + _ => None, + })) + } +} + +pub fn local_history_filter( + hist: Vec, + min_time: Option, + max_time: Option, + path_prefix: Option<&[u8]>, + path_strict: bool, +) -> impl Iterator { + let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec())); + + hist.into_iter().filter(move |entry| { + if let Some(t) = &min_time + && t.unix() > entry.time.unix() + { + return false; + } + + if let Some(t) = &max_time + && t.unix() < entry.time.unix() + { + return false; + } + + if let Some(path_prefix) = &path_prefix { + if path_strict { + let canon_path = canonical_path(entry.loc.clone()); + return &canon_path == path_prefix; + } else { + if !entry.loc.starts_with(&path_prefix) { + return false; + } + } + } + + true + }) +} diff --git a/src/main.rs b/src/main.rs index c2cf375..f038336 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::env::current_dir; use std::ffi::OsStr; use std::fs; use std::io::{self, IsTerminal, Read, Write}; diff --git a/src/run/builtin.rs b/src/run/builtin.rs index 341d98d..33025cb 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -257,6 +257,21 @@ struct HistoryArgs { // TODO: temporal control, i.e. before & after } +fn display_history>(it: T, stdout: &mut dyn Write) -> Result { + let now = crate::date::DateTime::now(); + for entry in it { + 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(()) +} + pub struct history; impl Builtin for history { fn name(&self) -> &str { @@ -271,48 +286,43 @@ impl Builtin for history { stdout: &mut dyn Write, ) -> Result { let args: HistoryArgs = read_args(args, stdout)?; - let mut hist = session.lock().unwrap().history.clone(); - let now = crate::date::DateTime::now(); - let mut in_dir = if args.here { - current_dir()?.as_os_str().as_bytes().to_vec() + let path_prefix = if args.here { + Some(current_dir()?.as_os_str().as_bytes().to_vec()) } else if let Some(path) = args.at { - path.as_os_str().as_bytes().to_vec() + Some(path.as_os_str().as_bytes().to_vec()) } else { - Vec::new() + None }; - while let Some(b'/') = in_dir.last() { - in_dir.pop(); - } - - // TODO: local handling (first implement global history) - - for entry in hist.iter_mut() { - if !entry.loc.starts_with(&in_dir) { - continue; - } + let min_time = None; + let max_time = None; + + if args.local { + let hist = session.lock().unwrap().history.clone(); + display_history( + crate::history::local_history_filter( + hist, + min_time, + max_time, + path_prefix.as_deref(), + args.strict, + ), + stdout, + ) + } else { + let Ok(hist) = crate::history::HistoryQueryer::new() else { + write!(stdout, "error opening global history file\n")?; + return Err(Error::Exit(-1)); + }; - if args.strict { - while let Some(b'/') = entry.loc.last() { - entry.loc.pop(); - } - if entry.loc.len() != in_dir.len() { - continue; - } - } + let Ok(it) = hist.query(min_time, max_time, path_prefix.as_deref(), args.strict) else { + write!(stdout, "error querying global history\n")?; + return Err(Error::Exit(-1)); + }; - 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")?; + display_history(it, stdout) } - - Ok(()) } } -- cgit v1.2.3