From ba5ebda4a5aefcfb6d9d8df9733bf32a07c9bd58 Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 7 Jul 2026 15:01:13 +0800 Subject: [PATCH 1/2] Fix Wayland FD leak and clipboard sync feedback loop Reuse a persistent Wayland data-control connection in WlrBackend instead of opening a new wl-clipboard-rs session on every get/set, which caused EMFILE crashes under polling. Upgrade wl-clipboard-rs and wayland-client, and rewrite keep_synced to track per-clipboard state with a post-sync settle period so X11/Wayland write lag no longer spams sync logs. Fixes #50 Co-authored-by: Cursor --- Cargo.lock | 293 +++++++++++++++--------- Cargo.toml | 8 +- src/clipboard.rs | 61 ++--- src/error.rs | 7 +- src/main.rs | 1 + src/sync.rs | 141 +++++++----- src/wlr_backend/data_control.rs | 276 +++++++++++++++++++++++ src/wlr_backend/mod.rs | 383 ++++++++++++++++++++++++++++++++ src/wlr_backend/seat_data.rs | 31 +++ 9 files changed, 1000 insertions(+), 201 deletions(-) create mode 100644 src/wlr_backend/data_control.rs create mode 100644 src/wlr_backend/mod.rs create mode 100644 src/wlr_backend/seat_data.rs 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..5ce02ae 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,13 +1,10 @@ use std::cell::RefCell; use std::rc::Rc; -use std::{env, io::Read, process::Command}; +use std::{env, 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 crate::error::{Generify, MyResult, Standardize}; +use crate::error::{MyResult, Standardize}; +use crate::wlr_backend::WlrBackend; pub trait Clipboard: std::fmt::Debug { fn display(&self) -> String; @@ -47,9 +44,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 +73,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 { diff --git a/src/error.rs b/src/error.rs index 950c064..6f5e413 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), 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..0007861 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,8 +1,10 @@ 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}; @@ -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; } } } @@ -198,41 +246,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 +287,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..28d8685 --- /dev/null +++ b/src/wlr_backend/mod.rs @@ -0,0 +1,383 @@ +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::{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) { + let copy_result = (|| { + fcntl_setfl(&fd, OFlags::empty())?; + let mut target = File::from(fd); + io::copy(&mut Cursor::new(&**data), &mut target).map(drop) + })(); + if copy_result.is_err() { + source.destroy(); + } + } + } + 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(); + } + } +} From 28f1f2844ae8a9d8587001ce70a997e90f55903c Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Tue, 28 Jul 2026 17:20:37 +0800 Subject: [PATCH 2/2] Fix clipboard loss and sync stalls with large clipboard contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Large clipboard transfers exposed three failures that wiped or froze clipboard synchronization: - wlr_backend served data-source Send requests inline during event dispatch with a blocking write. Once the main loop owned the selection, its own get_text() roundtrip triggered Send for content larger than the pipe capacity: the write blocked waiting for the read end, which only runs after the roundtrip returns — a deadlock that froze all syncing and left Wayland pastes unserved until the watchdog restart. Send is now served from a side thread, and a failed write no longer destroys the source (a reader going away must not clear the clipboard). - X11 reads ran synchronously in the poll loop. Slow INCR transfers (selection owners proxying data from another machine) starved Wayland event dispatch, so Wayland apps timed out pasting. Reads now run in a dedicated watcher thread with a bounded timeout and populate a cache; the poll loop never blocks on X11. This also replaces the terminal-clipboard wrapper with direct x11-clipboard usage, lifting its hardcoded 2s read timeout that large transfers could never meet. - Transient backend read failures (Wayland NoSeats/NoMimeType when the selection holds non-text data, X11 errors collapsed by unwrap_or_default) surfaced as empty strings and were propagated as clipboard clears, wiping every synced clipboard. Empty reads are now never treated as changes. --- src/clipboard.rs | 94 ++++++++++++++++++++++++++---------------- src/error.rs | 3 ++ src/sync.rs | 9 ++-- src/wlr_backend/mod.rs | 21 ++++++---- 4 files changed, 77 insertions(+), 50 deletions(-) diff --git a/src/clipboard.rs b/src/clipboard.rs index 5ce02ae..f8bb9bc 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,9 +1,11 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::{env, process::Command}; -use terminal_clipboard::Clipboard as TerminalClipboard; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{env, process::Command, thread}; -use crate::error::{MyResult, Standardize}; +use chrono::Local; + +use crate::error::MyResult; +use crate::log; use crate::wlr_backend::WlrBackend; pub trait Clipboard: std::fmt::Debug { @@ -147,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 { @@ -172,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 { @@ -191,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 6f5e413..f42fdfa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,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/sync.rs b/src/sync.rs index 0007861..5ce4e9c 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -7,7 +7,7 @@ use wayland_client::ConnectError; 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>> { @@ -218,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 {}: {}", diff --git a/src/wlr_backend/mod.rs b/src/wlr_backend/mod.rs index 28d8685..586bcbf 100644 --- a/src/wlr_backend/mod.rs +++ b/src/wlr_backend/mod.rs @@ -11,6 +11,7 @@ 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; @@ -153,14 +154,18 @@ impl_dispatch_source!(SessionState, |state: &mut SessionState, match event { Event::Send { mime_type, fd } => { if let Some(data) = state.data_sources.get(&mime_type) { - let copy_result = (|| { - fcntl_setfl(&fd, OFlags::empty())?; - let mut target = File::from(fd); - io::copy(&mut Cursor::new(&**data), &mut target).map(drop) - })(); - if copy_result.is_err() { - source.destroy(); - } + // 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(),