mod 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, #[arg(trailing_var_arg = true, allow_hyphen_values = true)] rest: Vec, }, /// Fuzzy-search history in a TUI; prints the accepted command. Search { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] query: Vec, }, /// 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, /// 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> { 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, 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> { 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> { 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, shell: Shell) -> Result<(), Box> { // 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(()) }