Merge pull request #12 from NotAShelf/notashelf/push-lrzrznnxzlpw

stash: allow confirming destructive operations; add filter
This commit is contained in:
raf 2025-08-14 18:10:38 +03:00 committed by GitHub
commit 40f4c1196d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 475 additions and 76 deletions

274
Cargo.lock generated
View file

@ -198,6 +198,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
@ -309,6 +315,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "dirs"
version = "6.0.0"
@ -336,6 +367,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "env_filter"
version = "0.1.3"
@ -451,6 +488,15 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "getrandom"
version = "0.2.16"
@ -520,6 +566,22 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inquire"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [
"bitflags 2.9.1",
"crossterm",
"dyn-clone",
"fxhash",
"newline-converter",
"once_cell",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -568,7 +630,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"libc",
]
@ -595,6 +657,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
@ -613,6 +685,27 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "newline-converter"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -666,6 +759,29 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
@ -767,6 +883,15 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "redox_syscall"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "redox_users"
version = "0.5.2"
@ -835,7 +960,7 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@ -849,7 +974,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@ -862,7 +987,7 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.4",
@ -875,6 +1000,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
@ -913,6 +1044,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.6"
@ -961,7 +1113,9 @@ dependencies = [
"dirs",
"env_logger",
"imagesize",
"inquire",
"log",
"regex",
"rmp-serde",
"rusqlite",
"serde",
@ -979,9 +1133,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@ -1039,6 +1193,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -1085,7 +1251,7 @@ version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"rustix 1.0.8",
"wayland-backend",
"wayland-scanner",
@ -1097,7 +1263,7 @@ version = "0.32.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@ -1109,7 +1275,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
dependencies = [
"bitflags",
"bitflags 2.9.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@ -1136,12 +1302,43 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@ -1160,6 +1357,21 @@ dependencies = [
"windows-targets 0.53.3",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -1193,6 +1405,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -1205,6 +1423,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -1217,6 +1441,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -1241,6 +1471,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -1253,6 +1489,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -1265,6 +1507,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -1277,6 +1525,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@ -1295,7 +1549,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
"bitflags 2.9.1",
]
[[package]]

View file

@ -9,20 +9,24 @@ repository = "https://github.com/notashelf/stash"
rust-version = "1.85"
[dependencies]
clap = { version = "4.5.44", features = ["derive"] }
clap = { version = "4.5.45", features = ["derive"] }
clap-verbosity-flag = "3.0.3"
dirs = "6.0.0"
serde = { version = "1.0.219", features = ["derive"] }
rmp-serde = "1.3.0"
imagesize = "0.14"
imagesize = "0.14.0"
inquire = { default-features = false, version = "0.7.5", features = [
"crossterm",
] }
log = "0.4.27"
env_logger = "0.11.8"
clap-verbosity-flag = "3.0.3"
thiserror = "2.0.14"
wl-clipboard-rs = "0.9.2"
rusqlite = { version = "0.37.0", features = ["bundled"] }
smol = "2.0.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
base64 = "0.22.1"
regex = "1.11.1"
[profile.release]

View file

@ -4,6 +4,18 @@ Wayland clipboard "manager" with fast persistent history and multi-media
support. Stores and previews clipboard entries (text, images) on the command
line.
## Features
- Stores clipboard entries with automatic MIME detection
- Fast persistent storage using SQLite
- List, search, decode, delete, and wipe clipboard history
- Backwards compatible with Cliphist TSV format
- Import clipboard history from TSV (e.g., from `cliphist list`)
- Image preview (shows dimensions and format)
- Deduplication and entry limit control
- Text previews with customizable width
- Sensitive clipboard filtering via regex (see below)
## Installation
### With Nix
@ -59,23 +71,18 @@ releases are made when a version gets tagged, and are available under
cargo install --git https://github.com/notashelf/stash
```
## Features
- Stores clipboard entries with automatic MIME detection
- Fast persistent storage using SQLite
- List, search, decode, delete, and wipe clipboard history
- Backwards compatible with Cliphist TSV format
- Import clipboard history from TSV (e.g., from `cliphist list`)
- Image preview (shows dimensions and format)
- Deduplication and entry limit control
- Text previews with customizable width
## Usage
Command interface is only slightly different from Cliphist. In most cases, it
will be as simple as replacing `cliphist` with `stash` in your commands, aliases
or scripts.
> [!NOTE]
> It is not a priority to provide 1:1 backwards compatibility with Cliphist.
> While the interface is _almost_ identical, Stash chooses to build upon
> Cliphist's design and extend existing design choices. See
> [Migrating from Cliphist](#migrating-from-cliphist) for more details.
### Store an entry
```bash
@ -132,11 +139,41 @@ commands `--help` text for more details. The following are generally standard:
- `--preview-width <N>`: Text preview max width for `list`
- `--version`: Print the current version and exit
#### Sensitive Clipboard Filtering
Stash can be configured to avoid storing clipboard entries that match a
sensitive pattern, using a regular expression. This is useful for preventing
accidental storage of secrets, passwords, or other sensitive data. You don't
want sensitive data ending up in your persistent clipboard, right?
The filter can be configured in one of two ways:
- **Environment variable**: Set `STASH_SENSITIVE_REGEX` to a valid regex
pattern. If clipboard text matches, it will not be stored.
- **Systemd LoadCredential**: If running as a service, you can provide a regex
pattern via a credential file. For example, add to your `stash.service`:
```ini
LoadCredential=clipboard_filter:/etc/stash/clipboard_filter
```
The file `/etc/stash/clipboard_filter` should contain your regex pattern (no
quotes). This is done automatically in the vendored Systemd service. Remember
to set the appropriate file permissions if using this option.
The service will check the credential file first, then the environment variable.
If a clipboard entry matches the regex, it will be skipped and a warning will be
logged.
**Example regex to block common password patterns**:
- `(password|secret|api[_-]?key|token)[=: ]+[^\s]+`
## Tips & Tricks
### Migrating from Cliphist
Stash is designed to be a drop-in replacement for Cliphist, with only minor
Stash was designed to be a drop-in replacement for Cliphist, with only minor
improvements. If you are migrating from Cliphist, here are a few things you
should know.

View file

@ -1,9 +1,12 @@
use std::env;
use std::fmt;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::str;
use imagesize::{ImageSize, ImageType};
use log::{error, info};
use log::{error, info, warn};
use regex::Regex;
use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
@ -176,6 +179,18 @@ impl ClipboardDb for SqliteClipboardDb {
other => other,
};
// Try to load regex from systemd credential file, then env var
let regex = load_sensitive_regex();
if let Some(re) = regex {
// Only check text data
if let Ok(s) = std::str::from_utf8(&buf) {
if re.is_match(s) {
warn!("Clipboard entry matches sensitive regex, skipping store.");
return Err(StashError::Store("Filtered by sensitive regex".to_string()));
}
}
}
self.deduplicate(&buf, max_dedupe_search)?;
self.conn
@ -375,6 +390,31 @@ impl ClipboardDb for SqliteClipboardDb {
}
// Helper functions
/// Try to load a sensitive regex from systemd credential or env.
///
/// # Returns
/// `Some(Regex)` if present and valid, `None` otherwise.
fn load_sensitive_regex() -> Option<Regex> {
if let Ok(regex_path) = env::var("CREDENTIALS_DIRECTORY") {
let file = format!("{regex_path}/clipboard_filter");
if let Ok(contents) = fs::read_to_string(&file) {
if let Ok(re) = Regex::new(contents.trim()) {
return Some(re);
}
}
}
// Fallback to an environment variable
if let Ok(pattern) = env::var("STASH_SENSITIVE_REGEX") {
if let Ok(re) = Regex::new(&pattern) {
return Some(re);
}
}
None
}
pub fn extract_id(input: &str) -> Result<u64, &'static str> {
let id_str = input.split('\t').next().unwrap_or("");
id_str.parse().map_err(|_| "invalid id")

View file

@ -6,6 +6,7 @@ use std::{
};
use clap::{CommandFactory, Parser, Subcommand};
use inquire::Confirm;
mod commands;
mod db;
@ -39,6 +40,10 @@ struct Cli {
#[arg(long)]
db_path: Option<PathBuf>,
/// Ask for confirmation before destructive operations
#[arg(long)]
ask: bool,
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
}
@ -67,16 +72,28 @@ enum Command {
/// Explicitly specify type: "id" or "query"
#[arg(long, value_parser = ["id", "query"])]
r#type: Option<String>,
/// Ask for confirmation before deleting
#[arg(long)]
ask: bool,
},
/// Wipe all clipboard history
Wipe,
Wipe {
/// Ask for confirmation before wiping
#[arg(long)]
ask: bool,
},
/// Import clipboard data from stdin (default: TSV format)
Import {
/// Explicitly specify format: "tsv" (default)
#[arg(long, value_parser = ["tsv"])]
r#type: Option<String>,
/// Ask for confirmation before importing
#[arg(long)]
ask: bool,
},
/// Watch clipboard for changes and store automatically
@ -93,6 +110,7 @@ fn report_error<T>(result: Result<T, impl std::fmt::Display>, context: &str) ->
}
}
#[allow(clippy::too_many_lines)] // whatever
fn main() {
smol::block_on(async {
let cli = Cli::parse();
@ -144,17 +162,16 @@ fn main() {
"Failed to list entries",
);
}
"json" => {
// Implement JSON output
match db.list_json() {
Ok(json) => {
println!("{json}");
}
Err(e) => {
log::error!("Failed to list entries as JSON: {e}");
}
"json" => match db.list_json() {
Ok(json) => {
println!("{json}");
}
}
Err(e) => {
log::error!("Failed to list entries as JSON: {e}");
}
},
_ => {
log::error!("Unsupported format: {format}");
}
@ -166,52 +183,98 @@ fn main() {
"Failed to decode entry",
);
}
Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) {
(Some(s), Some("id")) => {
if let Ok(id) = s.parse::<u64>() {
use std::io::Cursor;
report_error(
db.delete(Cursor::new(format!("{id}\n"))),
"Failed to delete entry by id",
);
} else {
log::error!("Argument is not a valid id");
Some(Command::Delete { arg, r#type, ask }) => {
let mut should_proceed = true;
if ask {
should_proceed =
Confirm::new("Are you sure you want to delete clipboard entries?")
.with_default(false)
.prompt()
.unwrap_or(false);
if !should_proceed {
log::info!("Aborted by user.");
}
}
(Some(s), Some("query")) => {
report_error(db.query_delete(&s), "Failed to delete entry by query");
}
(Some(s), None) => {
if let Ok(id) = s.parse::<u64>() {
use std::io::Cursor;
report_error(
db.delete(Cursor::new(format!("{id}\n"))),
"Failed to delete entry by id",
);
} else {
report_error(db.query_delete(&s), "Failed to delete entry by query");
if should_proceed {
match (arg, r#type.as_deref()) {
(Some(s), Some("id")) => {
if let Ok(id) = s.parse::<u64>() {
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::<u64>() {
use std::io::Cursor;
report_error(
db.delete(Cursor::new(format!("{id}\n"))),
"Failed to delete entry by id",
);
} else {
report_error(
db.query_delete(&s),
"Failed to delete entry by query",
);
}
}
(None, _) => {
report_error(
db.delete(io::stdin()),
"Failed to delete entry from stdin",
);
}
(_, Some(_)) => {
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
}
}
}
(None, _) => {
report_error(db.delete(io::stdin()), "Failed to delete entry from stdin");
}
Some(Command::Wipe { ask }) => {
let mut should_proceed = true;
if ask {
should_proceed =
Confirm::new("Are you sure you want to wipe all clipboard history?")
.with_default(false)
.prompt()
.unwrap_or(false);
if !should_proceed {
log::info!("Aborted by user.");
}
}
(_, Some(_)) => {
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
if should_proceed {
report_error(db.wipe(), "Failed to wipe database");
}
},
Some(Command::Wipe) => {
report_error(db.wipe(), "Failed to wipe database");
}
Some(Command::Import { r#type }) => {
// Default format is TSV (Cliphist compatible)
let format = r#type.as_deref().unwrap_or("tsv");
match format {
"tsv" => {
db.import_tsv(io::stdin());
Some(Command::Import { r#type, ask }) => {
let mut should_proceed = true;
if ask {
should_proceed = Confirm::new("Are you sure you want to import clipboard data? This may overwrite existing entries.")
.with_default(false)
.prompt()
.unwrap_or(false);
if !should_proceed {
log::info!("Aborted by user.");
}
_ => {
log::error!("Unsupported import format: {format}");
}
if should_proceed {
let format = r#type.as_deref().unwrap_or("tsv");
match format {
"tsv" => {
db.import_tsv(io::stdin());
}
_ => {
log::error!("Unsupported import format: {format}");
}
}
}
}
@ -220,7 +283,7 @@ fn main() {
}
None => {
if let Err(e) = Cli::command().print_help() {
eprintln!("Failed to print help: {e}");
log::error!("Failed to print help: {e}");
}
println!();
}

View file

@ -8,6 +8,7 @@ Requisite=graphical-session.target
Type=simple
ExecStart=stash watch
Restart=on-failure
LoadCredential=clipboard_filter:/etc/stash/clipboard_filter
[Install]
WantedBy=graphical-session.target