mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-13 06:23:47 +00:00
Merge pull request #43 from NotAShelf/notashelf/push-rnnzunzyvynn
multicall: cleanup; match wl-copy/wl-paste interfaces more closely
This commit is contained in:
commit
5a71640e5f
10 changed files with 1079 additions and 366 deletions
|
|
@ -1,27 +1,26 @@
|
||||||
condense_wildcard_suffixes = true
|
condense_wildcard_suffixes = true
|
||||||
doc_comment_code_block_width = 80
|
doc_comment_code_block_width = 80
|
||||||
edition = "2024" # Keep in sync with Cargo.toml.
|
edition = "2024" # Keep in sync with Cargo.toml.
|
||||||
enum_discrim_align_threshold = 60
|
enum_discrim_align_threshold = 60
|
||||||
force_explicit_abi = false
|
force_explicit_abi = false
|
||||||
force_multiline_blocks = true
|
force_multiline_blocks = true
|
||||||
format_code_in_doc_comments = true
|
format_code_in_doc_comments = true
|
||||||
format_macro_matchers = true
|
format_macro_matchers = true
|
||||||
format_strings = true
|
format_strings = true
|
||||||
group_imports = "StdExternalCrate"
|
group_imports = "StdExternalCrate"
|
||||||
hex_literal_case = "Upper"
|
hex_literal_case = "Upper"
|
||||||
imports_granularity = "Crate"
|
imports_granularity = "Crate"
|
||||||
imports_layout = "HorizontalVertical"
|
imports_layout = "HorizontalVertical"
|
||||||
inline_attribute_width = 60
|
inline_attribute_width = 60
|
||||||
match_block_trailing_comma = true
|
match_block_trailing_comma = true
|
||||||
max_width = 80
|
max_width = 80
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
normalize_comments = true
|
normalize_comments = true
|
||||||
normalize_doc_attributes = true
|
normalize_doc_attributes = true
|
||||||
overflow_delimited_expr = true
|
overflow_delimited_expr = true
|
||||||
struct_field_align_threshold = 60
|
struct_field_align_threshold = 60
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
unstable_features = true
|
unstable_features = true
|
||||||
use_field_init_shorthand = true
|
use_field_init_shorthand = true
|
||||||
use_try_shorthand = true
|
use_try_shorthand = true
|
||||||
wrap_comments = true
|
wrap_comments = true
|
||||||
|
|
||||||
|
|
|
||||||
182
Cargo.lock
generated
182
Cargo.lock
generated
|
|
@ -2,6 +2,21 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -232,6 +247,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 = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -353,6 +383,33 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-eyre"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"color-spantrace",
|
||||||
|
"eyre",
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
"owo-colors",
|
||||||
|
"tracing-error",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-spantrace"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"owo-colors",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-error",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -440,6 +497,17 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctrlc"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3"
|
||||||
|
dependencies = [
|
||||||
|
"dispatch",
|
||||||
|
"nix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
|
|
@ -526,6 +594,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dispatch"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -650,6 +724,16 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eyre"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||||
|
dependencies = [
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -740,6 +824,12 @@ dependencies = [
|
||||||
"wasi 0.14.7+wasi-0.2.4",
|
"wasi 0.14.7+wasi-0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.32.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|
@ -796,6 +886,12 @@ version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indenter"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.11.4"
|
version = "2.11.4"
|
||||||
|
|
@ -883,6 +979,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.177"
|
version = "0.2.177"
|
||||||
|
|
@ -985,6 +1087,15 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1079,6 +1190,15 @@ dependencies = [
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.37.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
|
|
@ -1117,6 +1237,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owo-colors"
|
||||||
|
version = "4.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -1346,6 +1472,12 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.44"
|
version = "0.38.44"
|
||||||
|
|
@ -1444,6 +1576,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -1516,7 +1657,9 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"clap-verbosity-flag",
|
"clap-verbosity-flag",
|
||||||
|
"color-eyre",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
|
"ctrlc",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"imagesize",
|
"imagesize",
|
||||||
|
|
@ -1533,7 +1676,6 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
|
||||||
"wayland-protocols-wlr",
|
"wayland-protocols-wlr",
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
]
|
]
|
||||||
|
|
@ -1628,6 +1770,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.44"
|
version = "0.3.44"
|
||||||
|
|
@ -1706,6 +1857,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-error"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||||
|
dependencies = [
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||||
|
dependencies = [
|
||||||
|
"sharded-slab",
|
||||||
|
"thread_local",
|
||||||
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1772,6 +1945,12 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
@ -1822,6 +2001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"log",
|
||||||
"rustix 1.1.2",
|
"rustix 1.1.2",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
|
|
|
||||||
13
Cargo.toml
13
Cargo.toml
|
|
@ -10,21 +10,23 @@ repository = "https://github.com/notashelf/stash"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "stash" # actual binary name for Nix, Cargo, etc.
|
name = "stash" # actual binary name for Nix, Cargo, etc.
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["use-toplevel", "notifications"]
|
default = ["use-toplevel", "notifications"]
|
||||||
use-toplevel = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr"]
|
use-toplevel = ["dep:wayland-client", "dep:wayland-protocols-wlr"]
|
||||||
notifications = ["dep:notify-rust"]
|
notifications = ["dep:notify-rust"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.48", features = ["derive", "env"] }
|
clap = { version = "4.5.48", features = ["derive", "env"] }
|
||||||
clap-verbosity-flag = "3.0.4"
|
clap-verbosity-flag = "3.0.4"
|
||||||
|
ctrlc = "3.5.0"
|
||||||
|
color-eyre = "0.6.5"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
imagesize = "0.14.0"
|
imagesize = "0.14.0"
|
||||||
inquire = { default-features = false, version = "0.9.1", features = [
|
inquire = { default-features = false, version = "0.9.1", features = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
] }
|
] }
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
|
|
@ -40,9 +42,8 @@ ratatui = "0.29.0"
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.2.0" # FIXME: held back by ratatui
|
unicode-width = "0.2.0" # FIXME: held back by ratatui
|
||||||
wayland-client = { version = "0.31.11", optional = true }
|
wayland-client = { version = "0.31.11", features = ["log"], optional = true }
|
||||||
wayland-protocols = { version = "0.32.0", optional = true }
|
wayland-protocols-wlr = { version = "0.3.9", default-features = false, optional = true }
|
||||||
wayland-protocols-wlr = { version = "0.3.9", optional = true }
|
|
||||||
notify-rust = { version = "4.11.7", optional = true }
|
notify-rust = { version = "4.11.7", optional = true }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
122
README.md
122
README.md
|
|
@ -20,8 +20,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
Wayland clipboard "manager" with fast persistent history and multi-media
|
Lightweight Wayland clipboard "manager" with fast persistent history and
|
||||||
support. Stores and previews clipboard entries (text, images) on the command
|
robust multi-media support. Stores and previews clipboard entries (text, images)
|
||||||
|
on the clipboard with a neat TUI and advanced scripting capabilities.
|
||||||
line.
|
line.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -35,8 +36,8 @@
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Stash is a feature-rich, yet simple clipboard management utility with many
|
Stash is a feature-rich, yet simple and lightweight clipboard management utility
|
||||||
features such as but not limited to:
|
with many features such as but not necessarily limited to:
|
||||||
|
|
||||||
- Automatic MIME detection for stored entries
|
- Automatic MIME detection for stored entries
|
||||||
- Fast persistent storage using SQLite
|
- Fast persistent storage using SQLite
|
||||||
|
|
@ -64,7 +65,7 @@ you are on NixOS.
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
# Add Stash to your inputs like so
|
# Add Stash to your inputs like so
|
||||||
inputs.stash.url = "github:notashelf/stash";
|
inputs.stash.url = "github:NotAShelf/stash";
|
||||||
|
|
||||||
outputs = { /* ... */ };
|
outputs = { /* ... */ };
|
||||||
}
|
}
|
||||||
|
|
@ -86,10 +87,11 @@ in {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also run it one time with `nix run`
|
If you want to give Stash a try before you switch to it, you may also run it one
|
||||||
|
time with `nix run`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix run github:notashelf/stash -- watch # start the watch daemon
|
nix run github:NotAShelf/stash -- watch # start the watch daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without Nix
|
### Without Nix
|
||||||
|
|
@ -98,12 +100,13 @@ nix run github:notashelf/stash -- watch # start the watch daemon
|
||||||
|
|
||||||
You can also install Stash on any of your systems _without_ using Nix. New
|
You can also install Stash on any of your systems _without_ using Nix. New
|
||||||
releases are made when a version gets tagged, and are available under
|
releases are made when a version gets tagged, and are available under
|
||||||
[GitHub Releases]. To install Stash on your system without Nix, eiter:
|
[GitHub Releases]. To install Stash on your system without Nix, either:
|
||||||
|
|
||||||
- Download a tagged release from [GitHub Releases] for your platform and place
|
- Download a tagged release from [GitHub Releases] for your platform and place
|
||||||
the binary in your `$PATH`. Instructions may differ based on your
|
the binary in your `$PATH`. Instructions may differ based on your
|
||||||
distribution, but generally you want to download the built binary from
|
distribution, but generally you want to download the built binary from
|
||||||
releases and put it somewhere like `/usr/bin`.
|
releases and put it somewhere like `/usr/bin` or `~/.local/bin` depending on
|
||||||
|
your distribution.
|
||||||
- Build and install from source with Cargo:
|
- Build and install from source with Cargo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -112,16 +115,63 @@ releases are made when a version gets tagged, and are available under
|
||||||
|
|
||||||
## Usage
|
## 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]
|
> [!NOTE]
|
||||||
> It is not a priority to provide 1:1 backwards compatibility with Cliphist.
|
> It is not a priority to provide 1:1 backwards compatibility with Cliphist.
|
||||||
> While the interface is _almost_ identical, Stash chooses to build upon
|
> While the interface is _almost_ identical, Stash chooses to build upon
|
||||||
> Cliphist's design and extend existing design choices. See
|
> Cliphist's design and extend existing design choices. See
|
||||||
> [Migrating from Cliphist](#migrating-from-cliphist) for more details.
|
> [Migrating from Cliphist](#migrating-from-cliphist) for more details.
|
||||||
|
|
||||||
|
The command interface of Stash is _only slightly_ different from Cliphist. In
|
||||||
|
most cases, you may simply replace `cliphist` with `stash` and your commands,
|
||||||
|
aliases or scripts will continue to work as intended.
|
||||||
|
|
||||||
|
Some of the commands allow further fine-graining with flags such as `--type` or
|
||||||
|
`--format` to allow specific input and output specifiers. See `--help` for
|
||||||
|
individual subcommands if in doubt.
|
||||||
|
|
||||||
|
<!-- markdownlint-disable MD013 -->
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ stash help
|
||||||
|
Wayland clipboard manager
|
||||||
|
|
||||||
|
Usage: stash [OPTIONS] [COMMAND]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
store Store clipboard contents
|
||||||
|
list List clipboard history
|
||||||
|
decode Decode and output clipboard entry by id
|
||||||
|
delete Delete clipboard entry by id (if numeric), or entries matching a query (if not). Numeric arguments are treated as ids. Use --type to specify explicitly
|
||||||
|
wipe Wipe all clipboard history
|
||||||
|
import Import clipboard data from stdin (default: TSV format)
|
||||||
|
watch Start a process to watch clipboard for changes and store automatically
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--max-items <MAX_ITEMS>
|
||||||
|
Maximum number of clipboard entries to keep [default: 18446744073709551615]
|
||||||
|
--max-dedupe-search <MAX_DEDUPE_SEARCH>
|
||||||
|
Number of recent entries to check for duplicates when storing new clipboard data [default: 20]
|
||||||
|
--preview-width <PREVIEW_WIDTH>
|
||||||
|
Maximum width (in characters) for clipboard entry previews in list output [default: 100]
|
||||||
|
--db-path <DB_PATH>
|
||||||
|
Path to the `SQLite` clipboard database file
|
||||||
|
--excluded-apps <EXCLUDED_APPS>
|
||||||
|
Application names to exclude from clipboard history [env: STASH_EXCLUDED_APPS=]
|
||||||
|
--ask
|
||||||
|
Ask for confirmation before destructive operations
|
||||||
|
-v, --verbose...
|
||||||
|
Increase logging verbosity
|
||||||
|
-q, --quiet...
|
||||||
|
Decrease logging verbosity
|
||||||
|
-h, --help
|
||||||
|
Print help
|
||||||
|
-V, --version
|
||||||
|
Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- markdownlint-enable MD013 -->
|
||||||
|
|
||||||
### Store an entry
|
### Store an entry
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -134,18 +184,34 @@ echo "some clipboard text" | stash store
|
||||||
stash list
|
stash list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Stash list will list all entries in an interactive TUI that allows navigation
|
||||||
|
and copying/deleting entries. This behaviour is EXCLUSIVE TO TTYs and Stash will
|
||||||
|
display entries in Cliphist-compatible TSV format in Bash scripts. You may also
|
||||||
|
enforce the output format with `stash list --format <tsv | json>`.
|
||||||
|
|
||||||
### Decode an entry by ID
|
### Decode an entry by ID
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stash decode --input "1234"
|
stash decode <input ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Decoding from dmenu-compatible tools:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> stash list | tofi | stash decode
|
||||||
|
> ```
|
||||||
|
|
||||||
### Delete entries matching a query
|
### Delete entries matching a query
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stash delete --type query --arg "some text"
|
stash delete --type [id | query] <text or ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default stash will try to guess the type of an entry, but this may not be
|
||||||
|
desirable for all users. If you wish to be explicit, pass `--type` to
|
||||||
|
`stash delete`.
|
||||||
|
|
||||||
### Delete multiple entries by ID (from a file or stdin)
|
### Delete multiple entries by ID (from a file or stdin)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -205,7 +271,8 @@ This can be configured in one of two ways. You can use the **environment
|
||||||
variable** `STASTH_SENSITIVE_REGEX` to a valid regex pattern, and if the
|
variable** `STASTH_SENSITIVE_REGEX` to a valid regex pattern, and if the
|
||||||
clipboard text matches the regex it will not be stored. This can be used for
|
clipboard text matches the regex it will not be stored. This can be used for
|
||||||
trivial secrets such as but not limited to GitHub tokens or secrets that follow
|
trivial secrets such as but not limited to GitHub tokens or secrets that follow
|
||||||
a rule, e.g. a prefix.
|
a rule, e.g. a prefix. You would typically set this in your `~/.bashrc` or
|
||||||
|
similar but in some cases this might be a security flaw.
|
||||||
|
|
||||||
The safer alternative to this is using **Systemd LoadCrediental**. If Stash is
|
The safer alternative to this is using **Systemd LoadCrediental**. If Stash is
|
||||||
running as a Systemd service, you can provide a regex pattern using a crediental
|
running as a Systemd service, you can provide a regex pattern using a crediental
|
||||||
|
|
@ -228,6 +295,9 @@ logged.
|
||||||
> **Example regex to block common password patterns**:
|
> **Example regex to block common password patterns**:
|
||||||
>
|
>
|
||||||
> `(password|secret|api[_-]?key|token)[=: ]+[^\s]+`
|
> `(password|secret|api[_-]?key|token)[=: ]+[^\s]+`
|
||||||
|
>
|
||||||
|
> For security reasons, you are recommended to use the regex only for generic
|
||||||
|
> tokens that follow a specific rule, for example a generic prefix or suffix.
|
||||||
|
|
||||||
#### Clipboard Filtering by Application Class
|
#### Clipboard Filtering by Application Class
|
||||||
|
|
||||||
|
|
@ -327,6 +397,26 @@ figured out something new, e.g. a neat shell trick, feel free to add it here!
|
||||||
cliphist list --db ~/.cache/cliphist/db | stash import
|
cliphist list --db ~/.cache/cliphist/db | stash import
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. Stash provides its own implementation of `wl-copy` and `wl-paste` commands
|
||||||
|
backed by `wl-clipboard-rs`. Those implementations are backwards compatible
|
||||||
|
with `wl-clipboard`, and may be used as **drop-in** replacements. The default
|
||||||
|
build wrapper in `build.rs` links `stash` to `stash-copy` and `stash-paste`,
|
||||||
|
which are also available as `wl-copy` and `wl-paste` respectively. The Nix
|
||||||
|
package automatically links those to `$out/bin` for you, which means they are
|
||||||
|
installed by default but other package managers may need additional steps by
|
||||||
|
the packagers. While building from source, you may link
|
||||||
|
`target/release/stash` manually.
|
||||||
|
|
||||||
|
## Attributions
|
||||||
|
|
||||||
|
My thanks go first to [@YaLTeR](https://github.com/YaLTeR/) for the
|
||||||
|
[wl-clipboard-rs](https://github.com/YaLTeR/wl-clipboard-rs) crate. Stash is
|
||||||
|
powered by [several crates](./Cargo.toml), but none of them were as detrimental
|
||||||
|
in Stash's design process.
|
||||||
|
|
||||||
|
Additional thanks to my testers, who have tested earlier versions of Stash and
|
||||||
|
provided feedback. Thank you :)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is made available under Mozilla Public License (MPL) version 2.0.
|
This project is made available under Mozilla Public License (MPL) version 2.0.
|
||||||
|
|
|
||||||
10
build.rs
10
build.rs
|
|
@ -5,16 +5,6 @@ const MULTICALL_LINKS: &[&str] =
|
||||||
&["stash-copy", "stash-paste", "wl-copy", "wl-paste"];
|
&["stash-copy", "stash-paste", "wl-copy", "wl-paste"];
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Only run on Unix-like systems
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
"cargo:warning=Multicall symlinks are only supported on Unix-like \
|
|
||||||
systems."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OUT_DIR is something like .../target/debug/build/<pkg>/out
|
// OUT_DIR is something like .../target/debug/build/<pkg>/out
|
||||||
// We want .../target/debug or .../target/release
|
// We want .../target/debug or .../target/release
|
||||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||||
|
|
|
||||||
53
src/main.rs
53
src/main.rs
|
|
@ -2,7 +2,6 @@ use std::{
|
||||||
env,
|
env,
|
||||||
io::{self, IsTerminal},
|
io::{self, IsTerminal},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{CommandFactory, Parser, Subcommand};
|
||||||
|
|
@ -129,14 +128,27 @@ fn report_error<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)] // whatever
|
#[allow(clippy::too_many_lines)] // whatever
|
||||||
fn main() {
|
fn main() -> color_eyre::eyre::Result<()> {
|
||||||
// Multicall dispatch: stash-copy, stash-paste, wl-copy, wl-paste
|
// Check if we're being called as a multicall binary
|
||||||
if crate::multicall::multicall_dispatch() {
|
let program_name = env::args().next().map(|s| {
|
||||||
// If handled, exit immediately
|
PathBuf::from(s)
|
||||||
std::process::exit(0);
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("stash")
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref name) = program_name {
|
||||||
|
if name == "wl-copy" || name == "stash-copy" {
|
||||||
|
crate::multicall::wl_copy::wl_copy_main()?;
|
||||||
|
return Ok(());
|
||||||
|
} else if name == "wl-paste" || name == "stash-paste" {
|
||||||
|
crate::multicall::wl_paste::wl_paste_main()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not multicall, proceed with normal CLI handling
|
// Normal CLI handling
|
||||||
smol::block_on(async {
|
smol::block_on(async {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
env_logger::Builder::new()
|
env_logger::Builder::new()
|
||||||
|
|
@ -151,24 +163,11 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(parent) = db_path.parent() {
|
if let Some(parent) = db_path.parent() {
|
||||||
if let Err(e) = std::fs::create_dir_all(parent) {
|
std::fs::create_dir_all(parent)?;
|
||||||
log::error!("Failed to create database directory: {e}");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = rusqlite::Connection::open(&db_path).unwrap_or_else(|e| {
|
let conn = rusqlite::Connection::open(&db_path)?;
|
||||||
log::error!("Failed to open SQLite database: {e}");
|
let db = db::SqliteClipboardDb::new(conn)?;
|
||||||
process::exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
match cli.command {
|
||||||
Some(Command::Store) => {
|
Some(Command::Store) => {
|
||||||
|
|
@ -345,12 +344,12 @@ fn main() {
|
||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
if let Err(e) = Cli::command().print_help() {
|
Cli::command().print_help()?;
|
||||||
log::error!("Failed to print help: {e}");
|
|
||||||
}
|
|
||||||
println!();
|
println!();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
282
src/multicall.rs
282
src/multicall.rs
|
|
@ -1,282 +0,0 @@
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
|
|
||||||
use clap::{ArgAction, Parser};
|
|
||||||
use wl_clipboard_rs::paste::{
|
|
||||||
ClipboardType,
|
|
||||||
Error,
|
|
||||||
MimeType,
|
|
||||||
Seat,
|
|
||||||
get_contents,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Dispatch multicall binary logic based on argv[0].
|
|
||||||
/// Returns true if a multicall command was handled and the process should exit.
|
|
||||||
pub fn multicall_dispatch() -> bool {
|
|
||||||
let argv0 = std::env::args().next().unwrap_or_default();
|
|
||||||
let base = std::path::Path::new(&argv0)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
match base {
|
|
||||||
"stash-copy" | "wl-copy" => {
|
|
||||||
multicall_stash_copy();
|
|
||||||
true
|
|
||||||
},
|
|
||||||
"stash-paste" | "wl-paste" => {
|
|
||||||
multicall_stash_paste();
|
|
||||||
true
|
|
||||||
},
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn multicall_stash_copy() {
|
|
||||||
use clap::{ArgAction, Parser};
|
|
||||||
use wl_clipboard_rs::{
|
|
||||||
copy::{ClipboardType, MimeType, Options, ServeRequests, Source},
|
|
||||||
utils::{PrimarySelectionCheckError, is_primary_selection_supported},
|
|
||||||
};
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "stash-copy",
|
|
||||||
about = "Copy clipboard contents on Wayland.",
|
|
||||||
version,
|
|
||||||
disable_help_subcommand = true
|
|
||||||
)]
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
struct Args {
|
|
||||||
/// Serve only a single paste request and then exit
|
|
||||||
#[arg(short = 'o', long = "paste-once", action = ArgAction::SetTrue)]
|
|
||||||
paste_once: bool,
|
|
||||||
/// Stay in the foreground instead of forking
|
|
||||||
#[arg(short = 'f', long = "foreground", action = ArgAction::SetTrue)]
|
|
||||||
foreground: bool,
|
|
||||||
/// Clear the clipboard instead of copying
|
|
||||||
#[arg(short = 'c', long = "clear", action = ArgAction::SetTrue)]
|
|
||||||
clear: bool,
|
|
||||||
/// Use the \"primary\" clipboard
|
|
||||||
#[arg(short = 'p', long = "primary", action = ArgAction::SetTrue)]
|
|
||||||
primary: bool,
|
|
||||||
/// Use the regular clipboard
|
|
||||||
#[arg(short = 'r', long = "regular", action = ArgAction::SetTrue)]
|
|
||||||
regular: bool,
|
|
||||||
/// Trim the trailing newline character before copying
|
|
||||||
#[arg(short = 'n', long = "trim-newline", action = ArgAction::SetTrue)]
|
|
||||||
trim_newline: bool,
|
|
||||||
/// Pick the seat to work with
|
|
||||||
#[arg(short = 's', long = "seat")]
|
|
||||||
seat: Option<String>,
|
|
||||||
/// Override the inferred MIME type for the content
|
|
||||||
#[arg(short = 't', long = "type")]
|
|
||||||
mime_type: Option<String>,
|
|
||||||
/// Enable verbose logging
|
|
||||||
#[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
|
|
||||||
verbose: u8,
|
|
||||||
/// Check if primary selection is supported and exit
|
|
||||||
#[arg(long = "check-primary", action = ArgAction::SetTrue)]
|
|
||||||
check_primary: bool,
|
|
||||||
/// Do not offer additional text mime types (stash extension)
|
|
||||||
#[arg(long = "omit-additional-text-mime-types", action = ArgAction::SetTrue, hide = true)]
|
|
||||||
omit_additional_text_mime_types: bool,
|
|
||||||
/// Number of paste requests to serve before exiting (stash extension)
|
|
||||||
#[arg(short = 'x', long = "serve-requests", hide = true)]
|
|
||||||
serve_requests: Option<usize>,
|
|
||||||
/// Text to copy (if not given, read from stdin)
|
|
||||||
#[arg(value_name = "TEXT TO COPY", action = ArgAction::Append)]
|
|
||||||
text: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
if args.check_primary {
|
|
||||||
match is_primary_selection_supported() {
|
|
||||||
Ok(true) => {
|
|
||||||
log::info!("primary selection is supported.");
|
|
||||||
std::process::exit(0);
|
|
||||||
},
|
|
||||||
Ok(false) => {
|
|
||||||
log::info!("primary selection is NOT supported.");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
Err(PrimarySelectionCheckError::NoSeats) => {
|
|
||||||
log::error!("could not determine: no seats available.");
|
|
||||||
std::process::exit(2);
|
|
||||||
},
|
|
||||||
Err(PrimarySelectionCheckError::MissingProtocol) => {
|
|
||||||
log::error!("data-control protocol not supported by compositor.");
|
|
||||||
std::process::exit(3);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("error checking primary selection support: {e}");
|
|
||||||
std::process::exit(4);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let clipboard = if args.primary {
|
|
||||||
ClipboardType::Primary
|
|
||||||
} else {
|
|
||||||
ClipboardType::Regular
|
|
||||||
};
|
|
||||||
|
|
||||||
let mime_type = if let Some(mt) = args.mime_type.as_deref() {
|
|
||||||
if mt == "text" || mt == "text/plain" {
|
|
||||||
MimeType::Text
|
|
||||||
} else if mt == "autodetect" {
|
|
||||||
MimeType::Autodetect
|
|
||||||
} else {
|
|
||||||
MimeType::Specific(mt.to_string())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MimeType::Autodetect
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut input: Vec<u8> = Vec::new();
|
|
||||||
if args.text.is_empty() {
|
|
||||||
if let Err(e) = std::io::stdin().read_to_end(&mut input) {
|
|
||||||
eprintln!("failed to read stdin: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input = args.text.join(" ").into_bytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut opts = Options::new();
|
|
||||||
opts.clipboard(clipboard);
|
|
||||||
|
|
||||||
if args.trim_newline {
|
|
||||||
opts.trim_newline(true);
|
|
||||||
}
|
|
||||||
if args.foreground {
|
|
||||||
opts.foreground(true);
|
|
||||||
}
|
|
||||||
if let Some(seat) = args.seat.as_deref() {
|
|
||||||
log::debug!(
|
|
||||||
"'--seat' is not supported by stash (using default seat: {seat})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if args.omit_additional_text_mime_types {
|
|
||||||
opts.omit_additional_text_mime_types(true);
|
|
||||||
}
|
|
||||||
// --paste-once overrides serve-requests
|
|
||||||
if args.paste_once {
|
|
||||||
opts.serve_requests(ServeRequests::Only(1));
|
|
||||||
} else if let Some(n) = args.serve_requests {
|
|
||||||
opts.serve_requests(ServeRequests::Only(n));
|
|
||||||
}
|
|
||||||
// --clear
|
|
||||||
if args.clear {
|
|
||||||
// Clear clipboard by setting empty contents
|
|
||||||
if let Err(e) = opts.copy(Source::Bytes(Vec::new().into()), mime_type) {
|
|
||||||
log::error!("failed to clear clipboard: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Err(e) = opts.copy(Source::Bytes(input.into()), mime_type) {
|
|
||||||
log::error!("failed to copy to clipboard: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn multicall_stash_paste() {
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "stash-paste",
|
|
||||||
about = "Paste clipboard contents on Wayland.",
|
|
||||||
version,
|
|
||||||
disable_help_subcommand = true
|
|
||||||
)]
|
|
||||||
struct Args {
|
|
||||||
/// List the offered MIME types instead of pasting
|
|
||||||
#[arg(short = 'l', long = "list-types", action = ArgAction::SetTrue)]
|
|
||||||
list_types: bool,
|
|
||||||
/// Use the "primary" clipboard
|
|
||||||
#[arg(short = 'p', long = "primary", action = ArgAction::SetTrue)]
|
|
||||||
primary: bool,
|
|
||||||
/// Do not append a newline character
|
|
||||||
#[arg(short = 'n', long = "no-newline", action = ArgAction::SetTrue)]
|
|
||||||
no_newline: bool,
|
|
||||||
/// Pick the seat to work with
|
|
||||||
#[arg(short = 's', long = "seat")]
|
|
||||||
seat: Option<String>,
|
|
||||||
/// Request the given MIME type instead of inferring the MIME type
|
|
||||||
#[arg(short = 't', long = "type")]
|
|
||||||
mime_type: Option<String>,
|
|
||||||
/// Enable verbose logging
|
|
||||||
#[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
|
|
||||||
verbose: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
let clipboard = if args.primary {
|
|
||||||
ClipboardType::Primary
|
|
||||||
} else {
|
|
||||||
ClipboardType::Regular
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(seat) = args.seat.as_deref() {
|
|
||||||
log::debug!(
|
|
||||||
"'--seat' is not supported by stash (using default seat: {seat})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.list_types {
|
|
||||||
match get_contents(clipboard, Seat::Unspecified, MimeType::Text) {
|
|
||||||
Ok((_reader, available_types)) => {
|
|
||||||
log::info!("{available_types}");
|
|
||||||
std::process::exit(0);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("failed to list types: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mime_type = match args.mime_type.as_deref() {
|
|
||||||
None | Some("text" | "autodetect") => MimeType::Text,
|
|
||||||
Some(other) => MimeType::Specific(other),
|
|
||||||
};
|
|
||||||
|
|
||||||
match get_contents(clipboard, Seat::Unspecified, mime_type) {
|
|
||||||
Ok((mut reader, _types)) => {
|
|
||||||
let mut out = io::stdout();
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
match reader.read_to_end(&mut buf) {
|
|
||||||
Ok(n) => {
|
|
||||||
if n == 0 && args.no_newline {
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
let _ = out.write_all(&buf);
|
|
||||||
if !args.no_newline && !buf.ends_with(b"\n") {
|
|
||||||
let _ = out.write_all(b"\n");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("failed to read clipboard: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(Error::NoSeats) => {
|
|
||||||
log::error!("no seats available (is a Wayland compositor running?)");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
Err(Error::ClipboardEmpty) => {
|
|
||||||
if args.no_newline {
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(Error::NoMimeType) => {
|
|
||||||
log::error!("clipboard does not contain requested MIME type");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("clipboard error: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
src/multicall/mod.rs
Normal file
6
src/multicall/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Reference documentation:
|
||||||
|
// https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_data_device
|
||||||
|
// https://docs.rs/wl-clipboard-rs/latest/wl_clipboard_rs
|
||||||
|
// https://github.com/YaLTeR/wl-clipboard-rs/blob/master/wl-clipboard-rs-tools/src/bin/wl_copy.rs
|
||||||
|
pub mod wl_copy;
|
||||||
|
pub mod wl_paste;
|
||||||
276
src/multicall/wl_copy.rs
Normal file
276
src/multicall/wl_copy.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
use std::io::{self, Read};
|
||||||
|
|
||||||
|
use clap::{ArgAction, Parser};
|
||||||
|
use color_eyre::eyre::{Context, Result, bail};
|
||||||
|
use wl_clipboard_rs::{
|
||||||
|
copy::{
|
||||||
|
ClipboardType as CopyClipboardType,
|
||||||
|
MimeType as CopyMimeType,
|
||||||
|
Options,
|
||||||
|
Seat as CopySeat,
|
||||||
|
ServeRequests,
|
||||||
|
Source,
|
||||||
|
},
|
||||||
|
utils::{PrimarySelectionCheckError, is_primary_selection_supported},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maximum clipboard content size to prevent memory exhaustion (100MB)
|
||||||
|
const MAX_CLIPBOARD_SIZE: usize = 100 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "wl-copy",
|
||||||
|
about = "Copy clipboard contents on Wayland.",
|
||||||
|
version
|
||||||
|
)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
struct WlCopyArgs {
|
||||||
|
/// Serve only a single paste request and then exit
|
||||||
|
#[arg(short = 'o', long = "paste-once", action = ArgAction::SetTrue)]
|
||||||
|
paste_once: bool,
|
||||||
|
|
||||||
|
/// Stay in the foreground instead of forking
|
||||||
|
#[arg(short = 'f', long = "foreground", action = ArgAction::SetTrue)]
|
||||||
|
foreground: bool,
|
||||||
|
|
||||||
|
/// Clear the clipboard instead of copying
|
||||||
|
#[arg(short = 'c', long = "clear", action = ArgAction::SetTrue)]
|
||||||
|
clear: bool,
|
||||||
|
|
||||||
|
/// Use the "primary" clipboard
|
||||||
|
#[arg(short = 'p', long = "primary", action = ArgAction::SetTrue)]
|
||||||
|
primary: bool,
|
||||||
|
|
||||||
|
/// Use the regular clipboard
|
||||||
|
#[arg(short = 'r', long = "regular", action = ArgAction::SetTrue)]
|
||||||
|
regular: bool,
|
||||||
|
|
||||||
|
/// Trim the trailing newline character before copying
|
||||||
|
#[arg(short = 'n', long = "trim-newline", action = ArgAction::SetTrue)]
|
||||||
|
trim_newline: bool,
|
||||||
|
|
||||||
|
/// Pick the seat to work with
|
||||||
|
#[arg(short = 's', long = "seat")]
|
||||||
|
seat: Option<String>,
|
||||||
|
|
||||||
|
/// Override the inferred MIME type for the content
|
||||||
|
#[arg(short = 't', long = "type")]
|
||||||
|
mime_type: Option<String>,
|
||||||
|
|
||||||
|
/// Enable verbose logging
|
||||||
|
#[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
|
||||||
|
/// Check if primary selection is supported and exit
|
||||||
|
#[arg(long = "check-primary", action = ArgAction::SetTrue)]
|
||||||
|
check_primary: bool,
|
||||||
|
|
||||||
|
/// Do not offer additional text mime types (stash extension)
|
||||||
|
#[arg(long = "omit-additional-text-mime-types", action = ArgAction::SetTrue, hide = true)]
|
||||||
|
omit_additional_text_mime_types: bool,
|
||||||
|
|
||||||
|
/// Number of paste requests to serve before exiting (stash extension)
|
||||||
|
#[arg(short = 'x', long = "serve-requests", hide = true)]
|
||||||
|
serve_requests: Option<usize>,
|
||||||
|
|
||||||
|
/// Text to copy (if not given, read from stdin)
|
||||||
|
#[arg(value_name = "TEXT TO COPY", action = ArgAction::Append)]
|
||||||
|
text: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_check_primary() {
|
||||||
|
let exit_code = match is_primary_selection_supported() {
|
||||||
|
Ok(true) => {
|
||||||
|
log::info!("primary selection is supported.");
|
||||||
|
0
|
||||||
|
},
|
||||||
|
Ok(false) => {
|
||||||
|
log::info!("primary selection is NOT supported.");
|
||||||
|
1
|
||||||
|
},
|
||||||
|
Err(PrimarySelectionCheckError::NoSeats) => {
|
||||||
|
log::error!("could not determine: no seats available.");
|
||||||
|
2
|
||||||
|
},
|
||||||
|
Err(PrimarySelectionCheckError::MissingProtocol) => {
|
||||||
|
log::error!("data-control protocol not supported by compositor.");
|
||||||
|
3
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("error checking primary selection support: {e}");
|
||||||
|
4
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exit with the relevant code
|
||||||
|
std::process::exit(exit_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clipboard_type(primary: bool) -> CopyClipboardType {
|
||||||
|
if primary {
|
||||||
|
CopyClipboardType::Primary
|
||||||
|
} else {
|
||||||
|
CopyClipboardType::Regular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mime_type(mime_arg: Option<&str>) -> CopyMimeType {
|
||||||
|
match mime_arg {
|
||||||
|
Some("text" | "text/plain") => CopyMimeType::Text,
|
||||||
|
Some("autodetect") | None => CopyMimeType::Autodetect,
|
||||||
|
Some(specific) => CopyMimeType::Specific(specific.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_input_data(text_args: &[String]) -> Result<Vec<u8>> {
|
||||||
|
if text_args.is_empty() {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut stdin = io::stdin();
|
||||||
|
|
||||||
|
// Read with size limit to prevent memory exhaustion
|
||||||
|
let mut temp_buffer = [0; 8192];
|
||||||
|
loop {
|
||||||
|
let bytes_read = stdin
|
||||||
|
.read(&mut temp_buffer)
|
||||||
|
.context("failed to read from stdin")?;
|
||||||
|
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.len() + bytes_read > MAX_CLIPBOARD_SIZE {
|
||||||
|
bail!(
|
||||||
|
"input exceeds maximum clipboard size of {} bytes",
|
||||||
|
MAX_CLIPBOARD_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
} else {
|
||||||
|
let content = text_args.join(" ");
|
||||||
|
if content.len() > MAX_CLIPBOARD_SIZE {
|
||||||
|
bail!(
|
||||||
|
"input exceeds maximum clipboard size of {} bytes",
|
||||||
|
MAX_CLIPBOARD_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(content.into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_copy_options(
|
||||||
|
args: &WlCopyArgs,
|
||||||
|
clipboard: CopyClipboardType,
|
||||||
|
) -> Options {
|
||||||
|
let mut opts = Options::new();
|
||||||
|
opts.clipboard(clipboard);
|
||||||
|
opts.seat(
|
||||||
|
args
|
||||||
|
.seat
|
||||||
|
.as_deref()
|
||||||
|
.map_or(CopySeat::All, |s| CopySeat::Specific(s.to_string())),
|
||||||
|
);
|
||||||
|
|
||||||
|
if args.trim_newline {
|
||||||
|
opts.trim_newline(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.omit_additional_text_mime_types {
|
||||||
|
opts.omit_additional_text_mime_types(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.paste_once {
|
||||||
|
opts.serve_requests(ServeRequests::Only(1));
|
||||||
|
} else if let Some(n) = args.serve_requests {
|
||||||
|
opts.serve_requests(ServeRequests::Only(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_clear_clipboard(
|
||||||
|
args: &WlCopyArgs,
|
||||||
|
clipboard: CopyClipboardType,
|
||||||
|
mime_type: CopyMimeType,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut opts = Options::new();
|
||||||
|
opts.clipboard(clipboard);
|
||||||
|
opts.seat(
|
||||||
|
args
|
||||||
|
.seat
|
||||||
|
.as_deref()
|
||||||
|
.map_or(CopySeat::All, |s| CopySeat::Specific(s.to_string())),
|
||||||
|
);
|
||||||
|
|
||||||
|
opts
|
||||||
|
.copy(Source::Bytes(Vec::new().into()), mime_type)
|
||||||
|
.context("failed to clear clipboard")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fork_and_serve(prepared_copy: wl_clipboard_rs::copy::PreparedCopy) {
|
||||||
|
// Use a simpler approach: serve in background thread instead of forking
|
||||||
|
// This avoids all the complexity and safety issues with fork()
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
if let Err(e) = prepared_copy.serve() {
|
||||||
|
log::error!("background clipboard service failed: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the background thread a moment to start
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
log::debug!("clipboard service started in background thread");
|
||||||
|
|
||||||
|
// Detach the thread to allow it to run independently
|
||||||
|
// The thread will be cleaned up when it completes or when the process exits
|
||||||
|
std::mem::forget(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wl_copy_main() -> Result<()> {
|
||||||
|
let args = WlCopyArgs::parse();
|
||||||
|
|
||||||
|
if args.check_primary {
|
||||||
|
handle_check_primary();
|
||||||
|
}
|
||||||
|
|
||||||
|
let clipboard = get_clipboard_type(args.primary);
|
||||||
|
let mime_type = get_mime_type(args.mime_type.as_deref());
|
||||||
|
|
||||||
|
// Handle clear operation
|
||||||
|
if args.clear {
|
||||||
|
handle_clear_clipboard(&args, clipboard, mime_type)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read input data
|
||||||
|
let input =
|
||||||
|
read_input_data(&args.text).context("failed to read input data")?;
|
||||||
|
|
||||||
|
// Configure copy options
|
||||||
|
let opts = configure_copy_options(&args, clipboard);
|
||||||
|
|
||||||
|
// Handle foreground vs background mode
|
||||||
|
if args.foreground {
|
||||||
|
// Foreground mode: copy and serve in current process
|
||||||
|
opts
|
||||||
|
.copy(Source::Bytes(input.into()), mime_type)
|
||||||
|
.context("failed to copy to clipboard")?;
|
||||||
|
} else {
|
||||||
|
// Background mode: spawn child process to serve requests
|
||||||
|
// First prepare to copy to validate before spawning
|
||||||
|
let mut opts_fg = opts.clone();
|
||||||
|
opts_fg.foreground(true);
|
||||||
|
|
||||||
|
let prepared_copy = opts_fg
|
||||||
|
.prepare_copy(Source::Bytes(input.into()), mime_type)
|
||||||
|
.context("failed to prepare copy")?;
|
||||||
|
|
||||||
|
fork_and_serve(prepared_copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
454
src/multicall/wl_paste.rs
Normal file
454
src/multicall/wl_paste.rs
Normal file
|
|
@ -0,0 +1,454 @@
|
||||||
|
// https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_data_device
|
||||||
|
// https://docs.rs/wl-clipboard-rs/latest/wl_clipboard_rs
|
||||||
|
// https://github.com/YaLTeR/wl-clipboard-rs/blob/master/wl-clipboard-rs-tools/src/bin/wl_paste.rs
|
||||||
|
use std::{
|
||||||
|
collections::hash_map::DefaultHasher,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
io::{self, Read, Write},
|
||||||
|
process::{Command, Stdio},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::{ArgAction, Parser};
|
||||||
|
use color_eyre::eyre::{Context, Result, bail};
|
||||||
|
use wl_clipboard_rs::paste::{
|
||||||
|
ClipboardType as PasteClipboardType,
|
||||||
|
Error as PasteError,
|
||||||
|
MimeType as PasteMimeType,
|
||||||
|
Seat as PasteSeat,
|
||||||
|
get_contents,
|
||||||
|
get_mime_types,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch mode timing constants
|
||||||
|
const WATCH_POLL_INTERVAL_MS: u64 = 500;
|
||||||
|
const WATCH_DEBOUNCE_INTERVAL_MS: u64 = 1000;
|
||||||
|
|
||||||
|
// Maximum clipboard content size to prevent memory exhaustion (100MB)
|
||||||
|
const MAX_CLIPBOARD_SIZE: usize = 100 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "wl-paste",
|
||||||
|
about = "Paste clipboard contents on Wayland.",
|
||||||
|
version,
|
||||||
|
disable_help_subcommand = true
|
||||||
|
)]
|
||||||
|
struct WlPasteArgs {
|
||||||
|
/// List the offered MIME types instead of pasting
|
||||||
|
#[arg(short = 'l', long = "list-types", action = ArgAction::SetTrue)]
|
||||||
|
list_types: bool,
|
||||||
|
|
||||||
|
/// Use the "primary" clipboard
|
||||||
|
#[arg(short = 'p', long = "primary", action = ArgAction::SetTrue)]
|
||||||
|
primary: bool,
|
||||||
|
|
||||||
|
/// Do not append a newline character
|
||||||
|
#[arg(short = 'n', long = "no-newline", action = ArgAction::SetTrue)]
|
||||||
|
no_newline: bool,
|
||||||
|
|
||||||
|
/// Pick the seat to work with
|
||||||
|
#[arg(short = 's', long = "seat")]
|
||||||
|
seat: Option<String>,
|
||||||
|
|
||||||
|
/// Request the given MIME type instead of inferring the MIME type
|
||||||
|
#[arg(short = 't', long = "type")]
|
||||||
|
mime_type: Option<String>,
|
||||||
|
|
||||||
|
/// Enable verbose logging
|
||||||
|
#[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
|
||||||
|
/// Watch for clipboard changes and run a command
|
||||||
|
#[arg(short = 'w', long = "watch")]
|
||||||
|
watch: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_paste_mime_type(mime_arg: Option<&str>) -> PasteMimeType {
|
||||||
|
match mime_arg {
|
||||||
|
None | Some("text" | "autodetect") => PasteMimeType::Text,
|
||||||
|
Some(other) => PasteMimeType::Specific(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_list_types(
|
||||||
|
clipboard: PasteClipboardType,
|
||||||
|
seat: PasteSeat,
|
||||||
|
) -> Result<()> {
|
||||||
|
match get_mime_types(clipboard, seat) {
|
||||||
|
Ok(types) => {
|
||||||
|
for mime_type in types {
|
||||||
|
println!("{mime_type}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_return)]
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
Err(PasteError::NoSeats) => {
|
||||||
|
bail!("no seats available (is a Wayland compositor running?)");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
bail!("failed to list types: {e}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_watch_mode(
|
||||||
|
args: &WlPasteArgs,
|
||||||
|
clipboard: PasteClipboardType,
|
||||||
|
seat: PasteSeat,
|
||||||
|
) -> Result<()> {
|
||||||
|
let watch_args = args.watch.as_ref().unwrap();
|
||||||
|
if watch_args.is_empty() {
|
||||||
|
bail!("--watch requires a command to run");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("starting clipboard watch mode");
|
||||||
|
|
||||||
|
// Shared state for tracking last content and shutdown signal
|
||||||
|
let last_content_hash = Arc::new(Mutex::new(None::<u64>));
|
||||||
|
let shutdown = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
// Set up signal handler for graceful shutdown
|
||||||
|
let shutdown_clone = shutdown.clone();
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
log::info!("received shutdown signal, stopping watch mode");
|
||||||
|
if let Ok(mut shutdown_guard) = shutdown_clone.lock() {
|
||||||
|
*shutdown_guard = true;
|
||||||
|
} else {
|
||||||
|
log::error!("failed to acquire shutdown lock in signal handler");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("failed to set signal handler")?;
|
||||||
|
|
||||||
|
let poll_interval = Duration::from_millis(WATCH_POLL_INTERVAL_MS);
|
||||||
|
let debounce_interval = Duration::from_millis(WATCH_DEBOUNCE_INTERVAL_MS);
|
||||||
|
let mut last_change_time = Instant::now();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Check for shutdown signal
|
||||||
|
match shutdown.lock() {
|
||||||
|
Ok(shutdown_guard) => {
|
||||||
|
if *shutdown_guard {
|
||||||
|
log::info!("shutting down watch mode");
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to acquire shutdown lock: {e}");
|
||||||
|
thread::sleep(poll_interval);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current clipboard content
|
||||||
|
let current_hash = match get_clipboard_content_hash(clipboard, seat) {
|
||||||
|
Ok(hash) => hash,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to get clipboard content hash: {e}");
|
||||||
|
thread::sleep(poll_interval);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if content has changed
|
||||||
|
match last_content_hash.lock() {
|
||||||
|
Ok(mut last_hash_guard) => {
|
||||||
|
let changed = *last_hash_guard != Some(current_hash);
|
||||||
|
if changed {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
// Debounce rapid changes
|
||||||
|
if now.duration_since(last_change_time) >= debounce_interval {
|
||||||
|
*last_hash_guard = Some(current_hash);
|
||||||
|
last_change_time = now;
|
||||||
|
drop(last_hash_guard); // Release lock before spawning command
|
||||||
|
|
||||||
|
log::info!("clipboard content changed, executing watch command");
|
||||||
|
|
||||||
|
// Execute the watch command
|
||||||
|
if let Err(e) = execute_watch_command(watch_args, clipboard, seat) {
|
||||||
|
log::error!("failed to execute watch command: {e}");
|
||||||
|
// Continue watching even if command fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to acquire last_content_hash lock: {e}");
|
||||||
|
thread::sleep(poll_interval);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
thread::sleep(poll_interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clipboard_content_hash(
|
||||||
|
clipboard: PasteClipboardType,
|
||||||
|
seat: PasteSeat,
|
||||||
|
) -> Result<u64> {
|
||||||
|
match get_contents(clipboard, seat, PasteMimeType::Text) {
|
||||||
|
Ok((mut reader, _types)) => {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
let mut temp_buffer = [0; 8192];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = reader
|
||||||
|
.read(&mut temp_buffer)
|
||||||
|
.context("failed to read clipboard content")?;
|
||||||
|
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.len() + bytes_read > MAX_CLIPBOARD_SIZE {
|
||||||
|
bail!(
|
||||||
|
"clipboard content exceeds maximum size of {} bytes",
|
||||||
|
MAX_CLIPBOARD_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
content.hash(&mut hasher);
|
||||||
|
Ok(hasher.finish())
|
||||||
|
},
|
||||||
|
Err(PasteError::ClipboardEmpty) => {
|
||||||
|
Ok(0) // Empty clipboard has hash 0
|
||||||
|
},
|
||||||
|
Err(e) => bail!("clipboard error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate command name to prevent command injection
|
||||||
|
fn validate_command_name(cmd: &str) -> Result<()> {
|
||||||
|
if cmd.is_empty() {
|
||||||
|
bail!("command name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject commands with shell metacharacters or path traversal
|
||||||
|
if cmd.contains(|c| {
|
||||||
|
['|', '&', ';', '$', '`', '(', ')', '<', '>', '"', '\'', '\\'].contains(&c)
|
||||||
|
}) {
|
||||||
|
bail!("command contains invalid characters: {cmd}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject absolute paths and relative path traversal
|
||||||
|
if cmd.starts_with('/') || cmd.contains("..") {
|
||||||
|
bail!("command paths are not allowed: {cmd}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set environment variable safely with validation
|
||||||
|
fn set_clipboard_state_env(has_content: bool) -> Result<()> {
|
||||||
|
let value = if has_content { "data" } else { "nil" };
|
||||||
|
|
||||||
|
// Validate the environment variable value
|
||||||
|
if !matches!(value, "data" | "nil") {
|
||||||
|
bail!("invalid clipboard state value: {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe to set environment variable with validated, known-safe value
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("STASH_CLIPBOARD_STATE", value);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_watch_command(
|
||||||
|
watch_args: &[String],
|
||||||
|
clipboard: PasteClipboardType,
|
||||||
|
seat: PasteSeat,
|
||||||
|
) -> Result<()> {
|
||||||
|
if watch_args.is_empty() {
|
||||||
|
bail!("watch command cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate command name for security
|
||||||
|
validate_command_name(&watch_args[0])?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&watch_args[0]);
|
||||||
|
if watch_args.len() > 1 {
|
||||||
|
cmd.args(&watch_args[1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get clipboard content and pipe it to the command
|
||||||
|
match get_contents(clipboard, seat, PasteMimeType::Text) {
|
||||||
|
Ok((mut reader, _types)) => {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
let mut temp_buffer = [0; 8192];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = reader
|
||||||
|
.read(&mut temp_buffer)
|
||||||
|
.context("failed to read clipboard")?;
|
||||||
|
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.len() + bytes_read > MAX_CLIPBOARD_SIZE {
|
||||||
|
bail!(
|
||||||
|
"clipboard content exceeds maximum size of {} bytes",
|
||||||
|
MAX_CLIPBOARD_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable safely
|
||||||
|
set_clipboard_state_env(!content.is_empty())?;
|
||||||
|
|
||||||
|
// Spawn the command with the content as stdin
|
||||||
|
cmd.stdin(Stdio::piped());
|
||||||
|
|
||||||
|
let mut child = cmd.spawn()?;
|
||||||
|
|
||||||
|
if let Some(stdin) = child.stdin.take() {
|
||||||
|
let mut stdin = stdin;
|
||||||
|
if let Err(e) = stdin.write_all(&content) {
|
||||||
|
bail!("failed to write to command stdin: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match child.wait() {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
log::warn!("watch command exited with status: {status}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
bail!("failed to wait for command: {e}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(PasteError::ClipboardEmpty) => {
|
||||||
|
// Set environment variable safely
|
||||||
|
set_clipboard_state_env(false)?;
|
||||||
|
|
||||||
|
// Run command with /dev/null as stdin
|
||||||
|
cmd.stdin(Stdio::null());
|
||||||
|
|
||||||
|
match cmd.status() {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
log::warn!("watch command exited with status: {status}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
bail!("failed to run command: {e}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
bail!("clipboard error: {e}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_regular_paste(
|
||||||
|
args: &WlPasteArgs,
|
||||||
|
clipboard: PasteClipboardType,
|
||||||
|
seat: PasteSeat,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mime_type = get_paste_mime_type(args.mime_type.as_deref());
|
||||||
|
|
||||||
|
match get_contents(clipboard, seat, mime_type) {
|
||||||
|
Ok((mut reader, _types)) => {
|
||||||
|
let mut out = io::stdout();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut temp_buffer = [0; 8192];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = reader
|
||||||
|
.read(&mut temp_buffer)
|
||||||
|
.context("failed to read clipboard")?;
|
||||||
|
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.len() + bytes_read > MAX_CLIPBOARD_SIZE {
|
||||||
|
bail!(
|
||||||
|
"clipboard content exceeds maximum size of {} bytes",
|
||||||
|
MAX_CLIPBOARD_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.is_empty() && args.no_newline {
|
||||||
|
bail!("no content available and --no-newline specified");
|
||||||
|
}
|
||||||
|
if let Err(e) = out.write_all(&buf) {
|
||||||
|
bail!("failed to write to stdout: {e}");
|
||||||
|
}
|
||||||
|
if !args.no_newline && !buf.ends_with(b"\n") {
|
||||||
|
if let Err(e) = out.write_all(b"\n") {
|
||||||
|
bail!("failed to write newline to stdout: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(PasteError::NoSeats) => {
|
||||||
|
bail!("no seats available (is a Wayland compositor running?)");
|
||||||
|
},
|
||||||
|
Err(PasteError::ClipboardEmpty) => {
|
||||||
|
if args.no_newline {
|
||||||
|
bail!("clipboard empty and --no-newline specified");
|
||||||
|
}
|
||||||
|
// Otherwise, exit successfully with no output
|
||||||
|
},
|
||||||
|
Err(PasteError::NoMimeType) => {
|
||||||
|
bail!("clipboard does not contain requested MIME type");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
bail!("clipboard error: {e}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wl_paste_main() -> Result<()> {
|
||||||
|
let args = WlPasteArgs::parse();
|
||||||
|
|
||||||
|
let clipboard = if args.primary {
|
||||||
|
PasteClipboardType::Primary
|
||||||
|
} else {
|
||||||
|
PasteClipboardType::Regular
|
||||||
|
};
|
||||||
|
let seat = args
|
||||||
|
.seat
|
||||||
|
.as_deref()
|
||||||
|
.map_or(PasteSeat::Unspecified, PasteSeat::Specific);
|
||||||
|
|
||||||
|
// Handle list-types option
|
||||||
|
if args.list_types {
|
||||||
|
handle_list_types(clipboard, seat)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle watch mode
|
||||||
|
if args.watch.is_some() {
|
||||||
|
handle_watch_mode(&args, clipboard, seat)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular paste mode
|
||||||
|
handle_regular_paste(&args, clipboard, seat)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue