a tiny, local-first shell history manager with fuzzy search.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179mod db;mod import;mod tui;
use std::io::Write;use std::path::PathBuf;use std::process::ExitCode;
use clap::{Parser, Subcommand};use import::Shell;
const BASH_INIT: &str = include_str!("../shell/init.bash");const FISH_INIT: &str = include_str!("../shell/init.fish");const ZSH_INIT: &str = include_str!("../shell/init.zsh");
#[derive(Parser)]#[command(name = "diethist", about = "sqlite-backed shell history", version)]struct Cli { #[command(subcommand)] command: Command,}
#[derive(Subcommand)]enum Command { /// Print the shell integration script. Init { #[arg(value_enum, default_value_t = InitShell::Bash)] shell: InitShell, }, /// Record a command. Add { /// Exit code of the command. #[arg(long, default_value_t = 0)] exit: i64, /// Command to record (alternative to positional args). #[arg(long)] cmd: Option<String>, #[arg(trailing_var_arg = true, allow_hyphen_values = true)] rest: Vec<String>, }, /// Fuzzy-search history in a TUI; prints the accepted command. Search { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] query: Vec<String>, }, /// Print the 50 most recently recorded commands. List, /// Seed the database from an existing shell history file. Import { /// History file to read (default: per-shell location). #[arg(long)] file: Option<PathBuf>, /// History format. `auto` sniffs the file, or falls back to $SHELL. #[arg(long, value_enum, default_value = "auto")] shell: Shell, },}
#[derive(Copy, Clone, clap::ValueEnum)]enum InitShell { Bash, Fish, Zsh,}
fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("diethist: {e}"); ExitCode::FAILURE } }}
fn now() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0)}
fn run() -> Result<(), Box<dyn std::error::Error>> { match Cli::parse().command { Command::Init { shell } => { print!( "{}", match shell { InitShell::Bash => BASH_INIT, InitShell::Fish => FISH_INIT, InitShell::Zsh => ZSH_INIT, } ); }
Command::Add { exit, cmd, rest } => add(exit, cmd, &rest), Command::Search { query } => search(&query.join(" "))?, Command::List => list()?, Command::Import { file, shell } => run_import(file, shell)?, }
Ok(())}
/// Failures are swallowed: recording must not get in the way of the prompt.fn add(exit: i64, cmd: Option<String>, rest: &[String]) { let cmd = cmd.unwrap_or_else(|| rest.join(" ")); let cmd = cmd.trim_end_matches([' ', '\t', '\n']); if cmd.trim().is_empty() { return; }
let write = db::open(&db::db_path()).and_then(|conn| db::add(&conn, cmd, exit, now())); if let Err(e) = write { eprintln!("diethist: {e}"); }}
fn search(initial: &str) -> Result<(), Box<dyn std::error::Error>> { let candidates = { let conn = db::open(&db::db_path())?; db::load_candidates(&conn)? };
if let Some(selected) = tui::run(candidates, initial, now())? { println!("{selected}"); }
Ok(())}
fn list() -> Result<(), Box<dyn std::error::Error>> { let conn = db::open(&db::db_path())?; let tz = jiff::tz::TimeZone::system();
let mut out = std::io::stdout().lock(); for e in db::recent(&conn)? { let ts = jiff::Timestamp::from_second(e.ts) .map(|t| { t.to_zoned(tz.clone()) .strftime("%Y-%m-%d %H:%M") .to_string() }) .unwrap_or_default();
writeln!(out, "{}\t{}\t{}", ts, e.exit, e.cmd)?; }
Ok(())}
fn run_import(file: Option<PathBuf>, shell: Shell) -> Result<(), Box<dyn std::error::Error>> { // no --file: pick a default path from $SHELL. let (path, shell) = match file { Some(p) => (p, shell), None => { let shell = import::shell_from_env(shell); (import::default_path(shell), shell) } };
let (detected, records) = import::parse_file(&path, shell, now())?;
let mut conn = db::open(&db::db_path())?; let n = import::insert_all(&mut conn, &records)?;
eprintln!( "diethist: imported {n} commands from {} ({} format)", path.display(), match detected { Shell::Fish => "fish", Shell::Zsh => "zsh", _ => "bash", } );
Ok(())}