From 5731fb08a523a3b2f210ea17d0d18bb8dfec8750 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 22 Jan 2026 16:54:22 +0300 Subject: [PATCH] cli: add db subcommand Adds a `db` subcommand with `DbAction` for three new database operations: wipe, vacuum and stats. We can extend this database later, but this is a very good start for now and plays nicely with the `--expired` flag. This soft-deprecates `stash wipe` in favor of a `stash db wipe` with the addition of a new `--expired` flag that wipes the expired entries only. The `list` subcommand has also been refactored to allow for a similar `--expired` flag that lists only expired entries. Signed-off-by: NotAShelf Change-Id: I34107880185d231d207b0dab7782d5d96a6a6964 --- src/main.rs | 174 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 56 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1bf87ba..aca9838 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,12 +99,21 @@ enum Command { }, /// Wipe all clipboard history + /// + /// DEPRECATED: Use `stash db wipe` instead + #[command(hide = true)] Wipe { /// Ask for confirmation before wiping #[arg(long)] ask: bool, }, + /// Database management operations + Db { + #[command(subcommand)] + action: DbAction, + }, + /// Import clipboard data from stdin (default: TSV format) Import { /// Explicitly specify format: "tsv" (default) @@ -124,6 +133,26 @@ enum Command { }, } +#[derive(Subcommand)] +enum DbAction { + /// Wipe database entries + Wipe { + /// Only wipe expired entries instead of all entries + #[arg(long)] + expired: bool, + + /// Ask for confirmation before wiping + #[arg(long)] + ask: bool, + }, + + /// Optimize database using VACUUM + Vacuum, + + /// Show database statistics + Stats, +} + fn report_error( result: Result, context: &str, @@ -197,66 +226,39 @@ fn main() -> color_eyre::eyre::Result<()> { ); }, Some(Command::List { format, expired }) => { - if expired { - // Diagnostic mode: show expired entries only (does not cleanup) - match db.get_expired_entries() { - Ok(entries) => { - for (id, contents, mime) in entries { - let preview = db::preview_entry( - &contents, - mime.as_deref(), - cli.preview_width, - ); - println!("{id}\t{preview}"); - } - }, - Err(e) => { - log::error!("failed to list expired entries: {e}"); - }, - } - } else { - // Normal list mode - // Cleanup expired entries when daemon is not running - if let Ok(count) = db.cleanup_expired() { - if count > 0 { - log::info!("Cleaned up {} expired entries", count); + match format.as_deref() { + Some("tsv") => { + report_error( + db.list(io::stdout(), cli.preview_width, expired), + "failed to list entries", + ); + }, + Some("json") => { + match db.list_json(expired) { + Ok(json) => { + println!("{json}"); + }, + Err(e) => { + log::error!("failed to list entries as JSON: {e}"); + }, } - } - - match format.as_deref() { - Some("tsv") => { + }, + Some(other) => { + log::error!("unsupported format: {other}"); + }, + None => { + if std::io::stdout().is_terminal() { report_error( - db.list(io::stdout(), cli.preview_width), + db.list_tui(cli.preview_width, expired), + "failed to list entries in TUI", + ); + } else { + report_error( + db.list(io::stdout(), cli.preview_width, expired), "failed to list entries", ); - }, - Some("json") => { - match db.list_json() { - Ok(json) => { - println!("{json}"); - }, - Err(e) => { - log::error!("failed to list entries as JSON: {e}"); - }, - } - }, - Some(other) => { - log::error!("unsupported format: {other}"); - }, - None => { - if std::io::stdout().is_terminal() { - report_error( - db.list_tui(cli.preview_width), - "failed to list entries in TUI", - ); - } else { - report_error( - db.list(io::stdout(), cli.preview_width), - "failed to list entries", - ); - } - }, - } + } + }, } }, Some(Command::Decode { input }) => { @@ -324,6 +326,10 @@ fn main() -> color_eyre::eyre::Result<()> { } }, Some(Command::Wipe { ask }) => { + eprintln!( + "Warning: The 'stash wipe' command is deprecated. Use 'stash db \ + wipe' instead." + ); let mut should_proceed = true; if ask { should_proceed = Confirm::new( @@ -341,6 +347,62 @@ fn main() -> color_eyre::eyre::Result<()> { } }, + Some(Command::Db { action }) => { + match action { + DbAction::Wipe { expired, ask } => { + let mut should_proceed = true; + if ask { + let message = if expired { + "Are you sure you want to wipe all expired clipboard entries?" + } else { + "Are you sure you want to wipe ALL clipboard history?" + }; + should_proceed = Confirm::new(message) + .with_default(false) + .prompt() + .unwrap_or(false); + if !should_proceed { + log::info!("db wipe command aborted by user."); + } + } + if should_proceed { + if expired { + match db.cleanup_expired() { + Ok(count) => { + log::info!("Wiped {} expired entries", count); + }, + Err(e) => { + log::error!("failed to wipe expired entries: {e}"); + }, + } + } else { + report_error(db.wipe(), "failed to wipe database"); + } + } + }, + DbAction::Vacuum => { + match db.vacuum() { + Ok(()) => { + log::info!("Database optimized successfully"); + }, + Err(e) => { + log::error!("failed to vacuum database: {e}"); + }, + } + }, + DbAction::Stats => { + match db.stats() { + Ok(stats) => { + println!("{}", stats); + }, + Err(e) => { + log::error!("failed to get database stats: {e}"); + }, + } + }, + } + }, + Some(Command::Import { r#type, ask }) => { let mut should_proceed = true; if ask {