diff --git a/Cargo.lock b/Cargo.lock index 9c764f7..9f70688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -53,7 +53,7 @@ dependencies = [ "objc_id", "once_cell", "parking_lot 0.12.0", - "thiserror", + "thiserror 1.0.30", "winapi", "x11rb", ] @@ -96,6 +96,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + [[package]] name = "block" version = "0.1.6" @@ -159,7 +165,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim 0.8.0", "term_size", "textwrap", @@ -173,7 +179,7 @@ version = "4.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_derive", "clap_lex", "is-terminal", @@ -193,7 +199,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.90", ] [[package]] @@ -233,10 +239,14 @@ dependencies = [ "gag", "itertools", "nix 0.23.1", + "os_pipe 1.2.3", + "rustix 1.1.4", "terminal-clipboard", - "thiserror", - "wayland-client 0.29.4", - "wl-clipboard-rs 0.7.0", + "thiserror 1.0.30", + "wayland-client 0.31.14", + "wayland-protocols 0.32.13", + "wayland-protocols-wlr", + "wl-clipboard-rs 0.9.3", "x11-clipboard", ] @@ -257,7 +267,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -294,7 +304,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -307,7 +317,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "foreign-types", "libc", @@ -340,7 +350,7 @@ checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.90", ] [[package]] @@ -353,7 +363,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.90", ] [[package]] @@ -379,6 +389,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -426,7 +446,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.90", "synstructure", ] @@ -446,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.30", "winapi", ] @@ -592,7 +612,7 @@ checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" dependencies = [ "hermit-abi", "io-lifetimes", - "rustix", + "rustix 0.36.5", "windows-sys 0.42.0", ] @@ -625,9 +645,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.138" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" @@ -635,6 +655,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.3.4" @@ -743,7 +769,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if 0.1.10", "libc", @@ -756,7 +782,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if 0.1.10", "libc", @@ -768,7 +794,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if 1.0.0", "libc", @@ -781,25 +807,13 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if 1.0.0", "libc", "memoffset", ] -[[package]] -name = "nix" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" -dependencies = [ - "bitflags", - "cfg-if 1.0.0", - "libc", - "memoffset", -] - [[package]] name = "nom" version = "3.2.1" @@ -922,12 +936,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.0.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c92f2b54f081d635c77e7120862d48db8e91f7f21cef23ab1b4fe9971c59f55" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -1015,7 +1029,7 @@ version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "deflate", "miniz_oxide 0.3.7", @@ -1030,7 +1044,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.90", "version_check", ] @@ -1047,9 +1061,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1063,11 +1077,20 @@ dependencies = [ "memchr 2.4.1", ] +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr 2.4.1", +] + [[package]] name = "quote" -version = "1.0.17" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1084,7 +1107,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1126,14 +1149,27 @@ version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" dependencies = [ - "bitflags", - "errno", + "bitflags 1.3.2", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.3", "windows-sys 0.42.0", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1148,9 +1184,9 @@ checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "stderrlog" @@ -1204,7 +1240,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.90", ] [[package]] @@ -1218,6 +1254,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -1226,7 +1273,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.90", "unicode-xid", ] @@ -1281,7 +1328,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907" dependencies = [ - "rustix", + "rustix 0.36.5", "windows-sys 0.42.0", ] @@ -1319,7 +1366,16 @@ version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.30", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -1330,7 +1386,18 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -1458,34 +1525,44 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "smallvec", + "wayland-sys 0.31.11", +] + [[package]] name = "wayland-client" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab702fefbcd6d6f67fb5816e3a89a3b5a42a94290abbc015311c9a30d1068ae4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "downcast-rs", "libc", "nix 0.17.0", - "wayland-commons 0.27.0", + "wayland-commons", "wayland-scanner 0.27.0", "wayland-sys 0.27.0", ] [[package]] name = "wayland-client" -version = "0.29.4" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags", - "downcast-rs", - "libc", - "nix 0.22.3", - "wayland-commons 0.29.4", - "wayland-scanner 0.29.4", - "wayland-sys 0.29.4", + "bitflags 2.13.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner 0.31.10", ] [[package]] @@ -1500,40 +1577,41 @@ dependencies = [ "wayland-sys 0.27.0", ] -[[package]] -name = "wayland-commons" -version = "0.29.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" -dependencies = [ - "nix 0.22.3", - "once_cell", - "smallvec", - "wayland-sys 0.29.4", -] - [[package]] name = "wayland-protocols" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d6fc54b17b98b5083bc21ae3a30e6d75cb4b01647360e4c3a04648bcf8781d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "wayland-client 0.27.0", - "wayland-commons 0.27.0", + "wayland-commons", "wayland-scanner 0.27.0", ] [[package]] name = "wayland-protocols" -version = "0.29.4" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags", - "wayland-client 0.29.4", - "wayland-commons 0.29.4", - "wayland-scanner 0.29.4", + "bitflags 2.13.0", + "wayland-backend", + "wayland-client 0.31.14", + "wayland-scanner 0.31.10", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client 0.31.14", + "wayland-protocols 0.32.13", + "wayland-scanner 0.31.10", ] [[package]] @@ -1549,13 +1627,13 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.29.4" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", + "quick-xml 0.39.4", "quote", - "xml-rs", ] [[package]] @@ -1569,9 +1647,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.29.4" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "pkg-config", ] @@ -1622,6 +1700,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.34.0" @@ -1650,6 +1734,15 @@ dependencies = [ "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" @@ -1747,20 +1840,20 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" -version = "0.7.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981a303dfbb75d659f6612d05a14b2e363c103d24f676a2d44a00d18507a1ad9" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" dependencies = [ - "derive-new", "libc", "log", - "nix 0.24.2", - "os_pipe 1.0.1", - "tempfile", - "thiserror", + "os_pipe 1.2.3", + "rustix 1.1.4", + "thiserror 2.0.18", "tree_magic_mini", - "wayland-client 0.29.4", - "wayland-protocols 0.29.4", + "wayland-backend", + "wayland-client 0.31.14", + "wayland-protocols 0.32.13", + "wayland-protocols-wlr", ] [[package]] @@ -1792,7 +1885,7 @@ checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" dependencies = [ "libc", "log", - "quick-xml", + "quick-xml 0.22.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1da784e..d06812c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,8 @@ backtrace = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -wayland-client = "0.29.4" -wl-clipboard-rs = "0.7.0" +wayland-client = "0.31.14" +wl-clipboard-rs = "0.9.3" cli-clipboard = "0.2.0" arboard = "2.0.0" x11-clipboard = "0.5.3" @@ -24,3 +24,7 @@ chrono = "0.4" itertools = "0.10" gag = "1.0" clap = { version = "4.0.29", features = ["derive", "wrap_help"] } +wayland-protocols = { version = "0.32", features = ["client", "staging"] } +wayland-protocols-wlr = { version = "0.3", features = ["client"] } +os_pipe = "1.2" +rustix = { version = "1", features = ["fs"] } diff --git a/src/clipboard.rs b/src/clipboard.rs index 194dd77..f8bb9bc 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,13 +1,12 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::{env, io::Read, process::Command}; -use terminal_clipboard::Clipboard as TerminalClipboard; -use wl_clipboard_rs::copy::{MimeType as CopyMimeType, Options, Source}; -use wl_clipboard_rs::paste::{ - get_contents, ClipboardType, Error as PasteError, MimeType as PasteMimeType, Seat, -}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{env, process::Command, thread}; -use crate::error::{Generify, MyResult, Standardize}; +use chrono::Local; + +use crate::error::MyResult; +use crate::log; +use crate::wlr_backend::WlrBackend; pub trait Clipboard: std::fmt::Debug { fn display(&self) -> String; @@ -47,9 +46,27 @@ impl Clipboard for Box { } } -#[derive(Debug)] pub struct WlrClipboard { pub display: String, + backend: WlrBackend, +} + +impl std::fmt::Debug for WlrClipboard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WlrClipboard") + .field("display", &self.display) + .finish() + } +} + +impl WlrClipboard { + pub fn new(display: String) -> MyResult { + env::set_var("WAYLAND_DISPLAY", &display); + Ok(Self { + backend: WlrBackend::new(&display)?, + display, + }) + } } impl Clipboard for WlrClipboard { @@ -58,39 +75,11 @@ impl Clipboard for WlrClipboard { } fn get(&self) -> MyResult { - env::set_var("WAYLAND_DISPLAY", self.display.clone()); - let result = get_contents( - ClipboardType::Regular, - Seat::Unspecified, - PasteMimeType::Text, - ); - - match result { - Ok((mut pipe, _)) => { - let mut contents = vec![]; - pipe.read_to_end(&mut contents)?; - Ok(String::from_utf8_lossy(&contents).to_string()) - } - - Err(PasteError::NoSeats) - | Err(PasteError::ClipboardEmpty) - | Err(PasteError::NoMimeType) => Ok("".to_string()), - - Err(err) => Err(err)?, - } + self.backend.get_text_or_empty() } fn set(&self, value: &str) -> MyResult<()> { - env::set_var("WAYLAND_DISPLAY", self.display.clone()); - let opts = Options::new(); - let result = std::panic::catch_unwind(|| { - opts.copy( - Source::Bytes(value.to_string().into_bytes().into()), - CopyMimeType::Text, - ) - }); - - Ok(result.standardize().generify()??) + self.backend.set_text_result(value) } fn rank(&self) -> u8 { @@ -160,21 +149,8 @@ impl Clipboard for ArClipboard { pub struct X11Clipboard { display: String, - backend: X11Backend, -} - -#[derive(Clone)] -pub struct X11Backend(Rc>); -impl X11Backend { - /// try to only call this once because repeated initializations may not work. - /// i started seeing timeouts/errors after 4 - pub fn new(display: &str) -> MyResult { - // let backend = stdio! { terminal_clipboard::X11Clipboard::new() }.standardize()?; - env::set_var("DISPLAY", display); - let backend = terminal_clipboard::X11Clipboard::new().standardize()?; - - Ok(Self(Rc::new(RefCell::new(backend)))) - } + setter: x11_clipboard::Clipboard, + cache: Arc>, } impl std::fmt::Debug for X11Clipboard { @@ -185,17 +161,59 @@ impl std::fmt::Debug for X11Clipboard { } } +/// Bound on a single X11 read, used both for the startup seed and by the +/// watcher thread. Slow INCR transfers must not block reads indefinitely. +const X11_READ_TIMEOUT: Duration = Duration::from_secs(30); + impl X11Clipboard { pub fn new(display: String) -> MyResult { + env::set_var("DISPLAY", &display); + let setter = x11_clipboard::Clipboard::new()?; + let watcher = x11_clipboard::Clipboard::new()?; + + let atoms = &watcher.getter.atoms; + let initial = watcher + .load( + atoms.clipboard, + atoms.utf8_string, + atoms.property, + X11_READ_TIMEOUT, + ) + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) + .unwrap_or_default(); + + let cache = Arc::new(Mutex::new(initial)); + thread::spawn({ + let cache = cache.clone(); + move || watch_x11(watcher, cache) + }); + Ok(Self { - backend: X11Backend::new(&display)?, display, + setter, + cache, }) } +} - // fn backend(&self) -> X11Backend { - // X11Backend::new_str(&self.display).unwrap() - // } +/// Polls the X11 clipboard in a dedicated thread so the main loop never blocks +/// on a transfer. Selection owners that proxy clipboard data from another +/// machine serve INCR transfers slowly; done synchronously in the poll loop, +/// that starves Wayland event dispatch and pastes from Wayland apps time out. +/// The XFixes-driven load_wait is not used: it has no read timeout, and rapid +/// ownership changes (our own set racing another client re-taking the +/// selection) make its shared-property conversions stomp each other — a stuck +/// transfer would freeze this watcher silently. Bounded reads retry, so no +/// change is ever lost. +fn watch_x11(cb: x11_clipboard::Clipboard, cache: Arc>) { + let atoms = &cb.getter.atoms; + loop { + match cb.load(atoms.clipboard, atoms.utf8_string, atoms.property, X11_READ_TIMEOUT) { + Ok(bytes) => *cache.lock().unwrap() = String::from_utf8_lossy(&bytes).into_owned(), + Err(e) => log::debug!("X11 clipboard watch error: {e}"), + } + thread::sleep(Duration::from_millis(500)); + } } impl Clipboard for X11Clipboard { @@ -204,21 +222,12 @@ impl Clipboard for X11Clipboard { } fn get(&self) -> MyResult { - Ok(self - .backend - .0 - .try_borrow()? - .get_string() - .unwrap_or_default()) + Ok(self.cache.lock().unwrap().clone()) } fn set(&self, value: &str) -> MyResult<()> { - self.backend - .0 - .try_borrow_mut()? - .set_string(value) - .standardize()?; - + let atoms = &self.setter.setter.atoms; + self.setter.store(atoms.clipboard, atoms.utf8_string, value)?; Ok(()) } } diff --git a/src/error.rs b/src/error.rs index 950c064..f42fdfa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,11 +16,8 @@ pub enum MyError { #[error("Application crash '{msg}': {cause:#?}")] Crash { msg: String, cause: Vec }, - #[error("failed to get wlr clipboard: {0}")] - WlcrsPaste(#[from] wl_clipboard_rs::paste::Error), - - #[error("failed to set wlr clipboard: {0}")] - WlcrsCopy(#[from] wl_clipboard_rs::copy::Error), + #[error("failed to access wlr clipboard: {0}")] + WlrBackend(#[from] crate::wlr_backend::Error), #[error("{0}")] Io(#[from] std::io::Error), @@ -28,6 +25,9 @@ pub enum MyError { #[error("{0}")] TerminalClipboard(#[from] StandardizedError), + #[error("X11 clipboard error: {0}")] + X11Clipboard(#[from] x11_clipboard::error::Error), + #[error("{0}")] Arboard(#[from] arboard::Error), diff --git a/src/main.rs b/src/main.rs index a2a2f90..8bfb3bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod error; mod log; mod mustatex; mod sync; +mod wlr_backend; fn main() { let args = Args::parse(); diff --git a/src/sync.rs b/src/sync.rs index 270de67..5ce4e9c 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,11 +1,13 @@ use chrono::Local; use std::collections::HashSet; -use std::{thread::sleep, time::Duration}; +use std::thread::sleep; +use std::time::{Duration, Instant}; use wayland_client::ConnectError; -use wl_clipboard_rs::paste::Error as PasteError; + +use crate::wlr_backend::Error as WlrBackendError; use crate::clipboard::*; -use crate::error::{MyError, MyResult, StandardizedError}; +use crate::error::{MyError, MyResult}; use crate::log::{self, concise_numbers}; pub fn get_clipboards() -> MyResult>> { @@ -62,11 +64,57 @@ pub fn keep_synced(clipboards: &Vec>) -> MyResult<()> { if clipboards.is_empty() { return Err(MyError::NoClipboards); } + + let mut last: Vec = clipboards + .iter() + .map(|c| c.get().unwrap_or_default()) + .collect(); + let mut settle_until: Option = None; + loop { - sleep(Duration::from_millis(100)); - let new_value = await_change(clipboards)?; - for c in clipboards { - c.set(&new_value)?; + sleep(Duration::from_millis(200)); + + if let Some(deadline) = settle_until { + if Instant::now() < deadline { + continue; + } + for (j, c) in clipboards.iter().enumerate() { + if c.should_poll() { + last[j] = c.get().unwrap_or_default(); + } + } + settle_until = None; + } + + for (i, c) in clipboards.iter().enumerate() { + if !c.should_poll() { + continue; + } + let current = c.get()?; + // Read glitches (wayland NoSeats/NoMimeType when the selection holds + // non-text data, X11 errors via unwrap_or_default) surface as "". + // Propagating one would wipe every synced clipboard. + if current.is_empty() || current == last[i] { + continue; + } + + log::info!("clipboard updated from display {}", c.display()); + log::sensitive!(log::info, "clipboard contents: '{}'", current); + + for (j, other) in clipboards.iter().enumerate() { + if i == j { + continue; + } + if last[j] != current { + other.set(¤t)?; + } + last[j] = current.clone(); + } + last[i] = current; + + // One poll interval for Wayland/X11 writes to settle before re-baselining. + settle_until = Some(Instant::now() + Duration::from_millis(200)); + break; } } } @@ -170,12 +218,9 @@ fn get_clipboards_spec MyResult>>>( clipboards.push(clipboard); } } - Err(MyError::TerminalClipboard(StandardizedError { - inner, - stdio: None, - })) if format!("{inner}") == "clipboard error: X11 clipboard error : XCB connection error: Connection" => { + Err(MyError::X11Clipboard(x11_clipboard::error::Error::XcbConn(err))) => { xcb_conn_failed_clipboards.push(i); - xcb_conn_err = Some(inner); + xcb_conn_err = Some(err); }, Err(err) => log::error!( "unexpected error while attempting to setup clipboard {}: {}", @@ -198,41 +243,37 @@ Details: '{err}' for x11 displays: {displays}", fn get_wayland(n: u8) -> MyResult>> { let wl_display = format!("wayland-{}", n); - let clipboard = WlrClipboard { - display: wl_display.clone(), - }; - let attempt = clipboard.get(); - if let Err(MyError::WlcrsPaste(PasteError::WaylandConnection( - ConnectError::NoCompositorListening, - ))) = attempt - { - return Ok(None); - } - if let Err(MyError::WlcrsPaste(PasteError::MissingProtocol { - name: "zwlr_data_control_manager_v1", - version: 1, - })) = attempt - { - log::warning!( - "{wl_display} does not support zwlr_data_control_manager_v1. If you are running \ -gnome in wayland, that's OK because it provides an x11 clipboard, which will be used instead. \ -Otherwise, `wl-copy` will be used to sync data *into* this clipboard, but it will not be possible \ -to read data *from* this clipboard into other clipboards." - ); - let command = WlCommandClipboard { - display: wl_display.clone(), - }; - let Ok(gotten) = command.get() else { - return Ok(None); - }; - let Ok(_) = command.set(&gotten) else { - return Ok(None); - }; - return Ok(Some(Box::new(command))); + match WlrClipboard::new(wl_display.clone()) { + Ok(clipboard) => Ok(Some(Box::new(clipboard))), + Err(MyError::WlrBackend(WlrBackendError::WaylandConnection( + ConnectError::NoCompositor, + ))) + | Err(MyError::WlrBackend(WlrBackendError::SocketOpenError(_))) => Ok(None), + Err(MyError::WlrBackend(WlrBackendError::MissingProtocol { .. })) => { + log::warning!( + "{wl_display} does not support ext-data-control or wlr-data-control. If you are \ +running gnome in wayland, that's OK because it provides an x11 clipboard, which will be used \ +instead. Otherwise, `wl-copy` will be used to sync data *into* this clipboard, but it will not \ +be possible to read data *from* this clipboard into other clipboards." + ); + let command = WlCommandClipboard { + display: wl_display.clone(), + }; + let Ok(gotten) = command.get() else { + return Ok(None); + }; + let Ok(_) = command.set(&gotten) else { + return Ok(None); + }; + Ok(Some(Box::new(command))) + } + Err(err) => { + log::error!( + "unexpected error while attempting to setup wayland clipboard {wl_display}: {err}" + ); + Ok(None) + } } - attempt?; - - Ok(Some(Box::new(clipboard))) } fn get_x11(n: u8) -> MyResult>> { @@ -243,20 +284,3 @@ fn get_x11(n: u8) -> MyResult>> { Ok(Some(Box::new(clipboard))) } -fn await_change(clipboards: &Vec>) -> MyResult { - let start = clipboards[0].get()?; - loop { - for c in clipboards { - if !c.should_poll() { - continue; - } - let new = c.get()?; - if new != start { - log::info!("clipboard updated from display {}", c.display()); - log::sensitive!(log::info, "clipboard contents: '{}'", new); - return Ok(new); - } - } - sleep(Duration::from_millis(200)); - } -} diff --git a/src/wlr_backend/data_control.rs b/src/wlr_backend/data_control.rs new file mode 100644 index 0000000..005b087 --- /dev/null +++ b/src/wlr_backend/data_control.rs @@ -0,0 +1,276 @@ +//! Adapted from wl-clipboard-rs (MIT/Apache-2.0). + +use std::os::fd::BorrowedFd; + +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::{Dispatch, QueueHandle}; +use wayland_protocols::ext::data_control::v1::client::{ + ext_data_control_device_v1::ExtDataControlDeviceV1, + ext_data_control_manager_v1::ExtDataControlManagerV1, + ext_data_control_offer_v1::ExtDataControlOfferV1, + ext_data_control_source_v1::ExtDataControlSourceV1, +}; +use wayland_protocols_wlr::data_control::v1::client as zwlr; +use zwlr::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; +use zwlr::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; +use zwlr::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1; +use zwlr::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; + +#[derive(Clone)] +pub enum Manager { + Zwlr(ZwlrDataControlManagerV1), + Ext(ExtDataControlManagerV1), +} + +#[derive(Clone)] +pub enum Device { + Zwlr(ZwlrDataControlDeviceV1), + Ext(ExtDataControlDeviceV1), +} + +#[derive(Clone)] +pub enum Source { + Zwlr(ZwlrDataControlSourceV1), + Ext(ExtDataControlSourceV1), +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum Offer { + Zwlr(ZwlrDataControlOfferV1), + Ext(ExtDataControlOfferV1), +} + +impl Manager { + pub fn get_data_device(&self, seat: &WlSeat, qh: &QueueHandle, udata: U) -> Device + where + D: Dispatch + 'static, + D: Dispatch + 'static, + U: Send + Sync + 'static, + { + match self { + Manager::Zwlr(manager) => Device::Zwlr(manager.get_data_device(seat, qh, udata)), + Manager::Ext(manager) => Device::Ext(manager.get_data_device(seat, qh, udata)), + } + } + + pub fn create_data_source(&self, qh: &QueueHandle) -> Source + where + D: Dispatch + 'static, + D: Dispatch + 'static, + { + match self { + Manager::Zwlr(manager) => Source::Zwlr(manager.create_data_source(qh, ())), + Manager::Ext(manager) => Source::Ext(manager.create_data_source(qh, ())), + } + } +} + +impl Device { + pub fn destroy(&self) { + match self { + Device::Zwlr(device) => device.destroy(), + Device::Ext(device) => device.destroy(), + } + } + + pub fn set_selection(&self, source: Option<&Source>) { + match self { + Device::Zwlr(device) => device.set_selection(source.map(Source::zwlr)), + Device::Ext(device) => device.set_selection(source.map(Source::ext)), + } + } +} + +impl Source { + pub fn destroy(&self) { + match self { + Source::Zwlr(source) => source.destroy(), + Source::Ext(source) => source.destroy(), + } + } + + pub fn offer(&self, mime_type: String) { + match self { + Source::Zwlr(source) => source.offer(mime_type), + Source::Ext(source) => source.offer(mime_type), + } + } + + fn zwlr(&self) -> &ZwlrDataControlSourceV1 { + match self { + Source::Zwlr(source) => source, + Source::Ext(_) => panic!("non-Zwlr source"), + } + } + + fn ext(&self) -> &ExtDataControlSourceV1 { + match self { + Source::Ext(source) => source, + Source::Zwlr(_) => panic!("non-Ext source"), + } + } +} + +impl Offer { + pub fn destroy(&self) { + match self { + Offer::Zwlr(offer) => offer.destroy(), + Offer::Ext(offer) => offer.destroy(), + } + } + + pub fn receive(&self, mime_type: String, fd: BorrowedFd<'_>) { + match self { + Offer::Zwlr(offer) => offer.receive(mime_type, fd), + Offer::Ext(offer) => offer.receive(mime_type, fd), + } + } +} + +impl From for Source { + fn from(v: ZwlrDataControlSourceV1) -> Self { + Self::Zwlr(v) + } +} + +impl From for Source { + fn from(v: ExtDataControlSourceV1) -> Self { + Self::Ext(v) + } +} + +impl From for Offer { + fn from(v: ZwlrDataControlOfferV1) -> Self { + Self::Zwlr(v) + } +} + +impl From for Offer { + fn from(v: ExtDataControlOfferV1) -> Self { + Self::Ext(v) + } +} + +macro_rules! impl_dispatch_manager { + ($handler:ty => [$($iface:ty),*]) => { + $( + impl Dispatch<$iface, ()> for $handler { + fn event( + _state: &mut Self, + _proxy: &$iface, + _event: <$iface as wayland_client::Proxy>::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + } + } + )* + }; + + ($handler:ty) => { + impl_dispatch_manager!($handler => [ + ZwlrDataControlManagerV1, + ExtDataControlManagerV1 + ]); + }; +} +pub(crate) use impl_dispatch_manager; + +macro_rules! impl_dispatch_device { + ($handler:ty, $udata:ty, $code:expr => [$(($iface:ty, $opcode:path, $offer:ty)),*]) => { + $( + impl Dispatch<$iface, $udata> for $handler { + fn event( + state: &mut Self, + _proxy: &$iface, + event: <$iface as wayland_client::Proxy>::Event, + data: &$udata, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + type Event = <$iface as wayland_client::Proxy>::Event; + ($code)(state, event, data) + } + + event_created_child!($handler, $iface, [ + $opcode => ($offer, ()), + ]); + } + )* + }; + + ($handler:ty, $udata:ty, $code:expr) => { + impl_dispatch_device!($handler, $udata, $code => [ + ( + ZwlrDataControlDeviceV1, + wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE, + ZwlrDataControlOfferV1 + ), + ( + ExtDataControlDeviceV1, + wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::EVT_DATA_OFFER_OPCODE, + ExtDataControlOfferV1 + ) + ]); + }; +} +pub(crate) use impl_dispatch_device; + +macro_rules! impl_dispatch_source { + ($handler:ty, $code:expr => [$($iface:ty),*]) => { + $( + impl Dispatch<$iface, ()> for $handler { + fn event( + state: &mut Self, + proxy: &$iface, + event: <$iface as wayland_client::Proxy>::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + type Event = <$iface as wayland_client::Proxy>::Event; + let source = crate::wlr_backend::data_control::Source::from(proxy.clone()); + ($code)(state, source, event) + } + } + )* + }; + + ($handler:ty, $code:expr) => { + impl_dispatch_source!($handler, $code => [ + ZwlrDataControlSourceV1, + ExtDataControlSourceV1 + ]); + }; +} +pub(crate) use impl_dispatch_source; + +macro_rules! impl_dispatch_offer { + ($handler:ty, $code:expr => [$($iface:ty),*]) => { + $( + impl Dispatch<$iface, ()> for $handler { + fn event( + state: &mut Self, + proxy: &$iface, + event: <$iface as wayland_client::Proxy>::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + type Event = <$iface as wayland_client::Proxy>::Event; + let offer = crate::wlr_backend::data_control::Offer::from(proxy.clone()); + ($code)(state, offer, event) + } + } + )* + }; + + ($handler:ty, $code:expr) => { + impl_dispatch_offer!($handler, $code => [ + ZwlrDataControlOfferV1, + ExtDataControlOfferV1 + ]); + }; +} +pub(crate) use impl_dispatch_offer; diff --git a/src/wlr_backend/mod.rs b/src/wlr_backend/mod.rs new file mode 100644 index 0000000..586bcbf --- /dev/null +++ b/src/wlr_backend/mod.rs @@ -0,0 +1,388 @@ +mod data_control; +mod seat_data; + +use std::cell::RefCell; +use std::collections::HashMap; +use std::ffi::OsString; +use std::fs::File; +use std::io::{self, Cursor, Read}; +use std::os::fd::AsFd; +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::thread; +use std::{env, mem}; + +use os_pipe::pipe; +use rustix::fs::{fcntl_setfl, OFlags}; +use wayland_client::globals::{registry_queue_init, GlobalError, GlobalListContents}; +use wayland_client::protocol::wl_registry::WlRegistry; +use wayland_client::protocol::wl_seat::{self, WlSeat}; +use wayland_client::{ + delegate_dispatch, event_created_child, ConnectError, Connection, Dispatch, DispatchError, + EventQueue, Proxy, +}; +use wayland_protocols::ext::data_control::v1::client::{ + ext_data_control_device_v1::ExtDataControlDeviceV1, + ext_data_control_manager_v1::ExtDataControlManagerV1, + ext_data_control_offer_v1::ExtDataControlOfferV1, + ext_data_control_source_v1::ExtDataControlSourceV1, +}; +use wayland_protocols_wlr::data_control::v1::client::{ + zwlr_data_control_device_v1::ZwlrDataControlDeviceV1, + zwlr_data_control_manager_v1::ZwlrDataControlManagerV1, + zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, + zwlr_data_control_source_v1::ZwlrDataControlSourceV1, +}; +use wl_clipboard_rs::utils::is_text; + +use crate::error::MyResult; +use data_control::{ + impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, impl_dispatch_source, + Manager, Offer, Source, +}; +use seat_data::SeatData; + +const TEXT_MIMES: &[&str] = &[ + "text/plain;charset=utf-8", + "UTF8_STRING", + "text/plain", + "STRING", + "TEXT", +]; + +pub struct ConnectionState { + pub seats: HashMap, + pub clipboard_manager: Manager, +} + +struct SessionState { + common: ConnectionState, + offers: HashMap>, + data_sources: HashMap>, + owned_sources: Vec, +} + +struct WlrSession { + queue: EventQueue, + state: SessionState, +} + +#[derive(Clone)] +pub struct WlrBackend(Rc>); + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("There are no seats")] + NoSeats, + + #[error("The clipboard is empty")] + ClipboardEmpty, + + #[error("No suitable type of content copied")] + NoMimeType, + + #[error("Couldn't open the provided Wayland socket")] + SocketOpenError(#[source] io::Error), + + #[error("Couldn't connect to the Wayland compositor")] + WaylandConnection(#[source] ConnectError), + + #[error("Wayland compositor communication error")] + WaylandCommunication(#[source] DispatchError), + + #[error( + "A required Wayland protocol ({name} version {version}) is not supported by the compositor" + )] + MissingProtocol { name: &'static str, version: u32 }, + + #[error("Couldn't create a pipe for content transfer")] + PipeCreation(#[source] io::Error), + + #[error("Clipboard I/O error")] + Io(#[source] io::Error), +} + +impl AsMut for SessionState { + fn as_mut(&mut self) -> &mut ConnectionState { + &mut self.common + } +} + +delegate_dispatch!(SessionState: [WlSeat: ()] => ConnectionState); + +impl Dispatch for SessionState { + fn event( + _state: &mut Self, + _proxy: &WlRegistry, + _event: ::Event, + _data: &GlobalListContents, + _conn: &wayland_client::Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + } +} + +impl_dispatch_manager!(SessionState); + +impl_dispatch_device!(SessionState, WlSeat, |state: &mut SessionState, event, seat| { + match event { + Event::DataOffer { id } => { + state.offers.entry(Offer::from(id)).or_default(); + } + Event::Selection { id } => { + let offer = id.map(Offer::from); + state.common.seats.get_mut(seat).unwrap().set_offer(offer); + } + Event::Finished => { + state.common.seats.get_mut(seat).unwrap().set_device(None); + } + _ => (), + } +}); + +impl_dispatch_offer!(SessionState, |state: &mut SessionState, offer: Offer, event| { + if let Event::Offer { mime_type } = event { + state.offers.entry(offer).or_default().push(mime_type); + } +}); + +impl_dispatch_source!(SessionState, |state: &mut SessionState, + source: Source, + event| { + match event { + Event::Send { mime_type, fd } => { + if let Some(data) = state.data_sources.get(&mime_type) { + // Serve from a side thread: a blocking write larger than the + // pipe capacity would deadlock the dispatch loop when the + // reader only starts draining after this handler returns — + // which is exactly what our own get_text() roundtrip does. + let data = data.clone(); + thread::spawn(move || { + let _ = (|| -> io::Result<()> { + fcntl_setfl(&fd, OFlags::empty())?; + let mut target = File::from(fd); + io::copy(&mut Cursor::new(&*data), &mut target).map(drop) + })(); + }); + } + } + Event::Cancelled => source.destroy(), + _ => (), + } +}); + +impl Dispatch for ConnectionState +where + S: Dispatch + AsMut, +{ + fn event( + parent: &mut S, + seat: &WlSeat, + event: ::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qh: &wayland_client::QueueHandle, + ) { + if let wl_seat::Event::Name { name } = event { + parent + .as_mut() + .seats + .get_mut(seat) + .unwrap() + .set_name(name); + } + } +} + +impl WlrBackend { + pub fn new(display: &str) -> Result { + let socket_name = OsString::from(display); + let (mut queue, mut common) = connect(false, Some(socket_name))?; + + if common.seats.is_empty() { + return Err(Error::NoSeats); + } + + for (seat, data) in &mut common.seats { + let device = common + .clipboard_manager + .get_data_device(seat, &queue.handle(), seat.clone()); + data.set_device(Some(device)); + } + + let mut state = SessionState { + common, + offers: HashMap::new(), + data_sources: HashMap::new(), + owned_sources: Vec::new(), + }; + + queue + .roundtrip(&mut state) + .map_err(Error::WaylandCommunication)?; + + Ok(Self(Rc::new(RefCell::new(WlrSession { queue, state })))) + } + + pub fn get_text(&self) -> Result { + let mut session = self.0.borrow_mut(); + session.state.offers.clear(); + { + let WlrSession { queue, state } = &mut *session; + queue.roundtrip(state).map_err(Error::WaylandCommunication)?; + } + + let offer = session + .state + .common + .seats + .values() + .find_map(|seat| seat.offer.clone()) + .ok_or(Error::ClipboardEmpty)?; + + let mut mime_types = session + .state + .offers + .remove(&offer) + .unwrap_or_default(); + + let mime_type = mime_types + .iter() + .position(|x| x == "text/plain;charset=utf-8") + .or_else(|| mime_types.iter().position(|x| x == "UTF8_STRING")) + .or_else(|| mime_types.iter().position(|x| is_text(x))) + .map(|i| mime_types.swap_remove(i)); + + let Some(mime_type) = mime_type else { + return Err(Error::NoMimeType); + }; + + let (mut read, write) = pipe().map_err(Error::PipeCreation)?; + offer.receive(mime_type.clone(), write.as_fd()); + mem::drop(write); + + { + let WlrSession { queue, state } = &mut *session; + queue.roundtrip(state).map_err(Error::WaylandCommunication)?; + } + + let mut contents = Vec::new(); + read.read_to_end(&mut contents).map_err(Error::Io)?; + Ok(String::from_utf8_lossy(&contents).to_string()) + } + + pub fn set_text(&self, value: &str) -> Result<(), Error> { + let mut session = self.0.borrow_mut(); + for source in session.state.owned_sources.drain(..) { + source.destroy(); + } + + let bytes: Arc<[u8]> = Arc::from(value.as_bytes()); + session.state.data_sources.clear(); + for mime in TEXT_MIMES { + session + .state + .data_sources + .insert((*mime).to_string(), bytes.clone()); + } + + { + let WlrSession { queue, state } = &mut *session; + let qh = queue.handle(); + let devices: Vec<_> = state + .common + .seats + .values() + .filter_map(|seat_data| seat_data.device.clone()) + .collect(); + for device in devices { + let source = state.common.clipboard_manager.create_data_source(&qh); + for mime in state.data_sources.keys() { + source.offer(mime.clone()); + } + device.set_selection(Some(&source)); + state.owned_sources.push(source); + } + queue.roundtrip(state).map_err(Error::WaylandCommunication)?; + } + Ok(()) + } + + pub fn get_text_or_empty(&self) -> MyResult { + match self.get_text() { + Ok(text) => Ok(text), + Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) | Err(Error::NoSeats) => { + Ok(String::new()) + } + Err(err) => Err(err.into()), + } + } + + pub fn set_text_result(&self, value: &str) -> MyResult<()> { + self.set_text(value).map_err(Into::into) + } +} + +fn connect( + primary: bool, + socket_name: Option, +) -> Result<(EventQueue, ConnectionState), Error> { + let conn = match socket_name { + Some(name) => { + let mut socket_path = env::var_os("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .ok_or(ConnectError::NoCompositor) + .map_err(Error::WaylandConnection)?; + if !socket_path.is_absolute() { + return Err(Error::WaylandConnection(ConnectError::NoCompositor)); + } + socket_path.push(name); + let stream = UnixStream::connect(socket_path).map_err(Error::SocketOpenError)?; + Connection::from_socket(stream) + } + None => Connection::connect_to_env(), + } + .map_err(Error::WaylandConnection)?; + + let (globals, queue) = registry_queue_init::(&conn).map_err(|err| match err { + GlobalError::Backend(err) => Error::WaylandCommunication(err.into()), + GlobalError::InvalidId(err) => panic!("missing wl_registry: {err:?}"), + })?; + let qh = queue.handle(); + + let ext_manager = globals.bind(&qh, 1..=1, ()).ok().map(Manager::Ext); + let wlr_v = if primary { 2 } else { 1 }; + let wlr_manager = || globals.bind(&qh, wlr_v..=wlr_v, ()).ok().map(Manager::Zwlr); + + let clipboard_manager = match ext_manager.or_else(wlr_manager) { + Some(manager) => manager, + None => { + return Err(Error::MissingProtocol { + name: "ext-data-control, or wlr-data-control", + version: wlr_v, + }) + } + }; + + let registry = globals.registry(); + let seats = globals.contents().with_list(|globals| { + globals + .iter() + .filter(|global| global.interface == WlSeat::interface().name && global.version >= 2) + .map(|global| { + let seat = registry.bind(global.name, 2, &qh, ()); + (seat, SeatData::default()) + }) + .collect() + }); + + Ok(( + queue, + ConnectionState { + seats, + clipboard_manager, + }, + )) +} diff --git a/src/wlr_backend/seat_data.rs b/src/wlr_backend/seat_data.rs new file mode 100644 index 0000000..90eb286 --- /dev/null +++ b/src/wlr_backend/seat_data.rs @@ -0,0 +1,31 @@ +use crate::wlr_backend::data_control::{Device, Offer}; + +#[derive(Default)] +pub struct SeatData { + pub name: Option, + pub device: Option, + pub offer: Option, + pub primary_offer: Option, +} + +impl SeatData { + pub fn set_name(&mut self, name: String) { + self.name = Some(name); + } + + pub fn set_device(&mut self, device: Option) { + let old_device = self.device.take(); + self.device = device; + if let Some(device) = old_device { + device.destroy(); + } + } + + pub fn set_offer(&mut self, new_offer: Option) { + let old_offer = self.offer.take(); + self.offer = new_offer; + if let Some(offer) = old_offer { + offer.destroy(); + } + } +}