Merge pull request #16 from NotAShelf/notashelf/push-qwkxlsnpqyyo

stash: improvements to import command
This commit is contained in:
raf 2025-08-20 09:59:07 +03:00 committed by GitHub
commit bafe272a83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1584 additions and 885 deletions

27
.rustfmt.toml Normal file
View file

@ -0,0 +1,27 @@
condense_wildcard_suffixes = true
doc_comment_code_block_width = 80
edition = "2024" # Keep in sync with Cargo.toml.
enum_discrim_align_threshold = 60
force_explicit_abi = false
force_multiline_blocks = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Upper"
imports_granularity = "Crate"
imports_layout = "HorizontalVertical"
inline_attribute_width = 60
match_block_trailing_comma = true
max_width = 80
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 60
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true

326
Cargo.lock generated
View file

@ -11,6 +11,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.20" version = "0.6.20"
@ -186,6 +192,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -229,6 +246,21 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.32" version = "1.2.32"
@ -300,6 +332,20 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -309,6 +355,15 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@ -324,13 +379,47 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"crossterm_winapi", "crossterm_winapi",
"libc", "libc",
"mio", "mio 0.8.11",
"parking_lot", "parking_lot",
"signal-hook", "signal-hook",
"signal-hook-mio", "signal-hook-mio",
"winapi", "winapi",
] ]
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.1",
"crossterm_winapi",
"mio 1.0.4",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.9.1",
"crossterm_winapi",
"derive_more",
"document-features",
"mio 1.0.4",
"parking_lot",
"rustix 1.0.8",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.9.1" version = "0.9.1"
@ -340,6 +429,62 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@ -361,6 +506,15 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "1.2.1" version = "1.2.1"
@ -373,6 +527,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.3" version = "0.1.3"
@ -457,6 +617,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@ -526,6 +692,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@ -544,12 +712,27 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "imagesize" name = "imagesize"
version = "0.14.0" version = "0.14.0"
@ -566,6 +749,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]] [[package]]
name = "inquire" name = "inquire"
version = "0.7.5" version = "0.7.5"
@ -573,13 +762,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"crossterm", "crossterm 0.25.0",
"dyn-clone", "dyn-clone",
"fxhash", "fxhash",
"newline-converter", "newline-converter",
"once_cell", "once_cell",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.1.14",
]
[[package]]
name = "instability"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -588,6 +790,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -657,6 +868,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litrs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.13" version = "0.4.13"
@ -673,6 +890,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.5" version = "2.7.5"
@ -697,6 +923,18 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "newline-converter" name = "newline-converter"
version = "0.3.0" version = "0.3.0"
@ -829,7 +1067,7 @@ checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"hermit-abi", "hermit-abi 0.5.2",
"pin-project-lite", "pin-project-lite",
"rustix 1.0.8", "rustix 1.0.8",
"windows-sys 0.60.2", "windows-sys 0.60.2",
@ -883,6 +1121,27 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "ratatui"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.9.1",
"cassowary",
"compact_str",
"crossterm 0.28.1",
"indoc",
"instability",
"itertools",
"lru",
"paste",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.17" version = "0.5.17"
@ -994,6 +1253,12 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -1061,7 +1326,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [ dependencies = [
"libc", "libc",
"mio", "mio 0.8.11",
"mio 1.0.4",
"signal-hook", "signal-hook",
] ]
@ -1107,14 +1373,17 @@ dependencies = [
name = "stash" name = "stash"
version = "0.2.4" version = "0.2.4"
dependencies = [ dependencies = [
"atty",
"base64", "base64",
"clap", "clap",
"clap-verbosity-flag", "clap-verbosity-flag",
"crossterm 0.29.0",
"dirs", "dirs",
"env_logger", "env_logger",
"imagesize", "imagesize",
"inquire", "inquire",
"log", "log",
"ratatui",
"regex", "regex",
"rmp-serde", "rmp-serde",
"rusqlite", "rusqlite",
@ -1122,15 +1391,45 @@ dependencies = [
"serde_json", "serde_json",
"smol", "smol",
"thiserror", "thiserror",
"unicode-segmentation",
"unicode-width 0.2.0",
"wl-clipboard-rs", "wl-clipboard-rs",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.105" version = "2.0.105"
@ -1199,12 +1498,29 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.14" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"

View file

@ -27,7 +27,11 @@ serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142" serde_json = "1.0.142"
base64 = "0.22.1" base64 = "0.22.1"
regex = "1.11.1" regex = "1.11.1"
ratatui = "0.29.0"
atty = "0.2.14"
crossterm = "0.29.0"
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"
[profile.release] [profile.release]
lto = true lto = true

View file

@ -1,20 +1,25 @@
{ {
mkShell, mkShell,
rust-analyzer,
rustfmt,
rustc, rustc,
clippy,
cargo, cargo,
rustfmt,
clippy,
taplo,
rust-analyzer-unwrapped,
rustPlatform, rustPlatform,
}: }:
mkShell { mkShell {
name = "rust"; name = "rust";
packages = [ packages = [
rust-analyzer rustc
rustfmt cargo
(rustfmt.override {asNightly = true;})
clippy clippy
cargo cargo
rustc taplo
rust-analyzer-unwrapped
]; ];
RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";

View file

@ -1,10 +1,9 @@
use crate::db::{ClipboardDb, SqliteClipboardDb};
use std::io::{Read, Write}; use std::io::{Read, Write};
use crate::db::StashError;
use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents}; use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents};
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
pub trait DecodeCommand { pub trait DecodeCommand {
fn decode( fn decode(
&self, &self,
@ -50,10 +49,14 @@ impl DecodeCommand for SqliteClipboardDb {
} }
// Try decode as usual // Try decode as usual
match self.decode_entry(input_str.as_bytes(), &mut out, Some(input_str.clone())) { match self.decode_entry(
input_str.as_bytes(),
&mut out,
Some(input_str.clone()),
) {
Ok(()) => { Ok(()) => {
log::info!("Entry decoded"); log::info!("Entry decoded");
} },
Err(e) => { Err(e) => {
log::error!("Failed to decode entry: {e}"); log::error!("Failed to decode entry: {e}");
if let Ok((mut reader, _mime)) = if let Ok((mut reader, _mime)) =
@ -68,7 +71,7 @@ impl DecodeCommand for SqliteClipboardDb {
} else { } else {
log::error!("Failed to get clipboard contents for relay"); log::error!("Failed to get clipboard contents for relay");
} }
} },
} }
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
use std::io::Read; use std::io::Read;
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
pub trait DeleteCommand { pub trait DeleteCommand {
fn delete(&self, input: impl Read) -> Result<usize, StashError>; fn delete(&self, input: impl Read) -> Result<usize, StashError>;
} }
@ -12,11 +12,11 @@ impl DeleteCommand for SqliteClipboardDb {
Ok(deleted) => { Ok(deleted) => {
log::info!("Deleted {deleted} entries"); log::info!("Deleted {deleted} entries");
Ok(deleted) Ok(deleted)
} },
Err(e) => { Err(e) => {
log::error!("Failed to delete entries: {e}"); log::error!("Failed to delete entries: {e}");
Err(e) Err(e)
} },
} }
} }
} }

View file

@ -1,14 +1,286 @@
use crate::db::{ClipboardDb, SqliteClipboardDb};
use std::io::Write; use std::io::Write;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
pub trait ListCommand { pub trait ListCommand {
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; fn list(&self, out: impl Write, preview_width: u32)
-> Result<(), StashError>;
} }
impl ListCommand for SqliteClipboardDb { impl ListCommand for SqliteClipboardDb {
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { fn list(
&self,
out: impl Write,
preview_width: u32,
) -> Result<(), StashError> {
self.list_entries(out, preview_width)?; self.list_entries(out, preview_width)?;
log::info!("Listed clipboard entries"); log::info!("Listed clipboard entries");
Ok(()) Ok(())
} }
} }
impl SqliteClipboardDb {
/// Public TUI listing function for use in main.rs
#[allow(clippy::too_many_lines)]
pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> {
use std::io::stdout;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{
EnterAlternateScreen,
LeaveAlternateScreen,
disable_raw_mode,
enable_raw_mode,
},
};
use ratatui::{
Terminal,
backend::CrosstermBackend,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
};
// Query entries from DB
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<(u64, String, String)> = Vec::new();
let mut max_id_width = 2;
let mut max_mime_width = 8;
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<u8> = row
.get(1)
.map_err(|e| StashError::ListDecode(e.to_string()))?;
let mime: Option<String> = row
.get(2)
.map_err(|e| StashError::ListDecode(e.to_string()))?;
let preview =
crate::db::preview_entry(&contents, mime.as_deref(), preview_width);
let mime_str = mime.as_deref().unwrap_or("").to_string();
let id_str = id.to_string();
max_id_width = max_id_width.max(id_str.width());
max_mime_width = max_mime_width.max(mime_str.width());
entries.push((id, preview, mime_str));
}
enable_raw_mode().map_err(|e| StashError::ListDecode(e.to_string()))?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
.map_err(|e| StashError::ListDecode(e.to_string()))?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)
.map_err(|e| StashError::ListDecode(e.to_string()))?;
let mut state = ListState::default();
if !entries.is_empty() {
state.select(Some(0));
}
let res = (|| -> Result<(), StashError> {
loop {
terminal
.draw(|f| {
let area = f.area();
let block = Block::default()
.title("Clipboard Entries (j/k/↑/↓ to move, q/ESC to quit)")
.borders(Borders::ALL);
let border_width = 2;
let highlight_symbol = ">";
let highlight_width = 1;
let content_width = area.width as usize - border_width;
// Minimum widths for columns
let min_id_width = 2;
let min_mime_width = 6;
let min_preview_width = 4;
let spaces = 3; // [id][ ][preview][ ][mime]
// Dynamically allocate widths
let mut id_col = max_id_width.max(min_id_width);
let mut mime_col = max_mime_width.max(min_mime_width);
let mut preview_col = content_width
.saturating_sub(highlight_width)
.saturating_sub(id_col)
.saturating_sub(mime_col)
.saturating_sub(spaces);
// If not enough space, shrink columns
if preview_col < min_preview_width {
let needed = min_preview_width - preview_col;
if mime_col > min_mime_width {
let reduce = mime_col - min_mime_width;
let take = reduce.min(needed);
mime_col -= take;
preview_col += take;
}
}
if preview_col < min_preview_width {
let needed = min_preview_width - preview_col;
if id_col > min_id_width {
let reduce = id_col - min_id_width;
let take = reduce.min(needed);
id_col -= take;
preview_col += take;
}
}
if preview_col < min_preview_width {
preview_col = min_preview_width;
}
let selected = state.selected();
let list_items: Vec<ListItem> = entries
.iter()
.enumerate()
.map(|(i, entry)| {
// Truncate preview by grapheme clusters and display width
let mut preview = String::new();
let mut width = 0;
for g in entry.1.graphemes(true) {
let g_width = UnicodeWidthStr::width(g);
if width + g_width > preview_col {
preview.push('…');
break;
}
preview.push_str(g);
width += g_width;
}
// Truncate and pad mimetype
let mut mime = String::new();
let mut mwidth = 0;
for g in entry.2.graphemes(true) {
let g_width = UnicodeWidthStr::width(g);
if mwidth + g_width > mime_col {
mime.push('…');
break;
}
mime.push_str(g);
mwidth += g_width;
}
// Compose the row as highlight + id + space + preview + space +
// mimetype
let mut spans = Vec::new();
let (id, preview, mime) = entry;
if Some(i) == selected {
spans.push(Span::styled(
highlight_symbol,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!("{id:>id_col$}"),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::raw(" "));
spans.push(Span::styled(
format!("{preview:<preview_col$}"),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::raw(" "));
spans.push(Span::styled(
format!("{mime:>mime_col$}"),
Style::default().fg(Color::Green),
));
} else {
spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{id:>id_col$}")));
spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{preview:<preview_col$}")));
spans.push(Span::raw(" "));
spans.push(Span::raw(format!("{mime:>mime_col$}")));
}
ListItem::new(Line::from(spans))
})
.collect();
let list = List::new(list_items)
.block(block)
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(""); // handled manually
f.render_stateful_widget(list, area, &mut state);
})
.map_err(|e| StashError::ListDecode(e.to_string()))?;
if event::poll(std::time::Duration::from_millis(250))
.map_err(|e| StashError::ListDecode(e.to_string()))?
{
if let Event::Key(key) =
event::read().map_err(|e| StashError::ListDecode(e.to_string()))?
{
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Down | KeyCode::Char('j') => {
let i = match state.selected() {
Some(i) => {
if i >= entries.len() - 1 {
0
} else {
i + 1
}
},
None => 0,
};
state.select(Some(i));
},
KeyCode::Up | KeyCode::Char('k') => {
let i = match state.selected() {
Some(i) => {
if i == 0 {
entries.len() - 1
} else {
i - 1
}
},
None => 0,
};
state.select(Some(i));
},
_ => {},
}
}
}
}
Ok(())
})();
disable_raw_mode().ok();
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)
.ok();
terminal.show_cursor().ok();
res
}
}

View file

@ -1,6 +1,4 @@
use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
use crate::db::StashError;
pub trait QueryCommand { pub trait QueryCommand {
fn query_delete(&self, query: &str) -> Result<usize, StashError>; fn query_delete(&self, query: &str) -> Result<usize, StashError>;

View file

@ -1,7 +1,7 @@
use crate::db::{ClipboardDb, SqliteClipboardDb};
use std::io::Read; use std::io::Read;
use crate::db::{ClipboardDb, SqliteClipboardDb};
pub trait StoreCommand { pub trait StoreCommand {
fn store( fn store(
&self, &self,

View file

@ -1,9 +1,10 @@
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb}; use std::{io::Read, time::Duration};
use smol::Timer; use smol::Timer;
use std::io::Read;
use std::time::Duration;
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents}; use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
pub trait WatchCommand { pub trait WatchCommand {
fn watch(&self, max_dedupe_search: u64, max_items: u64); fn watch(&self, max_dedupe_search: u64, max_items: u64);
} }
@ -64,13 +65,13 @@ impl WatchCommand for SqliteClipboardDb {
// Drop clipboard contents after storing // Drop clipboard contents after storing
last_contents = None; last_contents = None;
} }
} },
Err(e) => { Err(e) => {
let error_msg = e.to_string(); let error_msg = e.to_string();
if !error_msg.contains("empty") { if !error_msg.contains("empty") {
log::error!("Failed to get clipboard contents: {e}"); log::error!("Failed to get clipboard contents: {e}");
} }
} },
} }
Timer::after(Duration::from_millis(500)).await; Timer::after(Duration::from_millis(500)).await;
} }

View file

@ -1,6 +1,4 @@
use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
use crate::db::StashError;
pub trait WipeCommand { pub trait WipeCommand {
fn wipe(&self) -> Result<(), StashError>; fn wipe(&self) -> Result<(), StashError>;

View file

@ -1,20 +1,19 @@
use std::env; use std::{
use std::fmt; env,
use std::fs; fmt,
use std::io::{BufRead, BufReader, Read, Write}; fs,
use std::str; io::{BufRead, BufReader, Read, Write},
str,
};
use base64::{Engine, engine::general_purpose::STANDARD};
use imagesize::{ImageSize, ImageType}; use imagesize::{ImageSize, ImageType};
use log::{error, info, warn}; use log::{error, info, warn};
use regex::Regex; use regex::Regex;
use rusqlite::{Connection, OptionalExtension, params}; use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde_json::json; use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum StashError { pub enum StashError {
@ -67,7 +66,11 @@ pub trait ClipboardDb {
fn trim_db(&self, max: u64) -> Result<(), StashError>; fn trim_db(&self, max: u64) -> Result<(), StashError>;
fn delete_last(&self) -> Result<(), StashError>; fn delete_last(&self) -> Result<(), StashError>;
fn wipe_db(&self) -> Result<(), StashError>; fn wipe_db(&self) -> Result<(), StashError>;
fn list_entries(&self, out: impl Write, preview_width: u32) -> Result<usize, StashError>; fn list_entries(
&self,
out: impl Write,
preview_width: u32,
) -> Result<usize, StashError>;
fn decode_entry( fn decode_entry(
&self, &self,
in_: impl Read, in_: impl Read,
@ -98,7 +101,8 @@ pub struct SqliteClipboardDb {
impl SqliteClipboardDb { impl SqliteClipboardDb {
pub fn new(conn: Connection) -> Result<Self, StashError> { pub fn new(conn: Connection) -> Result<Self, StashError> {
conn.execute_batch( conn
.execute_batch(
"CREATE TABLE IF NOT EXISTS clipboard ( "CREATE TABLE IF NOT EXISTS clipboard (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
contents BLOB NOT NULL, contents BLOB NOT NULL,
@ -138,7 +142,7 @@ impl SqliteClipboardDb {
let contents_str = match mime.as_deref() { let contents_str = match mime.as_deref() {
Some(m) if m.starts_with("text/") || m == "application/json" => { Some(m) if m.starts_with("text/") || m == "application/json" => {
String::from_utf8_lossy(&contents).to_string() String::from_utf8_lossy(&contents).to_string()
} },
_ => STANDARD.encode(&contents), _ => STANDARD.encode(&contents),
}; };
entries.push(json!({ entries.push(json!({
@ -148,7 +152,8 @@ impl SqliteClipboardDb {
})); }));
} }
serde_json::to_string_pretty(&entries).map_err(|e| StashError::ListDecode(e.to_string())) serde_json::to_string_pretty(&entries)
.map_err(|e| StashError::ListDecode(e.to_string()))
} }
} }
@ -160,7 +165,10 @@ impl ClipboardDb for SqliteClipboardDb {
max_items: u64, max_items: u64,
) -> Result<u64, StashError> { ) -> Result<u64, StashError> {
let mut buf = Vec::new(); let mut buf = Vec::new();
if input.read_to_end(&mut buf).is_err() || buf.is_empty() || buf.len() > 5 * 1_000_000 { if input.read_to_end(&mut buf).is_err()
|| buf.is_empty()
|| buf.len() > 5 * 1_000_000
{
return Err(StashError::EmptyOrTooLarge); return Err(StashError::EmptyOrTooLarge);
} }
if buf.iter().all(u8::is_ascii_whitespace) { if buf.iter().all(u8::is_ascii_whitespace) {
@ -175,7 +183,7 @@ impl ClipboardDb for SqliteClipboardDb {
} else { } else {
None None
} }
} },
other => other, other => other,
}; };
@ -186,14 +194,17 @@ impl ClipboardDb for SqliteClipboardDb {
if let Ok(s) = std::str::from_utf8(&buf) { if let Ok(s) = std::str::from_utf8(&buf) {
if re.is_match(s) { if re.is_match(s) {
warn!("Clipboard entry matches sensitive regex, skipping store."); warn!("Clipboard entry matches sensitive regex, skipping store.");
return Err(StashError::Store("Filtered by sensitive regex".to_string())); return Err(StashError::Store(
"Filtered by sensitive regex".to_string(),
));
} }
} }
} }
self.deduplicate(&buf, max_dedupe_search)?; self.deduplicate(&buf, max_dedupe_search)?;
self.conn self
.conn
.execute( .execute(
"INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)", "INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)",
params![buf, mime], params![buf, mime],
@ -224,7 +235,8 @@ impl ClipboardDb for SqliteClipboardDb {
.get(1) .get(1)
.map_err(|e| StashError::DeduplicationDecode(e.to_string()))?; .map_err(|e| StashError::DeduplicationDecode(e.to_string()))?;
if contents == buf { if contents == buf {
self.conn self
.conn
.execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .execute("DELETE FROM clipboard WHERE id = ?1", params![id])
.map_err(|e| StashError::DeduplicationRemove(e.to_string()))?; .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?;
deduped += 1; deduped += 1;
@ -240,10 +252,14 @@ impl ClipboardDb for SqliteClipboardDb {
.map_err(|e| StashError::Trim(e.to_string()))?; .map_err(|e| StashError::Trim(e.to_string()))?;
if count > max { if count > max {
let to_delete = count - max; let to_delete = count - max;
self.conn.execute( self
"DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER BY id ASC LIMIT ?1)", .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)], params![i64::try_from(to_delete).unwrap_or(i64::MAX)],
).map_err(|e| StashError::Trim(e.to_string()))?; )
.map_err(|e| StashError::Trim(e.to_string()))?;
} }
Ok(()) Ok(())
} }
@ -259,7 +275,8 @@ impl ClipboardDb for SqliteClipboardDb {
.optional() .optional()
.map_err(|e| StashError::DeleteLast(e.to_string()))?; .map_err(|e| StashError::DeleteLast(e.to_string()))?;
if let Some(id) = id { if let Some(id) = id {
self.conn self
.conn
.execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .execute("DELETE FROM clipboard WHERE id = ?1", params![id])
.map_err(|e| StashError::DeleteLast(e.to_string()))?; .map_err(|e| StashError::DeleteLast(e.to_string()))?;
Ok(()) Ok(())
@ -269,13 +286,18 @@ impl ClipboardDb for SqliteClipboardDb {
} }
fn wipe_db(&self) -> Result<(), StashError> { fn wipe_db(&self) -> Result<(), StashError> {
self.conn self
.conn
.execute("DELETE FROM clipboard", []) .execute("DELETE FROM clipboard", [])
.map_err(|e| StashError::Wipe(e.to_string()))?; .map_err(|e| StashError::Wipe(e.to_string()))?;
Ok(()) Ok(())
} }
fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result<usize, StashError> { fn list_entries(
&self,
mut out: impl Write,
preview_width: u32,
) -> Result<usize, StashError> {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC") .prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC")
@ -315,11 +337,13 @@ impl ClipboardDb for SqliteClipboardDb {
input input
} else { } else {
let mut buf = String::new(); let mut buf = String::new();
in_.read_to_string(&mut buf) in_
.read_to_string(&mut buf)
.map_err(|e| StashError::DecodeRead(e.to_string()))?; .map_err(|e| StashError::DecodeRead(e.to_string()))?;
buf buf
}; };
let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?; let id =
extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?;
let (contents, _mime): (Vec<u8>, Option<String>) = self let (contents, _mime): (Vec<u8>, Option<String>) = self
.conn .conn
.query_row( .query_row(
@ -328,7 +352,8 @@ impl ClipboardDb for SqliteClipboardDb {
|row| Ok((row.get(0)?, row.get(1)?)), |row| Ok((row.get(0)?, row.get(1)?)),
) )
.map_err(|e| StashError::DecodeGet(e.to_string()))?; .map_err(|e| StashError::DecodeGet(e.to_string()))?;
out.write_all(&contents) out
.write_all(&contents)
.map_err(|e| StashError::DecodeWrite(e.to_string()))?; .map_err(|e| StashError::DecodeWrite(e.to_string()))?;
info!("Decoded entry with id {id}"); info!("Decoded entry with id {id}");
Ok(()) Ok(())
@ -354,7 +379,8 @@ impl ClipboardDb for SqliteClipboardDb {
.get(1) .get(1)
.map_err(|e| StashError::QueryDelete(e.to_string()))?; .map_err(|e| StashError::QueryDelete(e.to_string()))?;
if contents.windows(query.len()).any(|w| w == query.as_bytes()) { if contents.windows(query.len()).any(|w| w == query.as_bytes()) {
self.conn self
.conn
.execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .execute("DELETE FROM clipboard WHERE id = ?1", params![id])
.map_err(|e| StashError::QueryDelete(e.to_string()))?; .map_err(|e| StashError::QueryDelete(e.to_string()))?;
deleted += 1; deleted += 1;
@ -368,7 +394,8 @@ impl ClipboardDb for SqliteClipboardDb {
let mut deleted = 0; let mut deleted = 0;
for line in reader.lines().map_while(Result::ok) { for line in reader.lines().map_while(Result::ok) {
if let Ok(id) = extract_id(&line) { if let Ok(id) = extract_id(&line) {
self.conn self
.conn
.execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .execute("DELETE FROM clipboard WHERE id = ?1", params![id])
.map_err(|e| StashError::DeleteEntry(id, e.to_string()))?; .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?;
deleted += 1; deleted += 1;
@ -430,6 +457,21 @@ pub fn detect_mime(data: &[u8]) -> Option<String> {
ImageType::Bmp => "image/bmp", ImageType::Bmp => "image/bmp",
ImageType::Tiff => "image/tiff", ImageType::Tiff => "image/tiff",
ImageType::Webp => "image/webp", ImageType::Webp => "image/webp",
ImageType::Aseprite => "image/x-aseprite",
ImageType::Dds => "image/vnd.ms-dds",
ImageType::Exr => "image/aces",
ImageType::Farbfeld => "image/farbfeld",
ImageType::Hdr => "image/vnd.radiance",
ImageType::Ico => "image/x-icon",
ImageType::Ilbm => "image/ilbm",
ImageType::Jxl => "image/jxl",
ImageType::Ktx2 => "image/ktx2",
ImageType::Pnm => "image/x-portable-anymap",
ImageType::Psd => "image/vnd.adobe.photoshop",
ImageType::Qoi => "image/qoi",
ImageType::Tga => "image/x-tga",
ImageType::Vtf => "image/x-vtf",
ImageType::Heif(_) => "image/heif",
_ => "application/octet-stream", _ => "application/octet-stream",
} }
.to_string(), .to_string(),
@ -461,7 +503,7 @@ pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String {
Err(e) => { Err(e) => {
error!("Failed to decode UTF-8 clipboard data: {e}"); error!("Failed to decode UTF-8 clipboard data: {e}");
"" ""
} },
}; };
let s = s.trim().replace(|c: char| c.is_whitespace(), " "); let s = s.trim().replace(|c: char| c.is_whitespace(), " ");
return truncate(&s, width as usize, ""); return truncate(&s, width as usize, "");

View file

@ -1,7 +1,9 @@
use crate::db::{Entry, SqliteClipboardDb, detect_mime};
use log::{error, info};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use log::{error, info};
use crate::db::{Entry, SqliteClipboardDb, detect_mime};
pub trait ImportCommand { pub trait ImportCommand {
fn import_tsv(&self, input: impl io::Read); fn import_tsv(&self, input: impl io::Read);
} }
@ -34,7 +36,7 @@ impl ImportCommand for SqliteClipboardDb {
Ok(_) => { Ok(_) => {
imported += 1; imported += 1;
info!("Imported entry from TSV"); info!("Imported entry from TSV");
} },
Err(e) => error!("Failed to insert entry: {e}"), Err(e) => error!("Failed to insert entry: {e}"),
} }
} }

View file

@ -5,6 +5,7 @@ use std::{
process, process,
}; };
use atty::Stream;
use clap::{CommandFactory, Parser, Subcommand}; use clap::{CommandFactory, Parser, Subcommand};
use inquire::Confirm; use inquire::Confirm;
@ -12,14 +13,18 @@ mod commands;
mod db; mod db;
mod import; mod import;
use crate::commands::decode::DecodeCommand; use crate::{
use crate::commands::delete::DeleteCommand; commands::{
use crate::commands::list::ListCommand; decode::DecodeCommand,
use crate::commands::query::QueryCommand; delete::DeleteCommand,
use crate::commands::store::StoreCommand; list::ListCommand,
use crate::commands::watch::WatchCommand; query::QueryCommand,
use crate::commands::wipe::WipeCommand; store::StoreCommand,
use crate::import::ImportCommand; watch::WatchCommand,
wipe::WipeCommand,
},
import::ImportCommand,
};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "stash")] #[command(name = "stash")]
@ -28,7 +33,8 @@ struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Option<Command>, command: Option<Command>,
#[arg(long, default_value_t = 750)] /// Maximum number of clipboard entries to keep
#[arg(long, default_value_t = u64::MAX)]
max_items: u64, max_items: u64,
#[arg(long, default_value_t = 100)] #[arg(long, default_value_t = 100)]
@ -63,8 +69,9 @@ enum Command {
/// Decode and output clipboard entry by id /// Decode and output clipboard entry by id
Decode { input: Option<String> }, Decode { input: Option<String> },
/// Delete clipboard entry by id (if numeric), or entries matching a query (if not). /// Delete clipboard entry by id (if numeric), or entries matching a query (if
/// Numeric arguments are treated as ids. Use --type to specify explicitly. /// not). Numeric arguments are treated as ids. Use --type to specify
/// explicitly.
Delete { Delete {
/// Id or query string /// Id or query string
arg: Option<String>, arg: Option<String>,
@ -100,13 +107,16 @@ enum Command {
Watch, Watch,
} }
fn report_error<T>(result: Result<T, impl std::fmt::Display>, context: &str) -> Option<T> { fn report_error<T>(
result: Result<T, impl std::fmt::Display>,
context: &str,
) -> Option<T> {
match result { match result {
Ok(val) => Some(val), Ok(val) => Some(val),
Err(e) => { Err(e) => {
log::error!("{context}: {e}"); log::error!("{context}: {e}");
None None
} },
} }
} }
@ -142,7 +152,7 @@ fn main() {
Err(e) => { Err(e) => {
log::error!("Failed to initialize SQLite database: {e}"); log::error!("Failed to initialize SQLite database: {e}");
process::exit(1); process::exit(1);
} },
}; };
match cli.command { match cli.command {
@ -152,37 +162,49 @@ fn main() {
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state), db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state),
"Failed to store entry", "Failed to store entry",
); );
} },
Some(Command::List { format }) => { Some(Command::List { format }) => {
let format = format.as_deref().unwrap_or("tsv"); match format.as_deref() {
match format { Some("tsv") => {
"tsv" => { report_error(
db.list(io::stdout(), cli.preview_width),
"Failed to list entries",
);
},
Some("json") => {
match db.list_json() {
Ok(json) => {
println!("{json}");
},
Err(e) => {
log::error!("Failed to list entries as JSON: {e}");
},
}
},
Some(other) => {
log::error!("Unsupported format: {other}");
},
None => {
if atty::is(Stream::Stdout) {
report_error(
db.list_tui(cli.preview_width),
"Failed to list entries in TUI",
);
} else {
report_error( report_error(
db.list(io::stdout(), cli.preview_width), db.list(io::stdout(), cli.preview_width),
"Failed to list entries", "Failed to list entries",
); );
} }
},
"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}");
}
}
}
Some(Command::Decode { input }) => { Some(Command::Decode { input }) => {
report_error( report_error(
db.decode(io::stdin(), io::stdout(), input), db.decode(io::stdin(), io::stdout(), input),
"Failed to decode entry", "Failed to decode entry",
); );
} },
Some(Command::Delete { arg, r#type, ask }) => { Some(Command::Delete { arg, r#type, ask }) => {
let mut should_proceed = true; let mut should_proceed = true;
if ask { if ask {
@ -208,10 +230,13 @@ fn main() {
} else { } else {
log::error!("Argument is not a valid id"); log::error!("Argument is not a valid id");
} }
} },
(Some(s), Some("query")) => { (Some(s), Some("query")) => {
report_error(db.query_delete(&s), "Failed to delete entry by query"); report_error(
} db.query_delete(&s),
"Failed to delete entry by query",
);
},
(Some(s), None) => { (Some(s), None) => {
if let Ok(id) = s.parse::<u64>() { if let Ok(id) = s.parse::<u64>() {
use std::io::Cursor; use std::io::Cursor;
@ -225,24 +250,25 @@ fn main() {
"Failed to delete entry by query", "Failed to delete entry by query",
); );
} }
} },
(None, _) => { (None, _) => {
report_error( report_error(
db.delete(io::stdin()), db.delete(io::stdin()),
"Failed to delete entry from stdin", "Failed to delete entry from stdin",
); );
} },
(_, Some(_)) => { (_, Some(_)) => {
log::error!("Unknown type for --type. Use \"id\" or \"query\"."); log::error!("Unknown type for --type. Use \"id\" or \"query\".");
},
} }
} }
} },
}
Some(Command::Wipe { ask }) => { Some(Command::Wipe { ask }) => {
let mut should_proceed = true; let mut should_proceed = true;
if ask { if ask {
should_proceed = should_proceed = Confirm::new(
Confirm::new("Are you sure you want to wipe all clipboard history?") "Are you sure you want to wipe all clipboard history?",
)
.with_default(false) .with_default(false)
.prompt() .prompt()
.unwrap_or(false); .unwrap_or(false);
@ -253,12 +279,15 @@ fn main() {
if should_proceed { if should_proceed {
report_error(db.wipe(), "Failed to wipe database"); report_error(db.wipe(), "Failed to wipe database");
} }
} },
Some(Command::Import { r#type, ask }) => { Some(Command::Import { r#type, ask }) => {
let mut should_proceed = true; let mut should_proceed = true;
if ask { if ask {
should_proceed = Confirm::new("Are you sure you want to import clipboard data? This may overwrite existing entries.") should_proceed = Confirm::new(
"Are you sure you want to import clipboard data? This may \
overwrite existing entries.",
)
.with_default(false) .with_default(false)
.prompt() .prompt()
.unwrap_or(false); .unwrap_or(false);
@ -270,23 +299,25 @@ fn main() {
let format = r#type.as_deref().unwrap_or("tsv"); let format = r#type.as_deref().unwrap_or("tsv");
match format { match format {
"tsv" => { "tsv" => {
db.import_tsv(io::stdin()); if let Err(e) = db.import_tsv(io::stdin(), cli.max_items) {
log::error!("Failed to import TSV: {e}");
} }
},
_ => { _ => {
log::error!("Unsupported import format: {format}"); log::error!("Unsupported import format: {format}");
},
} }
} }
} },
}
Some(Command::Watch) => { Some(Command::Watch) => {
db.watch(cli.max_dedupe_search, cli.max_items); db.watch(cli.max_dedupe_search, cli.max_items);
} },
None => { None => {
if let Err(e) = Cli::command().print_help() { if let Err(e) = Cli::command().print_help() {
log::error!("Failed to print help: {e}"); log::error!("Failed to print help: {e}");
} }
println!(); println!();
} },
} }
}); });
} }