use std::path::{Path, PathBuf}; use rusqlite::{Connection, Result}; /// Newest rows pulled into the search UI. const SEARCH_LIMIT: usize = 10_000; /// Rows shown by `diethist list`. const LIST_LIMIT: usize = 50; pub fn db_dir() -> PathBuf { let dir = match std::env::var_os("XDG_DATA_HOME") { Some(d) if !d.is_empty() => PathBuf::from(d), _ => PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join(".local/share"), }; dir.join("diethist") } pub fn db_path() -> PathBuf { db_dir().join("history.db") } pub fn open(path: &Path) -> Result { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|e| { rusqlite::Error::InvalidPath(PathBuf::from(format!("{}: {e}", parent.display()))) })?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)); } } let db = Connection::open(path)?; db.busy_timeout(std::time::Duration::from_millis(5000))?; db.pragma_update(None, "journal_mode", "WAL")?; db.execute_batch( r" create table if not exists history ( id integer primary key autoincrement, cmd text not null unique, cwd text, exit integer, ts integer, session text ); -- newest-first, so queries can stop at the limit. create index if not exists history_ts_cmd on history(ts desc, cmd);", )?; Ok(db) } pub struct Candidate { pub cmd: String, /// Unix seconds. pub ts: i64, } pub fn load_candidates(db: &Connection) -> Result> { let mut stmt = db.prepare(&format!( "select cmd, ts from history order by ts desc limit {SEARCH_LIMIT}" ))?; let rows = stmt.query_map([], |r| { Ok(Candidate { cmd: r.get(0)?, ts: r.get(1).unwrap_or(0), }) })?; Ok(rows.flatten().collect()) } /// Records a command, bumping the timestamp when it has been run before. pub fn add(db: &Connection, cmd: &str, exit: i64, ts: i64) -> Result<()> { let cwd = std::env::current_dir().unwrap_or_default(); let session = std::env::var("diethist_SESSION").unwrap_or_default(); db.execute( "insert into history(cmd, cwd, exit, ts, session) values(?1, ?2, ?3, ?4, ?5) on conflict(cmd) do update set cwd = excluded.cwd, exit = excluded.exit, ts = excluded.ts, session = excluded.session", rusqlite::params![cmd, cwd.to_string_lossy(), exit, ts, session], )?; Ok(()) } pub struct Entry { pub ts: i64, pub exit: i64, pub cmd: String, } pub fn recent(db: &Connection) -> Result> { let mut stmt = db.prepare(&format!( "select ts, exit, cmd from history order by id desc limit {LIST_LIMIT}" ))?; let rows = stmt.query_map([], |r| { Ok(Entry { ts: r.get(0).unwrap_or(0), exit: r.get(1).unwrap_or(0), cmd: r.get(2)?, }) })?; Ok(rows.flatten().collect()) }