From efcbe17d73603f90ce0c025417a380239d9c4b6e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 17:10:33 +0300 Subject: [PATCH 01/15] stash: add `watch` subcommand Watches primary clipboard for changes and stores them in the database. Signed-off-by: NotAShelf Change-Id: I6a6a6964de9949bbe6d8b9301ca97ae785852b46 --- Cargo.lock | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/main.rs | 54 +++++++++ 3 files changed, 350 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2c5b62..635a19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -335,9 +335,15 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.60.2", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "either" version = "1.15.0" @@ -393,6 +399,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "exr" version = "1.73.0" @@ -408,6 +424,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -417,6 +439,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.2" @@ -657,6 +685,18 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "lock_api" version = "0.4.13" @@ -804,6 +844,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -835,6 +885,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -921,6 +981,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -1113,6 +1182,32 @@ dependencies = [ "serde", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1211,6 +1306,7 @@ dependencies = [ "serde", "sled", "thiserror 2.0.14", + "wl-clipboard-rs", ] [[package]] @@ -1249,6 +1345,19 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1334,6 +1443,18 @@ dependencies = [ "winnow", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" +dependencies = [ + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1436,6 +1557,76 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.1", + "rustix 1.0.8", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "pkg-config", +] + [[package]] name = "weezl" version = "0.1.10" @@ -1470,13 +1661,38 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1486,58 +1702,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.0" @@ -1562,6 +1826,25 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 0.38.44", + "tempfile", + "thiserror 2.0.14", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index 0ea81ed..9f5fd97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ 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" diff --git a/src/main.rs b/src/main.rs index c6c39d5..73ddb27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,12 @@ mod commands; mod db; mod import; +use crate::db::ClipboardDb; + +use std::io::Read; +use std::time::Duration; +use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; + use crate::commands::decode::DecodeCommand; use crate::commands::delete::DeleteCommand; use crate::commands::list::ListCommand; @@ -73,6 +79,9 @@ enum Command { #[arg(long, value_parser = ["tsv"])] r#type: Option, }, + + /// Watch clipboard for changes and store automatically + Watch, } fn report_error(result: Result, context: &str) -> Option { @@ -85,6 +94,48 @@ fn report_error(result: Result, context: &str) -> } } +/// Watch clipboard and store changes +fn run_daemon(db: &db::SledClipboardDb, max_dedupe_search: u64, max_items: u64) { + log::info!("Starting clipboard watch daemon (Wayland)"); + + let mut last_contents: Option> = None; + + loop { + match get_contents( + ClipboardType::Regular, + Seat::Unspecified, + wl_clipboard_rs::paste::MimeType::Any, + ) { + Ok((mut reader, mime_type)) => { + let mut buf = Vec::new(); + if let Err(e) = reader.read_to_end(&mut buf) { + log::error!("Failed to read clipboard contents: {e}"); + std::thread::sleep(Duration::from_millis(500)); + continue; + } + // Only store if changed and not empty + if !buf.is_empty() && Some(&buf) != last_contents.as_ref() { + last_contents = Some(buf.clone()); + let mime = Some(mime_type.to_string()); + let entry = db::Entry { + contents: buf, + mime, + }; + let id = db.next_sequence(); + match db.store_entry(&entry.contents[..], max_dedupe_search, max_items) { + Ok(_) => log::info!("Stored new clipboard entry (id: {id})"), + Err(e) => log::error!("Failed to store clipboard entry: {e}"), + } + } + } + Err(e) => { + log::error!("Failed to get clipboard contents: {e}"); + } + } + std::thread::sleep(Duration::from_millis(500)); + } +} + fn main() { let cli = Cli::parse(); env_logger::Builder::new() @@ -174,6 +225,9 @@ fn main() { } } } + Some(Command::Watch) => { + run_daemon(&db, cli.max_dedupe_search, cli.max_items); + } _ => { log::warn!("No subcommand provided"); } From 4f725425fc0a771f3348f57de258c1048d1996be Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 18:54:07 +0300 Subject: [PATCH 02/15] db: switch to sqlite as the primary backend Signed-off-by: NotAShelf Change-Id: I6a6a69648f81d0d094e11a3e0f0a19d3b8eccd5d --- Cargo.lock | 173 +++++++++---------------- Cargo.toml | 2 +- README.md | 4 +- src/commands/decode.rs | 4 +- src/commands/delete.rs | 4 +- src/commands/list.rs | 4 +- src/commands/query.rs | 6 +- src/commands/store.rs | 4 +- src/commands/wipe.rs | 4 +- src/db/mod.rs | 277 ++++++++++++++++++++++------------------- src/import.rs | 24 ++-- src/main.rs | 14 ++- 12 files changed, 246 insertions(+), 274 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 635a19d..3a04cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -456,23 +468,10 @@ dependencies = [ ] [[package]] -name = "fs2" -version = "0.4.3" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" @@ -522,6 +521,18 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -578,15 +589,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -685,6 +687,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -697,16 +710,6 @@ 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" @@ -854,31 +857,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - [[package]] name = "paste" version = "1.0.15" @@ -1105,15 +1083,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -1182,6 +1151,20 @@ dependencies = [ "serde", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1214,12 +1197,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[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" @@ -1270,22 +1247,6 @@ dependencies = [ "quote", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -1303,8 +1264,8 @@ dependencies = [ "image", "log", "rmp-serde", + "rusqlite", "serde", - "sled", "thiserror 2.0.14", "wl-clipboard-rs", ] @@ -1478,6 +1439,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -1633,28 +1600,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" -[[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" diff --git a/Cargo.toml b/Cargo.toml index 9f5fd97..3a3a176 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ rust-version = "1.85" [dependencies] clap = { version = "4.5.44", features = ["derive"] } -sled = "0.34.7" dirs = "6.0.0" serde = { version = "1.0.219", features = ["derive"] } rmp-serde = "1.3.0" @@ -20,3 +19,4 @@ 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" ] } diff --git a/README.md b/README.md index 555d4e8..88c144f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ must handle the migration yourself, with one simple command. ```bash $ cliphist list --db ~/.cache/cliphist/db | stash --import-tsv -# > Imported 750 records from TSV into sled database. +# > Imported 750 records from TSV into database. ``` Alternatively, you may first export from Cliphist and _then_ import the @@ -88,5 +88,5 @@ database. ```bash $ cliphist list --db ~/.cache/cliphist/db > cliphist.tsv $ stash --import-tsv < cliphist.tsv -# > Imported 750 records from TSV into sled database. +# > Imported 750 records from TSV into database. ``` diff --git a/src/commands/decode.rs b/src/commands/decode.rs index b1c94e8..b545f53 100644 --- a/src/commands/decode.rs +++ b/src/commands/decode.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::{Read, Write}; @@ -13,7 +13,7 @@ pub trait DecodeCommand { ) -> Result<(), StashError>; } -impl DecodeCommand for SledClipboardDb { +impl DecodeCommand for SqliteClipboardDb { fn decode( &self, in_: impl Read, diff --git a/src/commands/delete.rs b/src/commands/delete.rs index bd5d5c7..64fd1cf 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb, StashError}; +use crate::db::{ClipboardDb, SqliteClipboardDb, StashError}; use std::io::Read; @@ -6,7 +6,7 @@ pub trait DeleteCommand { fn delete(&self, input: impl Read) -> Result; } -impl DeleteCommand for SledClipboardDb { +impl DeleteCommand for SqliteClipboardDb { fn delete(&self, input: impl Read) -> Result { match self.delete_entries(input) { Ok(deleted) => { diff --git a/src/commands/list.rs b/src/commands/list.rs index 28d42a8..f79f407 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,11 +1,11 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::Write; pub trait ListCommand { fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; } -impl ListCommand for SledClipboardDb { +impl ListCommand for SqliteClipboardDb { fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { self.list_entries(out, preview_width)?; log::info!("Listed clipboard entries"); diff --git a/src/commands/query.rs b/src/commands/query.rs index 981334f..6673648 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::StashError; @@ -6,8 +6,8 @@ pub trait QueryCommand { fn query_delete(&self, query: &str) -> Result; } -impl QueryCommand for SledClipboardDb { +impl QueryCommand for SqliteClipboardDb { fn query_delete(&self, query: &str) -> Result { - ::delete_query(self, query) + ::delete_query(self, query) } } diff --git a/src/commands/store.rs b/src/commands/store.rs index 40db4d0..4e2c769 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::Read; @@ -12,7 +12,7 @@ pub trait StoreCommand { ) -> Result<(), crate::db::StashError>; } -impl StoreCommand for SledClipboardDb { +impl StoreCommand for SqliteClipboardDb { fn store( &self, input: impl Read, diff --git a/src/commands/wipe.rs b/src/commands/wipe.rs index cfde239..d815527 100644 --- a/src/commands/wipe.rs +++ b/src/commands/wipe.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::StashError; @@ -6,7 +6,7 @@ pub trait WipeCommand { fn wipe(&self) -> Result<(), StashError>; } -impl WipeCommand for SledClipboardDb { +impl WipeCommand for SqliteClipboardDb { fn wipe(&self) -> Result<(), StashError> { self.wipe_db()?; log::info!("Database wiped"); diff --git a/src/db/mod.rs b/src/db/mod.rs index 598d70b..1beafbf 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -4,9 +4,9 @@ use std::str; use image::{GenericImageView, ImageFormat}; use log::{error, info}; -use rmp_serde::{decode::from_read, encode::to_vec}; + +use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; -use sled::{Db, IVec}; use thiserror::Error; #[derive(Error, Debug)] @@ -15,8 +15,7 @@ pub enum StashError { EmptyOrTooLarge, #[error("Input is all whitespace, skipping store.")] AllWhitespace, - #[error("Failed to serialize entry: {0}")] - Serialize(String), + #[error("Failed to store entry: {0}")] Store(String), #[error("Error reading entry during deduplication: {0}")] @@ -41,10 +40,7 @@ pub enum StashError { DecodeExtractId(String), #[error("Failed to get entry for decode: {0}")] DecodeGet(String), - #[error("No entry found for id {0}")] - DecodeNoEntry(u64), - #[error("Failed to decode entry: {0}")] - DecodeDecode(String), + #[error("Failed to write decoded entry: {0}")] DecodeWrite(String), #[error("Failed to delete entry during query delete: {0}")] @@ -89,11 +85,25 @@ impl fmt::Display for Entry { } } -pub struct SledClipboardDb { - pub db: Db, +pub struct SqliteClipboardDb { + pub conn: Connection, } -impl ClipboardDb for SledClipboardDb { +impl SqliteClipboardDb { + pub fn new(conn: Connection) -> Result { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS clipboard ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contents BLOB NOT NULL, + mime TEXT + );", + ) + .map_err(|e| StashError::Store(e.to_string()))?; + Ok(Self { conn }) + } +} + +impl ClipboardDb for SqliteClipboardDb { fn store_entry( &self, mut input: impl Read, @@ -112,98 +122,111 @@ impl ClipboardDb for SledClipboardDb { self.deduplicate(&buf, max_dedupe_search)?; - let entry = Entry { - contents: buf.clone(), - mime, - }; - - let id = self.next_sequence(); - let enc = to_vec(&entry).map_err(|e| StashError::Serialize(e.to_string()))?; - - self.db - .insert(u64_to_ivec(id), enc) + self.conn + .execute( + "INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)", + params![buf, mime], + ) .map_err(|e| StashError::Store(e.to_string()))?; + self.trim_db(max_items)?; - Ok(id) + Ok(self.next_sequence()) } fn deduplicate(&self, buf: &[u8], max: u64) -> Result { - let mut count = 0; + let mut stmt = self + .conn + .prepare("SELECT id, contents FROM clipboard ORDER BY id DESC LIMIT ?1") + .map_err(|e| StashError::DeduplicationRead(e.to_string()))?; + let mut rows = stmt + .query(params![i64::try_from(max).unwrap_or(i64::MAX)]) + .map_err(|e| StashError::DeduplicationRead(e.to_string()))?; let mut deduped = 0; - for item in self - .db - .iter() - .rev() - .take(usize::try_from(max).unwrap_or(usize::MAX)) + while let Some(row) = rows + .next() + .map_err(|e| StashError::DeduplicationRead(e.to_string()))? { - let (k, v) = match item { - Ok((k, v)) => (k, v), - Err(e) => return Err(StashError::DeduplicationRead(e.to_string())), - }; - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(e) => return Err(StashError::DeduplicationDecode(e.to_string())), - }; - if entry.contents == buf { - self.db - .remove(k) - .map(|_| { - deduped += 1; - }) + let id: u64 = row + .get(0) + .map_err(|e| StashError::DeduplicationDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::DeduplicationDecode(e.to_string()))?; + if contents == buf { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?; - } - count += 1; - if count >= max { - break; + deduped += 1; } } Ok(deduped) } fn trim_db(&self, max: u64) -> Result<(), StashError> { - let mut keys: Vec<_> = self - .db - .iter() - .rev() - .filter_map(|kv| match kv { - Ok((k, _)) => Some(k), - Err(_e) => None, - }) - .collect(); - if keys.len() as u64 > max { - for k in keys.drain(usize::try_from(max).unwrap_or(0)..) { - self.db - .remove(k) - .map_err(|e| StashError::Trim(e.to_string()))?; - } + let count: u64 = self + .conn + .query_row("SELECT COUNT(*) FROM clipboard", [], |row| row.get(0)) + .map_err(|e| StashError::Trim(e.to_string()))?; + if count > max { + let to_delete = count - max; + self.conn.execute( + "DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER BY id ASC LIMIT ?1)", + params![i64::try_from(to_delete).unwrap_or(i64::MAX)], + ).map_err(|e| StashError::Trim(e.to_string()))?; } Ok(()) } fn delete_last(&self) -> Result<(), StashError> { - if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) { - self.db - .remove(k) - .map(|_| ()) - .map_err(|e| StashError::DeleteLast(e.to_string())) + let id: Option = self + .conn + .query_row( + "SELECT id FROM clipboard ORDER BY id DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map_err(|e| StashError::DeleteLast(e.to_string()))?; + if let Some(id) = id { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) + .map_err(|e| StashError::DeleteLast(e.to_string()))?; + Ok(()) } else { Err(StashError::NoEntriesToDelete) } } fn wipe_db(&self) -> Result<(), StashError> { - self.db.clear().map_err(|e| StashError::Wipe(e.to_string())) + self.conn + .execute("DELETE FROM clipboard", []) + .map_err(|e| StashError::Wipe(e.to_string()))?; + Ok(()) } fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result { + let mut stmt = self + .conn + .prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC") + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string()))?; let mut listed = 0; - for (k, v) in self.db.iter().rev().filter_map(Result::ok) { - let id = ivec_to_u64(&k); - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(e) => return Err(StashError::ListDecode(e.to_string())), - }; - let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); + while let Some(row) = rows + .next() + .map_err(|e| StashError::ListDecode(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mime: Option = row + .get(2) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let preview = preview_entry(&contents, mime.as_deref(), preview_width); if writeln!(out, "{id}\t{preview}").is_ok() { listed += 1; } @@ -226,38 +249,44 @@ impl ClipboardDb for SledClipboardDb { buf }; let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?; - let v = self - .db - .get(u64_to_ivec(id)) - .map_err(|e| StashError::DecodeGet(e.to_string()))? - .ok_or(StashError::DecodeNoEntry(id))?; - let entry: Entry = - from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?; - - out.write_all(&entry.contents) + let (contents, _mime): (Vec, Option) = self + .conn + .query_row( + "SELECT contents, mime FROM clipboard WHERE id = ?1", + params![id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|e| StashError::DecodeGet(e.to_string()))?; + out.write_all(&contents) .map_err(|e| StashError::DecodeWrite(e.to_string()))?; info!("Decoded entry with id {id}"); Ok(()) } fn delete_query(&self, query: &str) -> Result { + let mut stmt = self + .conn + .prepare("SELECT id, contents FROM clipboard") + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; let mut deleted = 0; - for (k, v) in self.db.iter().filter_map(Result::ok) { - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(_) => continue, - }; - if entry - .contents - .windows(query.len()) - .any(|w| w == query.as_bytes()) - { - self.db - .remove(k) - .map(|_| { - deleted += 1; - }) + while let Some(row) = rows + .next() + .map_err(|e| StashError::QueryDelete(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + if contents.windows(query.len()).any(|w| w == query.as_bytes()) { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::QueryDelete(e.to_string()))?; + deleted += 1; } } Ok(deleted) @@ -268,25 +297,24 @@ impl ClipboardDb for SledClipboardDb { let mut deleted = 0; for line in reader.lines().map_while(Result::ok) { if let Ok(id) = extract_id(&line) { - self.db - .remove(u64_to_ivec(id)) - .map(|_| { - deleted += 1; - }) + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?; + deleted += 1; } } Ok(deleted) } fn next_sequence(&self) -> u64 { - let last = self - .db - .iter() - .next_back() - .and_then(std::result::Result::ok) - .map(|(k, _)| ivec_to_u64(&k)); - last.unwrap_or(0) + 1 + match self + .conn + .query_row("SELECT MAX(id) FROM clipboard", [], |row| { + row.get::<_, Option>(0) + }) { + Ok(Some(max_id)) => max_id + 1, + Ok(None) | Err(_) => 1, + } } } @@ -296,20 +324,6 @@ pub fn extract_id(input: &str) -> Result { id_str.parse().map_err(|_| "invalid id") } -pub fn u64_to_ivec(v: u64) -> IVec { - IVec::from(&v.to_be_bytes()[..]) -} - -pub fn ivec_to_u64(v: &IVec) -> u64 { - let arr: [u8; 8] = if let Ok(arr) = v.as_ref().try_into() { - arr - } else { - error!("Failed to convert IVec to u64: invalid length"); - return 0; - }; - u64::from_be_bytes(arr) -} - pub fn detect_mime(data: &[u8]) -> Option { if image::guess_format(data).is_ok() { match image::guess_format(data) { @@ -347,7 +361,13 @@ pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String { ); } } else if mime == "application/json" || mime.starts_with("text/") { - let s = str::from_utf8(data).unwrap_or(""); + let s = match str::from_utf8(data) { + Ok(s) => s, + Err(e) => { + error!("Failed to decode UTF-8 clipboard data: {e}"); + "" + } + }; let s = s.trim().replace(|c: char| c.is_whitespace(), " "); return truncate(&s, width as usize, "…"); } @@ -366,7 +386,12 @@ pub fn truncate(s: &str, max: usize, ellip: &str) -> String { pub fn size_str(size: usize) -> String { let units = ["B", "KiB", "MiB"]; - let mut fsize = f64::from(u32::try_from(size).unwrap_or(u32::MAX)); + let mut fsize = if let Ok(val) = u32::try_from(size) { + f64::from(val) + } else { + error!("Clipboard entry size too large for display: {size}"); + f64::from(u32::MAX) + }; let mut i = 0; while fsize >= 1024.0 && i < units.len() - 1 { fsize /= 1024.0; diff --git a/src/import.rs b/src/import.rs index 2276489..c60d7d6 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,4 +1,4 @@ -use crate::db::{Entry, SledClipboardDb, detect_mime, u64_to_ivec}; +use crate::db::{Entry, SqliteClipboardDb, detect_mime}; use log::{error, info}; use std::io::{self, BufRead}; @@ -6,31 +6,27 @@ pub trait ImportCommand { fn import_tsv(&self, input: impl io::Read); } -impl ImportCommand for SledClipboardDb { +impl ImportCommand for SqliteClipboardDb { fn import_tsv(&self, input: impl io::Read) { let reader = io::BufReader::new(input); let mut imported = 0; for line in reader.lines().map_while(Result::ok) { let mut parts = line.splitn(2, '\t'); if let (Some(id_str), Some(val)) = (parts.next(), parts.next()) { - if let Ok(id) = id_str.parse::() { + if let Ok(_id) = id_str.parse::() { let entry = Entry { contents: val.as_bytes().to_vec(), mime: detect_mime(val.as_bytes()), }; - let enc = match rmp_serde::encode::to_vec(&entry) { - Ok(enc) => enc, - Err(e) => { - error!("Failed to encode entry for id {id}: {e}"); - continue; - } - }; - match self.db.insert(u64_to_ivec(id), enc) { + match self.conn.execute( + "INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)", + rusqlite::params![entry.contents, entry.mime], + ) { Ok(_) => { imported += 1; - info!("Imported entry with id {id}"); + info!("Imported entry from TSV"); } - Err(e) => error!("Failed to insert entry with id {id}: {e}"), + Err(e) => error!("Failed to insert entry: {e}"), } } else { error!("Failed to parse id from line: {id_str}"); @@ -39,6 +35,6 @@ impl ImportCommand for SledClipboardDb { error!("Malformed TSV line: {line:?}"); } } - info!("Imported {imported} records from TSV into sled database."); + info!("Imported {imported} records from TSV into SQLite database."); } } diff --git a/src/main.rs b/src/main.rs index 73ddb27..dcb600f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,7 +95,7 @@ fn report_error(result: Result, context: &str) -> } /// Watch clipboard and store changes -fn run_daemon(db: &db::SledClipboardDb, max_dedupe_search: u64, max_items: u64) { +fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { log::info!("Starting clipboard watch daemon (Wayland)"); let mut last_contents: Option> = None; @@ -149,12 +149,18 @@ fn main() { .join("db") }); - let sled_db = sled::open(&db_path).unwrap_or_else(|e| { - log::error!("Failed to open database: {e}"); + let conn = rusqlite::Connection::open(&db_path).unwrap_or_else(|e| { + log::error!("Failed to open SQLite database: {e}"); process::exit(1); }); - let db = db::SledClipboardDb { db: sled_db }; + let db = match db::SqliteClipboardDb::new(conn) { + Ok(db) => db, + Err(e) => { + log::error!("Failed to initialize SQLite database: {e}"); + process::exit(1); + } + }; match cli.command { Some(Command::Store) => { From d9029ef8b7a5c5f5eebad1ed79affcc818dd4367 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 19:04:58 +0300 Subject: [PATCH 03/15] docs: update README with the watch feature Signed-off-by: NotAShelf Change-Id: I6a6a69640e1c77cc479b40f276ba77b736040504 --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88c144f..1762906 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ line. ## Features - Stores clipboard entries with automatic MIME detection -- Fast persistent storage using sled +- 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`) @@ -42,13 +42,13 @@ stash decode --input "1234" ### Delete entries matching a query ```bash -stash delete-query --query "some text" +stash delete --type query --arg "some text" ``` ### Delete multiple entries by ID (from a file or stdin) ```bash -stash delete < ids.txt +stash delete --type id < ids.txt ``` ### Wipe all entries @@ -57,6 +57,15 @@ stash delete < ids.txt stash wipe ``` +### Watch clipboard for changes and store automatically + +```bash +stash watch +``` + +This runs a daemon that monitors the clipboard and stores new entries +automatically. + ### Options Some commands take additional flags to modify Stash's behavior. See each @@ -79,7 +88,7 @@ must handle the migration yourself, with one simple command. ```bash $ cliphist list --db ~/.cache/cliphist/db | stash --import-tsv -# > Imported 750 records from TSV into database. +# > Imported 750 records from TSV into SQLite database. ``` Alternatively, you may first export from Cliphist and _then_ import the @@ -88,5 +97,5 @@ database. ```bash $ cliphist list --db ~/.cache/cliphist/db > cliphist.tsv $ stash --import-tsv < cliphist.tsv -# > Imported 750 records from TSV into database. +# > Imported 750 records from TSV into SQLite database. ``` From fcaf5fb14fe562d706e499d31ebed2d7767faf52 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 19:11:13 +0300 Subject: [PATCH 04/15] stash: print `--help` text if no subcommand is provided Signed-off-by: NotAShelf Change-Id: I6a6a69646361b7ade52ee73a6aa11be859132a94 --- src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index dcb600f..7adc780 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use std::{ process, }; +use clap::CommandFactory; + use clap::{Parser, Subcommand}; mod commands; @@ -234,8 +236,11 @@ fn main() { Some(Command::Watch) => { run_daemon(&db, cli.max_dedupe_search, cli.max_items); } - _ => { - log::warn!("No subcommand provided"); + None => { + if let Err(e) = Cli::command().print_help() { + eprintln!("Failed to print help: {e}"); + } + println!(); } } } From d6e4f47bddc1f2e65e5a454223fa3512355a4475 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 19:20:45 +0300 Subject: [PATCH 05/15] docs: update README with project comparison; document `watch` feature Signed-off-by: NotAShelf Change-Id: I6a6a696451e174b6ba4f90cfb4c1547ebd2f694b --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1762906..64ddb8f 100644 --- a/README.md +++ b/README.md @@ -75,27 +75,66 @@ commands `--help` text for more details. The following are generally standard: - `--max-items `: Maximum number of entries to keep (oldest trimmed) - `--max-dedupe-search `: Deduplication window size - `--preview-width `: Text preview max width for `list` +- `--version`: Print the current version and exit ## Tips & Tricks ### Migrating from Cliphist -[Cliphist]: https://github.com/sentriz/cliphist +Stash is 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. -Stash is designed to be backwards compatible with [Cliphist]. Though for -brevity, I have elected to skip automatic database migration. Which means you -must handle the migration yourself, with one simple command. +- Most Cliphist commands have direct equivalents in Stash. For example, + `cliphist store` -> `stash store`, `cliphist list` -> `stash list`, etc. +- Cliphist uses `delete-query`; in Stash, you must use + `stash delete --type query --arg "your query"`. +- Both Cliphist and Stash support deleting by ID, including from stdin or a + file. +- Stash respects the `STASH_CLIPBOARD_STATE` environment variable for + sensitive/clear entries, just like Cliphist. The `STASH_` prefix is added for + granularity, you must update your scripts. +- You can export your Cliphist history to TSV and import it into Stash (see + below). +- Stash supports text and image previews, including dimensions and format. +- Stash adds a `watch` command to automatically store clipboard changes. This is + an alternative to `wl-paste --watch cliphist list`. You can avoid shelling out + and depending on `wl-paste` as Stash implements it through `wl-clipboard-rs` + crate. + +### TSV Export and Import + +Both Stash and Cliphist support TSV format for clipboard history. You can export +from Cliphist and import into Stash, or use Stash to export TSV for +interoperability. + +**Export TSV from Cliphist:** ```bash -$ cliphist list --db ~/.cache/cliphist/db | stash --import-tsv -# > Imported 750 records from TSV into SQLite database. +cliphist list --db ~/.cache/cliphist/db > cliphist.tsv ``` -Alternatively, you may first export from Cliphist and _then_ import the -database. +**Import TSV into Stash:** ```bash -$ cliphist list --db ~/.cache/cliphist/db > cliphist.tsv -$ stash --import-tsv < cliphist.tsv -# > Imported 750 records from TSV into SQLite database. +stash --import < cliphist.tsv ``` + +**Export TSV from Stash:** + +```bash +stash list > stash.tsv +``` + +**Import TSV into Cliphist:** + +```bash +cliphist --import < stash.tsv +``` + +### More Tricks + +- Use `stash list` to export your clipboard history in TSV format. This displays + your clipboard in the same format as `cliphist list` +- Use `stash import --type tsv` to import TSV clipboard history from Cliphist or + other tools. From 6c5408abd1c55e35babdbc940cee55af352490e7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 19:40:47 +0300 Subject: [PATCH 06/15] stash: make `watch` loop async Signed-off-by: NotAShelf Change-Id: I6a6a69641a4f39f0368de1bcf5780afbe8e5c0b1 --- src/main.rs | 199 +++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 96 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7adc780..196954c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod import; use crate::db::ClipboardDb; +use smol::Timer; use std::io::Read; use std::time::Duration; use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; @@ -97,8 +98,8 @@ fn report_error(result: Result, context: &str) -> } /// Watch clipboard and store changes -fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { - log::info!("Starting clipboard watch daemon (Wayland)"); +async fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { + log::info!("Starting clipboard watch daemon"); let mut last_contents: Option> = None; @@ -112,7 +113,7 @@ fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64 let mut buf = Vec::new(); if let Err(e) = reader.read_to_end(&mut buf) { log::error!("Failed to read clipboard contents: {e}"); - std::thread::sleep(Duration::from_millis(500)); + Timer::after(Duration::from_millis(500)).await; continue; } // Only store if changed and not empty @@ -131,116 +132,122 @@ fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64 } } Err(e) => { - log::error!("Failed to get clipboard contents: {e}"); + // Only log actual errors, not empty clipboard + let error_msg = e.to_string(); + if !error_msg.contains("empty") { + log::error!("Failed to get clipboard contents: {e}"); + } } } - std::thread::sleep(Duration::from_millis(500)); + Timer::after(Duration::from_millis(500)).await; } } fn main() { - let cli = Cli::parse(); - env_logger::Builder::new() - .filter_level(cli.verbosity.into()) - .init(); + smol::block_on(async { + let cli = Cli::parse(); + env_logger::Builder::new() + .filter_level(cli.verbosity.into()) + .init(); - let db_path = cli.db_path.unwrap_or_else(|| { - dirs::cache_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join("stash") - .join("db") - }); + let db_path = cli.db_path.unwrap_or_else(|| { + dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from("/tmp")) + .join("stash") + .join("db") + }); - let conn = rusqlite::Connection::open(&db_path).unwrap_or_else(|e| { - log::error!("Failed to open SQLite database: {e}"); - process::exit(1); - }); - - let db = match db::SqliteClipboardDb::new(conn) { - Ok(db) => db, - Err(e) => { - log::error!("Failed to initialize SQLite database: {e}"); + let conn = rusqlite::Connection::open(&db_path).unwrap_or_else(|e| { + log::error!("Failed to open SQLite database: {e}"); process::exit(1); - } - }; + }); - match cli.command { - Some(Command::Store) => { - let state = env::var("STASH_CLIPBOARD_STATE").ok(); - report_error( - db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state), - "Failed to store entry", - ); - } - Some(Command::List) => { - report_error( - db.list(io::stdout(), cli.preview_width), - "Failed to list entries", - ); - } - Some(Command::Decode { input }) => { - report_error( - db.decode(io::stdin(), io::stdout(), input), - "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"); + let db = match db::SqliteClipboardDb::new(conn) { + Ok(db) => db, + Err(e) => { + log::error!("Failed to initialize SQLite database: {e}"); + process::exit(1); + } + }; + + match cli.command { + Some(Command::Store) => { + let state = env::var("STASH_CLIPBOARD_STATE").ok(); + report_error( + db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state), + "Failed to store entry", + ); + } + Some(Command::List) => { + report_error( + db.list(io::stdout(), cli.preview_width), + "Failed to list entries", + ); + } + Some(Command::Decode { input }) => { + report_error( + db.decode(io::stdin(), io::stdout(), input), + "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(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 { + (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\"."); + } + }, + Some(Command::Wipe) => { + report_error(db.wipe(), "Failed to wipe database"); } - (None, _) => { - report_error(db.delete(io::stdin()), "Failed to delete entry from stdin"); - } - (_, Some(_)) => { - log::error!("Unknown type for --type. Use \"id\" or \"query\"."); - } - }, - 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()); - } - _ => { - log::error!("Unsupported import format: {format}"); + 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()); + } + _ => { + log::error!("Unsupported import format: {format}"); + } } } - } - Some(Command::Watch) => { - run_daemon(&db, cli.max_dedupe_search, cli.max_items); - } - None => { - if let Err(e) = Cli::command().print_help() { - eprintln!("Failed to print help: {e}"); + Some(Command::Watch) => { + run_daemon(&db, cli.max_dedupe_search, cli.max_items).await; + } + None => { + if let Err(e) = Cli::command().print_help() { + eprintln!("Failed to print help: {e}"); + } + println!(); } - println!(); } - } + }); } From 5bcc23b6f986e1946186036453edd1edc6a2e945 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 21:47:03 +0300 Subject: [PATCH 07/15] stash: don't initialize watch daemon with `None` Fixes the daemon always updating the latest entry. Signed-off-by: NotAShelf Change-Id: I6a6a696489a2e9a0928ff799a4210c160014c485 --- src/main.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 196954c..03b78ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,7 +101,22 @@ fn report_error(result: Result, context: &str) -> async fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { log::info!("Starting clipboard watch daemon"); - let mut last_contents: Option> = None; + // Initialize with current clipboard to avoid duplicating on startup + let mut last_contents: Option> = match get_contents( + ClipboardType::Regular, + Seat::Unspecified, + wl_clipboard_rs::paste::MimeType::Any, + ) { + Ok((mut reader, _)) => { + let mut buf = Vec::new(); + if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { + Some(buf) + } else { + None + } + } + Err(_) => None, + }; loop { match get_contents( From 7d1aa21cdd09401845d821e473e121cfb6dbaa01 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 21:51:22 +0300 Subject: [PATCH 08/15] db: switch from `image` to `imagesize` Signed-off-by: NotAShelf Change-Id: I6a6a6964ab449f38999b5036d6e5554ccae7c049 --- src/db/mod.rs | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 1beafbf..97d0ad0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,7 +2,7 @@ use std::fmt; use std::io::{BufRead, BufReader, Read, Write}; use std::str; -use image::{GenericImageView, ImageFormat}; +use imagesize::{ImageSize, ImageType}; use log::{error, info}; use rusqlite::{Connection, OptionalExtension, params}; @@ -325,23 +325,19 @@ pub fn extract_id(input: &str) -> Result { } pub fn detect_mime(data: &[u8]) -> Option { - if image::guess_format(data).is_ok() { - match image::guess_format(data) { - Ok(fmt) => Some( - match fmt { - ImageFormat::Png => "image/png", - ImageFormat::Jpeg => "image/jpeg", - ImageFormat::Gif => "image/gif", - ImageFormat::Bmp => "image/bmp", - ImageFormat::Tiff => "image/tiff", - _ => "application/octet-stream", - } - .to_string(), - ), - Err(_) => None, - } - } else if data.is_ascii() { - Some("text/plain".into()) + if let Ok(img_type) = imagesize::image_type(data) { + Some( + match img_type { + ImageType::Png => "image/png", + ImageType::Jpeg => "image/jpeg", + ImageType::Gif => "image/gif", + ImageType::Bmp => "image/bmp", + ImageType::Tiff => "image/tiff", + ImageType::Webp => "image/webp", + _ => "application/octet-stream", + } + .to_string(), + ) } else { None } @@ -350,14 +346,17 @@ pub fn detect_mime(data: &[u8]) -> Option { pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String { if let Some(mime) = mime { if mime.starts_with("image/") { - if let Ok(img) = image::load_from_memory(data) { - let (w, h) = img.dimensions(); + if let Ok(ImageSize { + width: img_width, + height: img_height, + }) = imagesize::blob_size(data) + { return format!( "[[ binary data {} {} {}x{} ]]", size_str(data.len()), mime, - w, - h + img_width, + img_height ); } } else if mime == "application/json" || mime.starts_with("text/") { From fbebdf6ed60562e8f132323d9f5fa4bd17446e30 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 21:53:03 +0300 Subject: [PATCH 09/15] meta: optimize release profile; enable LTO Signed-off-by: NotAShelf Change-Id: I6a6a696473887a4d9a526d93c6e1a93de5e3a85e --- Cargo.lock | 1055 +++++++++++++--------------------------------------- Cargo.toml | 11 +- 2 files changed, 262 insertions(+), 804 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a04cad..22ff536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.3" @@ -17,15 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - [[package]] name = "anstream" version = "0.6.20" @@ -77,33 +62,129 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.99" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ - "proc-macro2", - "quote", - "syn", + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] -name = "arrayvec" -version = "0.7.6" +name = "async-executor" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -111,41 +192,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "av1-grain" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - -[[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" @@ -153,28 +199,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] -name = "bitstream-io" -version = "2.6.0" +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" - -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytemuck" -version = "1.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] [[package]] name = "byteorder" @@ -182,33 +217,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "cc" version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.1" @@ -217,9 +234,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "clap" -version = "4.5.44" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -249,9 +266,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -265,12 +282,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.4" @@ -278,29 +289,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "crc32fast" -version = "1.5.0" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -311,12 +303,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "dirs" version = "6.0.0" @@ -344,12 +330,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "env_filter" version = "0.1.3" @@ -373,26 +353,6 @@ dependencies = [ "log", ] -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -410,18 +370,24 @@ dependencies = [ ] [[package]] -name = "exr" -version = "1.73.0" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -442,37 +408,43 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -496,26 +468,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "gif" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -541,43 +493,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "image" -version = "0.25.6" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "image-webp" -version = "0.2.3" +name = "imagesize" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "indexmap" @@ -589,32 +514,12 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "jiff" version = "0.2.15" @@ -639,51 +544,19 @@ dependencies = [ "syn", ] -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - [[package]] name = "libredox" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags", "libc", ] @@ -716,25 +589,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "memchr" version = "2.7.5" @@ -747,22 +601,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nom" version = "7.1.3" @@ -773,53 +611,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -857,6 +648,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "paste" version = "1.0.15" @@ -873,6 +670,23 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -880,16 +694,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "png" -version = "0.17.16" +name = "polling" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -907,15 +722,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.97" @@ -925,40 +731,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" version = "0.37.5" @@ -983,106 +755,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "system-deps", - "thiserror 1.0.69", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -1091,7 +763,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.14", + "thiserror", ] [[package]] @@ -1123,12 +795,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" - [[package]] name = "rmp" version = "0.8.14" @@ -1157,7 +823,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.9.1", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1171,7 +837,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1184,19 +850,13 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", "windows-sys 0.60.2", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "serde" version = "1.0.219" @@ -1217,15 +877,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "shlex" version = "1.3.0" @@ -1233,19 +884,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "simd-adler32" -version = "0.3.7" +name = "signal-hook-registry" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] [[package]] -name = "simd_helpers" -version = "0.1.0" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1253,6 +904,23 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + [[package]] name = "stash" version = "0.1.0" @@ -1261,12 +929,13 @@ dependencies = [ "clap-verbosity-flag", "dirs", "env_logger", - "image", + "imagesize", "log", "rmp-serde", "rusqlite", "serde", - "thiserror 2.0.14", + "smol", + "thiserror", "wl-clipboard-rs", ] @@ -1287,25 +956,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.20.0" @@ -1319,33 +969,13 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.14", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1359,51 +989,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tiff" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tree_magic_mini" version = "3.2.0" @@ -1428,29 +1013,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1466,64 +1034,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - [[package]] name = "wayland-backend" version = "0.3.11" @@ -1543,7 +1053,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", + "bitflags", "rustix 1.0.8", "wayland-backend", "wayland-scanner", @@ -1555,7 +1065,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1567,7 +1077,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1594,12 +1104,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - [[package]] name = "windows-link" version = "0.1.3" @@ -1753,22 +1257,13 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" -[[package]] -name = "winnow" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] @@ -1782,54 +1277,10 @@ dependencies = [ "os_pipe", "rustix 0.38.44", "tempfile", - "thiserror 2.0.14", + "thiserror", "tree_magic_mini", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", ] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 3a3a176..87a335a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,17 @@ clap = { version = "4.5.44", features = ["derive"] } dirs = "6.0.0" serde = { version = "1.0.219", features = ["derive"] } rmp-serde = "1.3.0" -image = "0.25.6" +imagesize = "0.14" 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" ] } +rusqlite = { version = "0.37.0", features = ["bundled"] } +smol = "2.0.2" + + +[profile.release] +lto = true +opt-level = "z" +strip = true From 1a9625ecc3ca6cb0c6d306d73a9cd58065aea7fd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 21:57:20 +0300 Subject: [PATCH 10/15] chore: bump crate version Signed-off-by: NotAShelf Change-Id: I6a6a69645f6727a52e7caefe47fb1adde3de20af --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22ff536..39be59b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -923,7 +923,7 @@ dependencies = [ [[package]] name = "stash" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "clap-verbosity-flag", diff --git a/Cargo.toml b/Cargo.toml index 87a335a..4529591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stash" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["NotAShelf "] license = "MPL-2.0" From d9b0908adaee108d3a20407dfcf825ef648084eb Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 22:17:36 +0300 Subject: [PATCH 11/15] db: allow listing database contents as JSON Signed-off-by: NotAShelf Change-Id: I6a6a6964a756588168f476d984d18f9e8d65bc4e --- Cargo.lock | 32 ++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/db/mod.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 39be59b..0428cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.9.1" @@ -520,6 +526,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jiff" version = "0.2.15" @@ -857,6 +869,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -877,6 +895,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -925,6 +955,7 @@ dependencies = [ name = "stash" version = "0.2.0" dependencies = [ + "base64", "clap", "clap-verbosity-flag", "dirs", @@ -934,6 +965,7 @@ dependencies = [ "rmp-serde", "rusqlite", "serde", + "serde_json", "smol", "thiserror", "wl-clipboard-rs", diff --git a/Cargo.toml b/Cargo.toml index 4529591..dc76816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ thiserror = "2.0.14" wl-clipboard-rs = "0.9.2" rusqlite = { version = "0.37.0", features = ["bundled"] } smol = "2.0.2" +serde_json = "1.0.142" +base64 = "0.22.1" [profile.release] diff --git a/src/db/mod.rs b/src/db/mod.rs index 97d0ad0..c675c98 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -9,6 +9,10 @@ use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use serde_json::json; + #[derive(Error, Debug)] pub enum StashError { #[error("Input is empty or too large, skipping store.")] @@ -103,6 +107,49 @@ impl SqliteClipboardDb { } } +impl SqliteClipboardDb { + pub fn list_json(&self) -> Result { + let mut stmt = self + .conn + .prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC") + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + + let mut entries = Vec::new(); + + while let Some(row) = rows + .next() + .map_err(|e| StashError::ListDecode(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mime: Option = row + .get(2) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let contents_str = match mime.as_deref() { + Some(m) if m.starts_with("text/") || m == "application/json" => { + String::from_utf8_lossy(&contents).to_string() + } + _ => STANDARD.encode(&contents), + }; + entries.push(json!({ + "id": id, + "contents": contents_str, + "mime": mime, + })); + } + + Ok(serde_json::to_string_pretty(&entries) + .map_err(|e| StashError::ListDecode(e.to_string()))?) + } +} + impl ClipboardDb for SqliteClipboardDb { fn store_entry( &self, From 9d40dde63ada6e2b3b80db236aa9e8ff9a3a3dd2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 22:55:39 +0300 Subject: [PATCH 12/15] commands/watch: make it trait-based; move out of main Signed-off-by: NotAShelf Change-Id: I6a6a6964c16396f2013e7f8a5c1a6c0c3bb2aeaa --- src/commands/mod.rs | 1 + src/commands/watch.rs | 79 ++++++++++++++++++++++++++++++ src/main.rs | 110 ++++++++++++------------------------------ 3 files changed, 112 insertions(+), 78 deletions(-) create mode 100644 src/commands/watch.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0e3b925..34294bc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod delete; pub mod list; pub mod query; pub mod store; +pub mod watch; pub mod wipe; diff --git a/src/commands/watch.rs b/src/commands/watch.rs new file mode 100644 index 0000000..dcf334d --- /dev/null +++ b/src/commands/watch.rs @@ -0,0 +1,79 @@ +use crate::db::{ClipboardDb, Entry, SqliteClipboardDb}; +use smol::Timer; +use std::io::Read; +use std::time::Duration; +use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; + +pub trait WatchCommand { + fn watch(&self, max_dedupe_search: u64, max_items: u64); +} + +impl WatchCommand for SqliteClipboardDb { + fn watch(&self, max_dedupe_search: u64, max_items: u64) { + smol::block_on(async { + log::info!("Starting clipboard watch daemon"); + + // Preallocate buffer for clipboard contents + let mut last_contents: Option> = None; + let mut buf = Vec::with_capacity(4096); // reasonable default, hopefully + + // Initialize with current clipboard to avoid duplicating on startup + if let Ok((mut reader, _)) = get_contents( + ClipboardType::Regular, + Seat::Unspecified, + wl_clipboard_rs::paste::MimeType::Any, + ) { + buf.clear(); + if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { + last_contents = Some(buf.clone()); + } + } + + loop { + match get_contents( + ClipboardType::Regular, + Seat::Unspecified, + wl_clipboard_rs::paste::MimeType::Any, + ) { + Ok((mut reader, mime_type)) => { + buf.clear(); + if let Err(e) = reader.read_to_end(&mut buf) { + log::error!("Failed to read clipboard contents: {e}"); + Timer::after(Duration::from_millis(500)).await; + continue; + } + + // Only store if changed and not empty + if !buf.is_empty() && (last_contents.as_ref() != Some(&buf)) { + last_contents = Some(std::mem::take(&mut buf)); + let mime = Some(mime_type.to_string()); + let entry = Entry { + contents: last_contents.as_ref().unwrap().clone(), + mime, + }; + let id = self.next_sequence(); + match self.store_entry( + &entry.contents[..], + max_dedupe_search, + max_items, + ) { + Ok(_) => log::info!("Stored new clipboard entry (id: {id})"), + Err(e) => log::error!("Failed to store clipboard entry: {e}"), + } + + // Drop clipboard contents after storing + last_contents = None; + } + } + Err(e) => { + let error_msg = e.to_string(); + if !error_msg.contains("empty") { + log::error!("Failed to get clipboard contents: {e}"); + } + } + } + Timer::after(Duration::from_millis(500)).await; + } + }); + } +} diff --git a/src/main.rs b/src/main.rs index 03b78ea..0e8128f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,26 +5,18 @@ use std::{ process, }; -use clap::CommandFactory; - -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; mod commands; mod db; mod import; -use crate::db::ClipboardDb; - -use smol::Timer; -use std::io::Read; -use std::time::Duration; -use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; - use crate::commands::decode::DecodeCommand; use crate::commands::delete::DeleteCommand; use crate::commands::list::ListCommand; use crate::commands::query::QueryCommand; use crate::commands::store::StoreCommand; +use crate::commands::watch::WatchCommand; use crate::commands::wipe::WipeCommand; use crate::import::ImportCommand; @@ -57,7 +49,11 @@ enum Command { Store, /// List clipboard history - List, + List { + /// Output format: "tsv" (default) or "json" + #[arg(long, value_parser = ["tsv", "json"])] + format: Option, + }, /// Decode and output clipboard entry by id Decode { input: Option }, @@ -97,67 +93,6 @@ fn report_error(result: Result, context: &str) -> } } -/// Watch clipboard and store changes -async fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { - log::info!("Starting clipboard watch daemon"); - - // Initialize with current clipboard to avoid duplicating on startup - let mut last_contents: Option> = match get_contents( - ClipboardType::Regular, - Seat::Unspecified, - wl_clipboard_rs::paste::MimeType::Any, - ) { - Ok((mut reader, _)) => { - let mut buf = Vec::new(); - if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { - Some(buf) - } else { - None - } - } - Err(_) => None, - }; - - loop { - match get_contents( - ClipboardType::Regular, - Seat::Unspecified, - wl_clipboard_rs::paste::MimeType::Any, - ) { - Ok((mut reader, mime_type)) => { - let mut buf = Vec::new(); - if let Err(e) = reader.read_to_end(&mut buf) { - log::error!("Failed to read clipboard contents: {e}"); - Timer::after(Duration::from_millis(500)).await; - continue; - } - // Only store if changed and not empty - if !buf.is_empty() && Some(&buf) != last_contents.as_ref() { - last_contents = Some(buf.clone()); - let mime = Some(mime_type.to_string()); - let entry = db::Entry { - contents: buf, - mime, - }; - let id = db.next_sequence(); - match db.store_entry(&entry.contents[..], max_dedupe_search, max_items) { - Ok(_) => log::info!("Stored new clipboard entry (id: {id})"), - Err(e) => log::error!("Failed to store clipboard entry: {e}"), - } - } - } - Err(e) => { - // Only log actual errors, not empty clipboard - let error_msg = e.to_string(); - if !error_msg.contains("empty") { - log::error!("Failed to get clipboard contents: {e}"); - } - } - } - Timer::after(Duration::from_millis(500)).await; - } -} - fn main() { smol::block_on(async { let cli = Cli::parse(); @@ -193,11 +128,30 @@ fn main() { "Failed to store entry", ); } - Some(Command::List) => { - report_error( - db.list(io::stdout(), cli.preview_width), - "Failed to list entries", - ); + Some(Command::List { format }) => { + let format = format.as_deref().unwrap_or("tsv"); + match format { + "tsv" => { + report_error( + db.list(io::stdout(), cli.preview_width), + "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); + } + } + } + _ => { + log::error!("Unsupported format: {}", format); + } + } } Some(Command::Decode { input }) => { report_error( @@ -255,7 +209,7 @@ fn main() { } } Some(Command::Watch) => { - run_daemon(&db, cli.max_dedupe_search, cli.max_items).await; + db.watch(cli.max_dedupe_search, cli.max_items); } None => { if let Err(e) = Cli::command().print_help() { From f9457911ae65bcfb5de36b3b62fd455993f823df Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 22:57:03 +0300 Subject: [PATCH 13/15] chore: bump crate version Signed-off-by: NotAShelf Change-Id: I6a6a696454149d06a8bcd3df62e1d9549d921b5b --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0428cb5..bdc43ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "stash" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base64", "clap", diff --git a/Cargo.toml b/Cargo.toml index dc76816..7c1e59f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stash" -version = "0.2.0" +version = "0.2.1" edition = "2024" authors = ["NotAShelf "] license = "MPL-2.0" From 9d0ad95e07fff677135dcefe420ee06649afb0cd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 23:00:18 +0300 Subject: [PATCH 14/15] db: treat valid UTF-8 entries as `text/plain` Makes JSON output more... servicable. Signed-off-by: NotAShelf Change-Id: I6a6a6964f642a02e990979f93f53a8b69764b469 --- src/db/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index c675c98..7b18dee 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -165,7 +165,17 @@ impl ClipboardDb for SqliteClipboardDb { return Err(StashError::AllWhitespace); } - let mime = detect_mime(&buf); + let mime = match detect_mime(&buf) { + None => { + // If valid UTF-8, treat as text/plain + if std::str::from_utf8(&buf).is_ok() { + Some("text/plain".to_string()) + } else { + None + } + } + other => other, + }; self.deduplicate(&buf, max_dedupe_search)?; From 291616e253d1f1addb4f6a154b41f61ff832d002 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 23:00:48 +0300 Subject: [PATCH 15/15] chore: bump crate version Signed-off-by: NotAShelf Change-Id: I6a6a6964832b0d19cf8919ed8503db5463dcdf54 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdc43ff..efbc845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "stash" -version = "0.2.1" +version = "0.2.2" dependencies = [ "base64", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7c1e59f..66772a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stash" -version = "0.2.1" +version = "0.2.2" edition = "2024" authors = ["NotAShelf "] license = "MPL-2.0"