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 <raf@notashelf.dev>
Change-Id: I34107880185d231d207b0dab7782d5d96a6a6964
This commit is contained in:
raf 2026-01-22 16:54:22 +03:00
commit 5731fb08a5
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -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<T>(
result: Result<T, impl std::fmt::Display>,
context: &str,
@ -197,41 +226,15 @@ 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),
db.list(io::stdout(), cli.preview_width, expired),
"failed to list entries",
);
},
Some("json") => {
match db.list_json() {
match db.list_json(expired) {
Ok(json) => {
println!("{json}");
},
@ -246,18 +249,17 @@ fn main() -> color_eyre::eyre::Result<()> {
None => {
if std::io::stdout().is_terminal() {
report_error(
db.list_tui(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),
db.list(io::stdout(), cli.preview_width, expired),
"failed to list entries",
);
}
},
}
}
},
Some(Command::Decode { input }) => {
report_error(
@ -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 {