aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <>2026-03-08 13:15:46 +0100
committerJonas Maier <>2026-03-08 13:15:46 +0100
commit25a3def15ce676f0902971c4d81d16636f2ea933 (patch)
treee9b15974523d2099cd4eab6cd74fd1f4bba275d2 /src
parent7bca57f2dddd637fa9a11a49c2cf524502e34e0e (diff)
downloadpish-25a3def15ce676f0902971c4d81d16636f2ea933.tar.gz
history :)
Diffstat (limited to 'src')
-rw-r--r--src/history.rs153
-rw-r--r--src/main.rs1
-rw-r--r--src/run/builtin.rs78
3 files changed, 191 insertions, 41 deletions
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<Self> {
+ Ok(Self { db: try_db()? })
+ }
+
+ pub fn query(
+ &self,
+ min_time: Option<DateTime>,
+ max_time: Option<DateTime>,
+ path_prefix: Option<&[u8]>,
+ path_strict: bool,
+ ) -> sqlite::Result<impl Iterator<Item = HistoryEntry>> {
+ 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::<i64, _>(1).ok()?;
+ let loc: Vec<u8> = stmt.read::<Vec<u8>, _>(2).ok()?;
+ let cmd: Vec<u8> = stmt.read::<Vec<u8>, _>(3).ok()?;
+ let time = DateTime::from_unix(ts as u64);
+ Some(HistoryEntry { time, loc, cmd })
+ }
+ _ => None,
+ }))
+ }
+}
+
+pub fn local_history_filter(
+ hist: Vec<HistoryEntry>,
+ min_time: Option<DateTime>,
+ max_time: Option<DateTime>,
+ path_prefix: Option<&[u8]>,
+ path_strict: bool,
+) -> impl Iterator<Item = HistoryEntry> {
+ 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<T: Iterator<Item = HistoryEntry>>(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(())
}
}