From 86001652cd003f7fa231435815b1969beb27a0cc Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 13 Aug 2025 19:30:56 +0300 Subject: [PATCH 1/6] stash: allow confirming destructive operations with `--ask` Signed-off-by: NotAShelf Change-Id: I6a6a69644c23734a8b088e20473d381390d532b4 --- src/main.rs | 160 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 49 deletions(-) diff --git a/src/main.rs b/src/main.rs index ecfaa3e..1e35a8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::{ }; use clap::{CommandFactory, Parser, Subcommand}; +use inquire::Confirm; mod commands; mod db; @@ -39,6 +40,10 @@ struct Cli { #[arg(long)] db_path: Option, + /// Ask for confirmation before destructive operations + #[arg(long)] + ask: bool, + #[command(flatten)] verbosity: clap_verbosity_flag::Verbosity, } @@ -67,16 +72,28 @@ enum Command { /// Explicitly specify type: "id" or "query" #[arg(long, value_parser = ["id", "query"])] r#type: Option, + + /// Ask for confirmation before deleting + #[arg(long)] + ask: bool, }, /// Wipe all clipboard history - Wipe, + Wipe { + /// Ask for confirmation before wiping + #[arg(long)] + ask: bool, + }, /// Import clipboard data from stdin (default: TSV format) Import { /// Explicitly specify format: "tsv" (default) #[arg(long, value_parser = ["tsv"])] r#type: Option, + + /// Ask for confirmation before importing + #[arg(long)] + ask: bool, }, /// Watch clipboard for changes and store automatically @@ -144,17 +161,16 @@ fn main() { "Failed to list entries", ); } - "json" => { - // Implement JSON output - match db.list_json() { - Ok(json) => { - println!("{json}"); - } - Err(e) => { - log::error!("Failed to list entries as JSON: {e}"); - } + + "json" => match db.list_json() { + Ok(json) => { + println!("{json}"); } - } + Err(e) => { + log::error!("Failed to list entries as JSON: {e}"); + } + }, + _ => { log::error!("Unsupported format: {format}"); } @@ -166,52 +182,98 @@ fn main() { "Failed to decode entry", ); } - Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) { - (Some(s), Some("id")) => { - if let Ok(id) = s.parse::() { - use std::io::Cursor; - report_error( - db.delete(Cursor::new(format!("{id}\n"))), - "Failed to delete entry by id", - ); - } else { - log::error!("Argument is not a valid id"); + Some(Command::Delete { arg, r#type, ask }) => { + let mut should_proceed = true; + if ask { + should_proceed = + Confirm::new("Are you sure you want to delete clipboard entries?") + .with_default(false) + .prompt() + .unwrap_or(false); + + if !should_proceed { + log::info!("Aborted by user."); } } - (Some(s), Some("query")) => { - report_error(db.query_delete(&s), "Failed to delete entry by query"); - } - (Some(s), None) => { - if let Ok(id) = s.parse::() { - use std::io::Cursor; - report_error( - db.delete(Cursor::new(format!("{id}\n"))), - "Failed to delete entry by id", - ); - } else { - report_error(db.query_delete(&s), "Failed to delete entry by query"); + if should_proceed { + match (arg, r#type.as_deref()) { + (Some(s), Some("id")) => { + if let Ok(id) = s.parse::() { + use std::io::Cursor; + report_error( + db.delete(Cursor::new(format!("{id}\n"))), + "Failed to delete entry by id", + ); + } else { + log::error!("Argument is not a valid id"); + } + } + (Some(s), Some("query")) => { + report_error(db.query_delete(&s), "Failed to delete entry by query"); + } + (Some(s), None) => { + if let Ok(id) = s.parse::() { + use std::io::Cursor; + report_error( + db.delete(Cursor::new(format!("{id}\n"))), + "Failed to delete entry by id", + ); + } else { + report_error( + db.query_delete(&s), + "Failed to delete entry by query", + ); + } + } + (None, _) => { + report_error( + db.delete(io::stdin()), + "Failed to delete entry from stdin", + ); + } + (_, Some(_)) => { + log::error!("Unknown type for --type. Use \"id\" or \"query\"."); + } } } - (None, _) => { - report_error(db.delete(io::stdin()), "Failed to delete entry from stdin"); + } + Some(Command::Wipe { ask }) => { + let mut should_proceed = true; + if ask { + should_proceed = + Confirm::new("Are you sure you want to wipe all clipboard history?") + .with_default(false) + .prompt() + .unwrap_or(false); + if !should_proceed { + log::info!("Aborted by user."); + } } - (_, Some(_)) => { - log::error!("Unknown type for --type. Use \"id\" or \"query\"."); + if should_proceed { + report_error(db.wipe(), "Failed to wipe database"); } - }, - Some(Command::Wipe) => { - report_error(db.wipe(), "Failed to wipe database"); } - Some(Command::Import { r#type }) => { - // Default format is TSV (Cliphist compatible) - let format = r#type.as_deref().unwrap_or("tsv"); - match format { - "tsv" => { - db.import_tsv(io::stdin()); + Some(Command::Import { r#type, ask }) => { + let mut should_proceed = true; + if ask { + should_proceed = Confirm::new("Are you sure you want to import clipboard data? This may overwrite existing entries.") + .with_default(false) + .prompt() + .unwrap_or(false); + if !should_proceed { + log::info!("Aborted by user."); } - _ => { - log::error!("Unsupported import format: {format}"); + } + if should_proceed { + let format = r#type.as_deref().unwrap_or("tsv"); + match format { + "tsv" => { + db.import_tsv(io::stdin()); + } + _ => { + log::error!("Unsupported import format: {format}"); + } } } } @@ -220,7 +282,7 @@ fn main() { } None => { if let Err(e) = Cli::command().print_help() { - eprintln!("Failed to print help: {e}"); + log::error!("Failed to print help: {e}"); } println!(); } From 0c0547b6e81298a31510913795f34a01f8193078 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 16:16:44 +0300 Subject: [PATCH 2/6] chore: bump dependencies Signed-off-by: NotAShelf Change-Id: I6a6a69647235c4fe03eb0c5e6ea1290110e29ccf --- Cargo.lock | 273 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 9 +- 2 files changed, 268 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2b6bc3..95cd7eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.1" @@ -309,6 +315,31 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "dirs" version = "6.0.0" @@ -336,6 +367,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "env_filter" version = "0.1.3" @@ -451,6 +488,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -520,6 +566,22 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.9.1", + "crossterm", + "dyn-clone", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -568,7 +630,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags", + "bitflags 2.9.1", "libc", ] @@ -595,6 +657,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -613,6 +685,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nom" version = "7.1.3" @@ -666,6 +759,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "paste" version = "1.0.15" @@ -767,6 +883,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -835,7 +960,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags", + "bitflags 2.9.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -849,7 +974,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -862,7 +987,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", @@ -875,6 +1000,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -913,6 +1044,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -961,6 +1113,7 @@ dependencies = [ "dirs", "env_logger", "imagesize", + "inquire", "log", "rmp-serde", "rusqlite", @@ -979,9 +1132,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", @@ -1039,6 +1192,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1085,7 +1250,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags", + "bitflags 2.9.1", "rustix 1.0.8", "wayland-backend", "wayland-scanner", @@ -1097,7 +1262,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1109,7 +1274,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1136,12 +1301,43 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1160,6 +1356,21 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1193,6 +1404,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1205,6 +1422,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1217,6 +1440,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1241,6 +1470,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1253,6 +1488,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1265,6 +1506,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1277,6 +1524,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1295,7 +1548,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1b6caa4..b16b514 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,18 +9,19 @@ repository = "https://github.com/notashelf/stash" rust-version = "1.85" [dependencies] -clap = { version = "4.5.44", features = ["derive"] } +clap = { version = "4.5.45", features = ["derive"] } +clap-verbosity-flag = "3.0.3" dirs = "6.0.0" -serde = { version = "1.0.219", features = ["derive"] } rmp-serde = "1.3.0" -imagesize = "0.14" +imagesize = "0.14.0" +inquire = { default-features = false, version = "0.7.5", features = [ "crossterm" ] } log = "0.4.27" env_logger = "0.11.8" -clap-verbosity-flag = "3.0.3" thiserror = "2.0.14" wl-clipboard-rs = "0.9.2" rusqlite = { version = "0.37.0", features = ["bundled"] } smol = "2.0.2" +serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" base64 = "0.22.1" From f3089148e079d2ce4030da8e0bbc11f4c91e940f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 17:01:43 +0300 Subject: [PATCH 3/6] db: allow explicitly skipping sensitive entries Signed-off-by: NotAShelf Change-Id: I6a6a6964ed1deaac0215ae9c6f4c70cfdc50164d --- Cargo.lock | 1 + Cargo.toml | 5 ++++- src/db/mod.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95cd7eb..4810791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1115,6 +1115,7 @@ dependencies = [ "imagesize", "inquire", "log", + "regex", "rmp-serde", "rusqlite", "serde", diff --git a/Cargo.toml b/Cargo.toml index b16b514..248e525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ clap-verbosity-flag = "3.0.3" dirs = "6.0.0" rmp-serde = "1.3.0" imagesize = "0.14.0" -inquire = { default-features = false, version = "0.7.5", features = [ "crossterm" ] } +inquire = { default-features = false, version = "0.7.5", features = [ + "crossterm", +] } log = "0.4.27" env_logger = "0.11.8" thiserror = "2.0.14" @@ -24,6 +26,7 @@ smol = "2.0.2" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" base64 = "0.22.1" +regex = "1.11.1" [profile.release] diff --git a/src/db/mod.rs b/src/db/mod.rs index 0e7b943..6b64cf7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,9 +1,12 @@ +use std::env; use std::fmt; +use std::fs; use std::io::{BufRead, BufReader, Read, Write}; use std::str; use imagesize::{ImageSize, ImageType}; -use log::{error, info}; +use log::{error, info, warn}; +use regex::Regex; use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; @@ -176,6 +179,18 @@ impl ClipboardDb for SqliteClipboardDb { other => other, }; + // Try to load regex from systemd credential file, then env var + let regex = load_sensitive_regex(); + if let Some(re) = regex { + // Only check text data + if let Ok(s) = std::str::from_utf8(&buf) { + if re.is_match(s) { + warn!("Clipboard entry matches sensitive regex, skipping store."); + return Err(StashError::Store("Filtered by sensitive regex".to_string())); + } + } + } + self.deduplicate(&buf, max_dedupe_search)?; self.conn @@ -375,6 +390,31 @@ impl ClipboardDb for SqliteClipboardDb { } // Helper functions + +/// Try to load a sensitive regex from systemd credential or env. +/// +/// # Returns +/// `Some(Regex)` if present and valid, `None` otherwise. +fn load_sensitive_regex() -> Option { + if let Ok(regex_path) = env::var("CREDENTIALS_DIRECTORY") { + let file = format!("{}/clipboard_filter", regex_path); + if let Ok(contents) = fs::read_to_string(&file) { + if let Ok(re) = Regex::new(contents.trim()) { + return Some(re); + } + } + } + + // Fallback to an environment variable + if let Ok(pattern) = env::var("STASH_SENSITIVE_REGEX") { + if let Ok(re) = Regex::new(&pattern) { + return Some(re); + } + } + + None +} + pub fn extract_id(input: &str) -> Result { let id_str = input.split('\t').next().unwrap_or(""); id_str.parse().map_err(|_| "invalid id") From bbe3a0fd8dbcf72a3679b5f50f762057b2987b70 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 17:13:03 +0300 Subject: [PATCH 4/6] docs: update README with filter options Signed-off-by: NotAShelf Change-Id: I6a6a696403633a49cbefdfe38bc8d6064fdd5a25 --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f78b9b1..d91e0c7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ Wayland clipboard "manager" with fast persistent history and multi-media support. Stores and previews clipboard entries (text, images) on the command line. +## Features + +- Stores clipboard entries with automatic MIME detection +- Fast persistent storage using SQLite +- List, search, decode, delete, and wipe clipboard history +- Backwards compatible with Cliphist TSV format + - Import clipboard history from TSV (e.g., from `cliphist list`) +- Image preview (shows dimensions and format) +- Deduplication and entry limit control +- Text previews with customizable width +- Sensitive clipboard filtering via regex (see below) + ## Installation ### With Nix @@ -59,23 +71,18 @@ releases are made when a version gets tagged, and are available under cargo install --git https://github.com/notashelf/stash ``` -## Features - -- Stores clipboard entries with automatic MIME detection -- Fast persistent storage using SQLite -- List, search, decode, delete, and wipe clipboard history -- Backwards compatible with Cliphist TSV format - - Import clipboard history from TSV (e.g., from `cliphist list`) -- Image preview (shows dimensions and format) -- Deduplication and entry limit control -- Text previews with customizable width - ## Usage Command interface is only slightly different from Cliphist. In most cases, it will be as simple as replacing `cliphist` with `stash` in your commands, aliases or scripts. +> [!NOTE] +> It is not a priority to provide 1:1 backwards compatibility with Cliphist. +> While the interface is _almost_ identical, Stash chooses to build upon +> Cliphist's design and extend existing design choices. See +> [Migrating from Cliphist](#migrating-from-cliphist) for more details. + ### Store an entry ```bash @@ -132,11 +139,41 @@ commands `--help` text for more details. The following are generally standard: - `--preview-width `: Text preview max width for `list` - `--version`: Print the current version and exit +#### Sensitive Clipboard Filtering + +Stash can be configured to avoid storing clipboard entries that match a +sensitive pattern, using a regular expression. This is useful for preventing +accidental storage of secrets, passwords, or other sensitive data. You don't +want sensitive data ending up in your persistent clipboard, right? + +The filter can be configured in one of two ways: + +- **Environment variable**: Set `STASH_SENSITIVE_REGEX` to a valid regex + pattern. If clipboard text matches, it will not be stored. +- **Systemd LoadCredential**: If running as a service, you can provide a regex + pattern via a credential file. For example, add to your `stash.service`: + + ```ini + LoadCredential=clipboard_filter:/etc/stash/clipboard_filter + ``` + + The file `/etc/stash/clipboard_filter` should contain your regex pattern (no + quotes). This is done automatically in the vendored Systemd service. Remember + to set the appropriate file permissions if using this option. + +The service will check the credential file first, then the environment variable. +If a clipboard entry matches the regex, it will be skipped and a warning will be +logged. + +**Example regex to block common password patterns**: + +- `(password|secret|api[_-]?key|token)[=: ]+[^\s]+` + ## Tips & Tricks ### Migrating from Cliphist -Stash is designed to be a drop-in replacement for Cliphist, with only minor +Stash was designed to be a drop-in replacement for Cliphist, with only minor improvements. If you are migrating from Cliphist, here are a few things you should know. From 261b154527053433ed8107432a0499bfa91dda80 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 17:22:46 +0300 Subject: [PATCH 5/6] stash: use LoadCrediental in the vendored Systemd service Signed-off-by: NotAShelf Change-Id: I6a6a6964e8080cc20ea3c78073141a1978f0b1f1 --- vendor/stash.service | 1 + 1 file changed, 1 insertion(+) diff --git a/vendor/stash.service b/vendor/stash.service index c493c78..64c3d3c 100644 --- a/vendor/stash.service +++ b/vendor/stash.service @@ -8,6 +8,7 @@ Requisite=graphical-session.target Type=simple ExecStart=stash watch Restart=on-failure +LoadCredential=clipboard_filter:/etc/stash/clipboard_filter [Install] WantedBy=graphical-session.target From 0547376a9e1ac1a4c90ddd389294d277aee3f2f7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 14 Aug 2025 17:28:36 +0300 Subject: [PATCH 6/6] chore: apply clippy fixes; suppress "too many lines" lint Signed-off-by: NotAShelf Change-Id: I6a6a696476135218b9979677c66c4be4d96aced8 --- src/db/mod.rs | 2 +- src/main.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 6b64cf7..52dee84 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -397,7 +397,7 @@ impl ClipboardDb for SqliteClipboardDb { /// `Some(Regex)` if present and valid, `None` otherwise. fn load_sensitive_regex() -> Option { if let Ok(regex_path) = env::var("CREDENTIALS_DIRECTORY") { - let file = format!("{}/clipboard_filter", regex_path); + let file = format!("{regex_path}/clipboard_filter"); if let Ok(contents) = fs::read_to_string(&file) { if let Ok(re) = Regex::new(contents.trim()) { return Some(re); diff --git a/src/main.rs b/src/main.rs index 1e35a8d..d4a1255 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,6 +110,7 @@ fn report_error(result: Result, context: &str) -> } } +#[allow(clippy::too_many_lines)] // whatever fn main() { smol::block_on(async { let cli = Cli::parse();