├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── async_io.rs ├── logger.rs ├── main.rs ├── settings.rs ├── states.rs └── wayland.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anstream" 46 | version = "0.6.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 49 | dependencies = [ 50 | "anstyle", 51 | "anstyle-parse", 52 | "anstyle-query", 53 | "anstyle-wincon", 54 | "colorchoice", 55 | "is_terminal_polyfill", 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle" 61 | version = "1.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 64 | 65 | [[package]] 66 | name = "anstyle-parse" 67 | version = "0.2.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 70 | dependencies = [ 71 | "utf8parse", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-query" 76 | version = "1.0.3" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 79 | dependencies = [ 80 | "windows-sys 0.52.0", 81 | ] 82 | 83 | [[package]] 84 | name = "anstyle-wincon" 85 | version = "3.0.3" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 88 | dependencies = [ 89 | "anstyle", 90 | "windows-sys 0.52.0", 91 | ] 92 | 93 | [[package]] 94 | name = "autocfg" 95 | version = "1.2.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 98 | 99 | [[package]] 100 | name = "backtrace" 101 | version = "0.3.71" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 104 | dependencies = [ 105 | "addr2line", 106 | "cc", 107 | "cfg-if", 108 | "libc", 109 | "miniz_oxide", 110 | "object", 111 | "rustc-demangle", 112 | ] 113 | 114 | [[package]] 115 | name = "bit-set" 116 | version = "0.5.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 119 | dependencies = [ 120 | "bit-vec", 121 | ] 122 | 123 | [[package]] 124 | name = "bit-vec" 125 | version = "0.6.3" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 128 | 129 | [[package]] 130 | name = "bumpalo" 131 | version = "3.16.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 134 | 135 | [[package]] 136 | name = "bytes" 137 | version = "1.6.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 140 | 141 | [[package]] 142 | name = "cc" 143 | version = "1.0.96" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" 146 | 147 | [[package]] 148 | name = "cfg-if" 149 | version = "1.0.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 152 | 153 | [[package]] 154 | name = "chrono" 155 | version = "0.4.38" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 158 | dependencies = [ 159 | "android-tzdata", 160 | "iana-time-zone", 161 | "js-sys", 162 | "num-traits", 163 | "wasm-bindgen", 164 | "windows-targets 0.52.5", 165 | ] 166 | 167 | [[package]] 168 | name = "clap" 169 | version = "4.5.4" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 172 | dependencies = [ 173 | "clap_builder", 174 | "clap_derive", 175 | ] 176 | 177 | [[package]] 178 | name = "clap_builder" 179 | version = "4.5.2" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 182 | dependencies = [ 183 | "anstream", 184 | "anstyle", 185 | "clap_lex", 186 | "strsim", 187 | ] 188 | 189 | [[package]] 190 | name = "clap_derive" 191 | version = "4.5.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 194 | dependencies = [ 195 | "heck", 196 | "proc-macro2", 197 | "quote", 198 | "syn", 199 | ] 200 | 201 | [[package]] 202 | name = "clap_lex" 203 | version = "0.7.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 206 | 207 | [[package]] 208 | name = "colorchoice" 209 | version = "1.0.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 212 | 213 | [[package]] 214 | name = "core-foundation-sys" 215 | version = "0.8.6" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 218 | 219 | [[package]] 220 | name = "env_filter" 221 | version = "0.1.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 224 | dependencies = [ 225 | "log", 226 | "regex", 227 | ] 228 | 229 | [[package]] 230 | name = "env_logger" 231 | version = "0.11.3" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" 234 | dependencies = [ 235 | "anstream", 236 | "anstyle", 237 | "env_filter", 238 | "log", 239 | ] 240 | 241 | [[package]] 242 | name = "equivalent" 243 | version = "1.0.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 246 | 247 | [[package]] 248 | name = "fancy-regex" 249 | version = "0.13.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" 252 | dependencies = [ 253 | "bit-set", 254 | "regex-automata", 255 | "regex-syntax", 256 | ] 257 | 258 | [[package]] 259 | name = "futures-core" 260 | version = "0.3.30" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 263 | 264 | [[package]] 265 | name = "futures-task" 266 | version = "0.3.30" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 269 | 270 | [[package]] 271 | name = "futures-util" 272 | version = "0.3.30" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 275 | dependencies = [ 276 | "futures-core", 277 | "futures-task", 278 | "pin-project-lite", 279 | "pin-utils", 280 | ] 281 | 282 | [[package]] 283 | name = "gimli" 284 | version = "0.28.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 287 | 288 | [[package]] 289 | name = "hashbrown" 290 | version = "0.14.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 293 | 294 | [[package]] 295 | name = "heck" 296 | version = "0.5.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 299 | 300 | [[package]] 301 | name = "hermit-abi" 302 | version = "0.3.9" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 305 | 306 | [[package]] 307 | name = "iana-time-zone" 308 | version = "0.1.60" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 311 | dependencies = [ 312 | "android_system_properties", 313 | "core-foundation-sys", 314 | "iana-time-zone-haiku", 315 | "js-sys", 316 | "wasm-bindgen", 317 | "windows-core", 318 | ] 319 | 320 | [[package]] 321 | name = "iana-time-zone-haiku" 322 | version = "0.1.2" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 325 | dependencies = [ 326 | "cc", 327 | ] 328 | 329 | [[package]] 330 | name = "indexmap" 331 | version = "2.2.6" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 334 | dependencies = [ 335 | "equivalent", 336 | "hashbrown", 337 | ] 338 | 339 | [[package]] 340 | name = "is_terminal_polyfill" 341 | version = "1.70.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 344 | 345 | [[package]] 346 | name = "js-sys" 347 | version = "0.3.69" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 350 | dependencies = [ 351 | "wasm-bindgen", 352 | ] 353 | 354 | [[package]] 355 | name = "libc" 356 | version = "0.2.154" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 359 | 360 | [[package]] 361 | name = "log" 362 | version = "0.4.21" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 365 | 366 | [[package]] 367 | name = "memchr" 368 | version = "2.7.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 371 | 372 | [[package]] 373 | name = "miniz_oxide" 374 | version = "0.7.2" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 377 | dependencies = [ 378 | "adler", 379 | ] 380 | 381 | [[package]] 382 | name = "mio" 383 | version = "0.8.11" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 386 | dependencies = [ 387 | "libc", 388 | "wasi", 389 | "windows-sys 0.48.0", 390 | ] 391 | 392 | [[package]] 393 | name = "num-traits" 394 | version = "0.2.18" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 397 | dependencies = [ 398 | "autocfg", 399 | ] 400 | 401 | [[package]] 402 | name = "num_cpus" 403 | version = "1.16.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 406 | dependencies = [ 407 | "hermit-abi", 408 | "libc", 409 | ] 410 | 411 | [[package]] 412 | name = "object" 413 | version = "0.32.2" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 416 | dependencies = [ 417 | "memchr", 418 | ] 419 | 420 | [[package]] 421 | name = "once_cell" 422 | version = "1.19.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 425 | 426 | [[package]] 427 | name = "pin-project-lite" 428 | version = "0.2.14" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 431 | 432 | [[package]] 433 | name = "pin-utils" 434 | version = "0.1.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 437 | 438 | [[package]] 439 | name = "proc-macro-crate" 440 | version = "3.1.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" 443 | dependencies = [ 444 | "toml_edit", 445 | ] 446 | 447 | [[package]] 448 | name = "proc-macro2" 449 | version = "1.0.81" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 452 | dependencies = [ 453 | "unicode-ident", 454 | ] 455 | 456 | [[package]] 457 | name = "quick-xml" 458 | version = "0.31.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" 461 | dependencies = [ 462 | "memchr", 463 | ] 464 | 465 | [[package]] 466 | name = "quote" 467 | version = "1.0.36" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 470 | dependencies = [ 471 | "proc-macro2", 472 | ] 473 | 474 | [[package]] 475 | name = "regex" 476 | version = "1.10.4" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 479 | dependencies = [ 480 | "aho-corasick", 481 | "memchr", 482 | "regex-automata", 483 | "regex-syntax", 484 | ] 485 | 486 | [[package]] 487 | name = "regex-automata" 488 | version = "0.4.6" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 491 | dependencies = [ 492 | "aho-corasick", 493 | "memchr", 494 | "regex-syntax", 495 | ] 496 | 497 | [[package]] 498 | name = "regex-syntax" 499 | version = "0.8.3" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 502 | 503 | [[package]] 504 | name = "rustc-demangle" 505 | version = "0.1.23" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 508 | 509 | [[package]] 510 | name = "socket2" 511 | version = "0.5.7" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 514 | dependencies = [ 515 | "libc", 516 | "windows-sys 0.52.0", 517 | ] 518 | 519 | [[package]] 520 | name = "strsim" 521 | version = "0.11.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 524 | 525 | [[package]] 526 | name = "syn" 527 | version = "2.0.60" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 530 | dependencies = [ 531 | "proc-macro2", 532 | "quote", 533 | "unicode-ident", 534 | ] 535 | 536 | [[package]] 537 | name = "thiserror" 538 | version = "1.0.59" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" 541 | dependencies = [ 542 | "thiserror-impl", 543 | ] 544 | 545 | [[package]] 546 | name = "thiserror-impl" 547 | version = "1.0.59" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" 550 | dependencies = [ 551 | "proc-macro2", 552 | "quote", 553 | "syn", 554 | ] 555 | 556 | [[package]] 557 | name = "tokio" 558 | version = "1.37.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" 561 | dependencies = [ 562 | "backtrace", 563 | "bytes", 564 | "libc", 565 | "mio", 566 | "num_cpus", 567 | "pin-project-lite", 568 | "socket2", 569 | "tokio-macros", 570 | "windows-sys 0.48.0", 571 | ] 572 | 573 | [[package]] 574 | name = "tokio-macros" 575 | version = "2.2.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 578 | dependencies = [ 579 | "proc-macro2", 580 | "quote", 581 | "syn", 582 | ] 583 | 584 | [[package]] 585 | name = "tokio-pipe" 586 | version = "0.2.12" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784" 589 | dependencies = [ 590 | "libc", 591 | "tokio", 592 | ] 593 | 594 | [[package]] 595 | name = "toml_datetime" 596 | version = "0.6.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 599 | 600 | [[package]] 601 | name = "toml_edit" 602 | version = "0.21.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" 605 | dependencies = [ 606 | "indexmap", 607 | "toml_datetime", 608 | "winnow", 609 | ] 610 | 611 | [[package]] 612 | name = "unicode-ident" 613 | version = "1.0.12" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 616 | 617 | [[package]] 618 | name = "utf8parse" 619 | version = "0.2.1" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 622 | 623 | [[package]] 624 | name = "wasi" 625 | version = "0.11.0+wasi-snapshot-preview1" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 628 | 629 | [[package]] 630 | name = "wasm-bindgen" 631 | version = "0.2.92" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 634 | dependencies = [ 635 | "cfg-if", 636 | "wasm-bindgen-macro", 637 | ] 638 | 639 | [[package]] 640 | name = "wasm-bindgen-backend" 641 | version = "0.2.92" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 644 | dependencies = [ 645 | "bumpalo", 646 | "log", 647 | "once_cell", 648 | "proc-macro2", 649 | "quote", 650 | "syn", 651 | "wasm-bindgen-shared", 652 | ] 653 | 654 | [[package]] 655 | name = "wasm-bindgen-macro" 656 | version = "0.2.92" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 659 | dependencies = [ 660 | "quote", 661 | "wasm-bindgen-macro-support", 662 | ] 663 | 664 | [[package]] 665 | name = "wasm-bindgen-macro-support" 666 | version = "0.2.92" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 669 | dependencies = [ 670 | "proc-macro2", 671 | "quote", 672 | "syn", 673 | "wasm-bindgen-backend", 674 | "wasm-bindgen-shared", 675 | ] 676 | 677 | [[package]] 678 | name = "wasm-bindgen-shared" 679 | version = "0.2.92" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 682 | 683 | [[package]] 684 | name = "wayrs-client" 685 | version = "1.1.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "c2fe9b4b50232be51af04c96bbe5888f272014e3550ba4370185f2cbba3de58d" 688 | dependencies = [ 689 | "thiserror", 690 | "tokio", 691 | "wayrs-core", 692 | "wayrs-scanner", 693 | ] 694 | 695 | [[package]] 696 | name = "wayrs-core" 697 | version = "1.0.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "443dab45c512423f367455e76dbf97196ecac5bb2bfbbf714b797fbc7be140b2" 700 | dependencies = [ 701 | "libc", 702 | "thiserror", 703 | ] 704 | 705 | [[package]] 706 | name = "wayrs-proto-parser" 707 | version = "2.0.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "1406ebeb6ba4a201745a92c221eca3dcec5b404fcbe948acf8a166b323582fa9" 710 | dependencies = [ 711 | "quick-xml", 712 | "thiserror", 713 | ] 714 | 715 | [[package]] 716 | name = "wayrs-protocols" 717 | version = "0.14.0" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "7e9b354ba4fc9fe9784d19acb433227e895233a0760f25243a9b54a88dc8f44b" 720 | dependencies = [ 721 | "wayrs-client", 722 | ] 723 | 724 | [[package]] 725 | name = "wayrs-scanner" 726 | version = "0.14.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "59b21c1bc712edfa6260dceff75d224331d89bc8757b19ce771ebc35ba33e1d0" 729 | dependencies = [ 730 | "proc-macro-crate", 731 | "proc-macro2", 732 | "quote", 733 | "syn", 734 | "wayrs-proto-parser", 735 | ] 736 | 737 | [[package]] 738 | name = "windows-core" 739 | version = "0.52.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 742 | dependencies = [ 743 | "windows-targets 0.52.5", 744 | ] 745 | 746 | [[package]] 747 | name = "windows-sys" 748 | version = "0.48.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 751 | dependencies = [ 752 | "windows-targets 0.48.5", 753 | ] 754 | 755 | [[package]] 756 | name = "windows-sys" 757 | version = "0.52.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 760 | dependencies = [ 761 | "windows-targets 0.52.5", 762 | ] 763 | 764 | [[package]] 765 | name = "windows-targets" 766 | version = "0.48.5" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 769 | dependencies = [ 770 | "windows_aarch64_gnullvm 0.48.5", 771 | "windows_aarch64_msvc 0.48.5", 772 | "windows_i686_gnu 0.48.5", 773 | "windows_i686_msvc 0.48.5", 774 | "windows_x86_64_gnu 0.48.5", 775 | "windows_x86_64_gnullvm 0.48.5", 776 | "windows_x86_64_msvc 0.48.5", 777 | ] 778 | 779 | [[package]] 780 | name = "windows-targets" 781 | version = "0.52.5" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 784 | dependencies = [ 785 | "windows_aarch64_gnullvm 0.52.5", 786 | "windows_aarch64_msvc 0.52.5", 787 | "windows_i686_gnu 0.52.5", 788 | "windows_i686_gnullvm", 789 | "windows_i686_msvc 0.52.5", 790 | "windows_x86_64_gnu 0.52.5", 791 | "windows_x86_64_gnullvm 0.52.5", 792 | "windows_x86_64_msvc 0.52.5", 793 | ] 794 | 795 | [[package]] 796 | name = "windows_aarch64_gnullvm" 797 | version = "0.48.5" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 800 | 801 | [[package]] 802 | name = "windows_aarch64_gnullvm" 803 | version = "0.52.5" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 806 | 807 | [[package]] 808 | name = "windows_aarch64_msvc" 809 | version = "0.48.5" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 812 | 813 | [[package]] 814 | name = "windows_aarch64_msvc" 815 | version = "0.52.5" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 818 | 819 | [[package]] 820 | name = "windows_i686_gnu" 821 | version = "0.48.5" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 824 | 825 | [[package]] 826 | name = "windows_i686_gnu" 827 | version = "0.52.5" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 830 | 831 | [[package]] 832 | name = "windows_i686_gnullvm" 833 | version = "0.52.5" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 836 | 837 | [[package]] 838 | name = "windows_i686_msvc" 839 | version = "0.48.5" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 842 | 843 | [[package]] 844 | name = "windows_i686_msvc" 845 | version = "0.52.5" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 848 | 849 | [[package]] 850 | name = "windows_x86_64_gnu" 851 | version = "0.48.5" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 854 | 855 | [[package]] 856 | name = "windows_x86_64_gnu" 857 | version = "0.52.5" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 860 | 861 | [[package]] 862 | name = "windows_x86_64_gnullvm" 863 | version = "0.48.5" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 866 | 867 | [[package]] 868 | name = "windows_x86_64_gnullvm" 869 | version = "0.52.5" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 872 | 873 | [[package]] 874 | name = "windows_x86_64_msvc" 875 | version = "0.48.5" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 878 | 879 | [[package]] 880 | name = "windows_x86_64_msvc" 881 | version = "0.52.5" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 884 | 885 | [[package]] 886 | name = "winnow" 887 | version = "0.5.40" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 890 | dependencies = [ 891 | "memchr", 892 | ] 893 | 894 | [[package]] 895 | name = "wl-clip-persist" 896 | version = "0.4.3" 897 | dependencies = [ 898 | "chrono", 899 | "clap", 900 | "env_logger", 901 | "fancy-regex", 902 | "futures-util", 903 | "libc", 904 | "log", 905 | "tokio", 906 | "tokio-pipe", 907 | "wayrs-client", 908 | "wayrs-protocols", 909 | ] 910 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wl-clip-persist" 3 | version = "0.4.3" 4 | description = "Keep Wayland clipboard even after programs close" 5 | authors = ["Linus789"] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/Linus789/wl-clip-persist" 9 | categories = ["command-line-utilities"] 10 | edition = "2021" 11 | rust-version = "1.76.0" 12 | 13 | [dependencies] 14 | log = "0.4" 15 | env_logger = { version = "0.11", default-features = false, features = ["auto-color", "regex"] } 16 | chrono = "0.4" 17 | clap = { version = "4.5", features = ["cargo", "derive"] } 18 | tokio = { version = "1.37", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"] } 19 | tokio-pipe = "0.2" 20 | futures-util = { version = "0.3", default-features = false, features = ["alloc"] } 21 | wayrs-client = { version = "1.1", features = ["tokio"] } 22 | wayrs-protocols = { version = "0.14", features = ["wlr-data-control-unstable-v1"] } 23 | fancy-regex = "0.13" 24 | libc = "0.2" 25 | 26 | [profile.release] 27 | lto = true 28 | strip = "symbols" 29 | codegen-units = 1 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Linus789 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wl-clip-persist 2 | Normally, when you copy something on Wayland and then close the application you copied from, the copied data (e.g. text) disappears and you cannot paste it anymore. If you run wl-clip-persist in the background, however, the copied data persists. 3 | 4 | ## How it works 5 | Whenever you copy something, it reads all the clipboard data into memory and then overwrites the clipboard with the data from our memory. 6 | By doing so, the data is available even after the program you copied from exits. 7 | 8 | ## Usage 9 | ### Clipboard Type 10 | When you specify the clipboard to operate on, the clipboard data there will persist. 11 | The clipboards we don’t operate on will continue to behave like before. 12 | 13 | Regular clipboard 14 | ``` 15 | wl-clip-persist --clipboard regular 16 | ``` 17 | 18 | Primary clipboard 19 | ``` 20 | wl-clip-persist --clipboard primary 21 | ``` 22 | 23 | Regular and Primary clipboard 24 | ``` 25 | wl-clip-persist --clipboard both 26 | ``` 27 | 28 | > [!NOTE] 29 | > The general recommendation is to operate on the regular clipboard only, 30 | since the primary clipboard seems to have unintended side effects for some applications, 31 | [see here](#primary-selection-mode-breaks-the-selection-system-3). 32 | 33 | ## Optional arguments 34 | ### Write timeout 35 | *Default: 3000 ms* 36 | 37 | It is possible to change the write timeout. 38 | In this example, the write timeout is reduced to 1000 ms. 39 | ``` 40 | wl-clip-persist --clipboard regular --write-timeout 1000 41 | ``` 42 | 43 | If the data size exceeds the pipe buffer capacity, we will have to 44 | wait for the receiving client to read some of the content to write the rest to it. 45 | To avoid keeping old clipboard data around for too long, there is a timeout 46 | which is also useful to limit the memory usage. 47 | 48 | ### Ignore event on error 49 | *Default: disabled* 50 | 51 | With `--ignore-event-on-error` only selection events where no error occurred are handled. If an error occurred and the selection event is ignored, you will still be able to paste the clipboard, but only for as long as the program you copied from is open. 52 | 53 | When this option is disabled, it will try to read the entire data for as many MIME types as possible. For example, when a clipboard event offers `image/png` and `text/plain` data and we are only able to read the `text/plain` data entirely because a read error occurred for `image/png`, then the clipboard will be overwritten with our data that only offers `text/plain`. 54 | 55 | ### Filter 56 | *Default: no filter* 57 | 58 | With `--all-mime-type-regex ` only selection events where all offered MIME types have a match for the regex are handled. 59 | You might want to use this option to ignore selection events that offer for example images. If the event is ignored, you will still be able to paste the images, but only for as long as the program you copied them from is open. 60 | 61 | Ignore events that offer images 62 | ``` 63 | wl-clip-persist --clipboard regular --all-mime-type-regex '(?i)^(?!image/).+' 64 | ``` 65 | 66 | Ignore most events that offer something else than text 67 | ``` 68 | wl-clip-persist --clipboard regular --all-mime-type-regex '(?i)^(?!(?:image|audio|video|font|model)/).+' 69 | ``` 70 | 71 | ### Selection size limit 72 | *Default: no limit* 73 | 74 | With `--selection-size-limit ` only selection events whose total data size does not exceed the size limit are handled. If the size limit has been exceeded, you will still be able to paste the clipboard, but only for as long as the program you copied from is open. 75 | 76 | Ignore events that exceed the size of 1 MiB 77 | ``` 78 | wl-clip-persist --clipboard regular --selection-size-limit 1048576 79 | ``` 80 | 81 | This option can be used to limit the memory usage. 82 | 83 | ### Reconnect tries 84 | *Default: no limit* 85 | 86 | With `--reconnect-tries`, the number of tries to reconnect to the Wayland server after a Wayland error occurred will be limited. 87 | 88 | This option only applies if a Wayland error occurred after at least one successful connection to the Wayland server has been established. If the first connection to the Wayland server on startup fails, the application will exit with exit code 1. 89 | 90 | ### Reconnect delay 91 | *Default: 100 ms* 92 | 93 | With `--reconnect-delay`, the delay between reconnect tries to the Wayland server is adjusted. 94 | 95 | After a Wayland error occurred, a reconnect will be tried immediately. If the reconnect failed, the delay is applied before another reconnect is tried. 96 | 97 | ### Disable timestamps 98 | *Default: not disabled* 99 | 100 | With `--disable-timestamps`, the timestamps in the log messages are disabled. 101 | This might be useful for systemd services or similar, which provide their own timestamps. 102 | 103 | ### Logging 104 | You can modify the log level to see more of what is going on, e.g. 105 | ``` 106 | RUST_LOG=trace wl-clip-persist --clipboard regular 107 | ``` 108 | 109 | ## Troubleshooting 110 | ### Primary selection mode breaks the selection system ([#3](https://github.com/Linus789/wl-clip-persist/issues/3)) 111 | > especially those based on GTK, e.g. Thunar and Inkscape 112 | 113 | > [...] once you start using the primary mode or both, it becomes impossible to select text, because once you release the cursor to finalize the selection, it disappears. 114 | 115 | **Solution:** 116 | Use the regular clipboard only, e.g. 117 | ``` 118 | wl-clip-persist --clipboard regular 119 | ``` 120 | 121 | ### Inkscape crashes when copy-pasting anything ([#7](https://github.com/Linus789/wl-clip-persist/issues/7)) 122 | **Solution:** 123 | ``` 124 | wl-clip-persist --clipboard regular --all-mime-type-regex '(?i)^(?!image/x-inkscape-svg).+' 125 | ``` 126 | 127 | ## FAQ 128 | ### Is it possible to have a clipboard history? 129 | It is perfectly possible to use a clipboard history application alongside wl-clip-persist. 130 | For example [cliphist](https://github.com/sentriz/cliphist) will work. 131 | 132 | ### Is it possible to ignore clipboard events from password managers? 133 | Depends. If the password manager advertises the selection event with the additional MIME type `x-kde-passwordManagerHint`, 134 | like for example KeePassXC does, then we can ignore the selection event via: 135 | ``` 136 | wl-clip-persist --clipboard regular --all-mime-type-regex '^(?!x-kde-passwordManagerHint).+' 137 | ``` 138 | 139 | ## Build from source 140 | * Install `rustup` to get the `rust` compiler installed on your system. [Install rustup](https://www.rust-lang.org/en-US/install.html) 141 | * Rust version 1.76.0 or later is required 142 | * Build in release mode: `cargo build --release` 143 | * The resulting executable can be found at `target/release/wl-clip-persist` 144 | 145 | ## Thanks 146 | * [wl-clipboard-rs](https://github.com/YaLTeR/wl-clipboard-rs) for showing how to interact with the Wayland clipboard 147 | * [wl-gammarelay-rs](https://github.com/MaxVerevkin/wl-gammarelay-rs) for showing how to use [wayrs](https://github.com/MaxVerevkin/wayrs) 148 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" 3 | max_width = 120 4 | use_field_init_shorthand = true 5 | newline_style = "Unix" 6 | -------------------------------------------------------------------------------- /src/async_io.rs: -------------------------------------------------------------------------------- 1 | // Basically a copy-paste from the tokio-pipe crate 2 | use std::io::ErrorKind; 3 | use std::os::fd::{AsRawFd, OwnedFd, RawFd}; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures_util::ready; 8 | use tokio::io::unix::AsyncFd; 9 | use tokio::io::AsyncWrite; 10 | 11 | const MAX_LEN: usize = ::MAX as _; 12 | 13 | fn set_non_blocking(fd: RawFd) -> Result<(), std::io::Error> { 14 | let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; 15 | 16 | if fd_flags == -1 { 17 | return Err(std::io::Error::last_os_error()); 18 | } 19 | 20 | if (fd_flags & libc::O_NONBLOCK) == 0 { 21 | let set_result = unsafe { libc::fcntl(fd, libc::F_SETFL, fd_flags | libc::O_NONBLOCK) }; 22 | 23 | if set_result == -1 { 24 | return Err(std::io::Error::last_os_error()); 25 | } 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | pub struct FdWrite(AsyncFd); 32 | 33 | impl TryFrom for FdWrite { 34 | type Error = std::io::Error; 35 | 36 | fn try_from(fd: OwnedFd) -> Result { 37 | set_non_blocking(fd.as_raw_fd())?; 38 | Ok(Self(AsyncFd::new(fd)?)) 39 | } 40 | } 41 | 42 | impl FdWrite { 43 | fn poll_write_impl(self: Pin<&Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 44 | let fd = self.0.as_raw_fd(); 45 | 46 | loop { 47 | let pinned = Pin::new(&self.0); 48 | let mut ready = match ready!(pinned.poll_write_ready(cx)) { 49 | Ok(ready) => ready, 50 | Err(err) => return Poll::Ready(Err(err)), 51 | }; 52 | 53 | let write_result = unsafe { libc::write(fd, buf.as_ptr().cast(), std::cmp::min(buf.len(), MAX_LEN)) }; 54 | 55 | if write_result == -1 { 56 | match std::io::Error::last_os_error() { 57 | err if err.kind() == ErrorKind::WouldBlock => { 58 | ready.clear_ready(); 59 | } 60 | err => return Poll::Ready(Err(err)), 61 | } 62 | } else { 63 | match usize::try_from(write_result) { 64 | Ok(written_bytes) => return Poll::Ready(Ok(written_bytes)), 65 | Err(err) => return Poll::Ready(Err(std::io::Error::new(ErrorKind::Other, err))), 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl AsyncWrite for FdWrite { 73 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 74 | self.as_ref().poll_write_impl(cx, buf) 75 | } 76 | 77 | fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { 78 | Poll::Ready(Ok(())) 79 | } 80 | 81 | fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 82 | Poll::Ready(Ok(())) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use env_logger::fmt::style::{AnsiColor, Color, Style}; 3 | use env_logger::Builder; 4 | use log::{Level, LevelFilter}; 5 | 6 | pub(crate) fn init_logger(with_timestamps: bool) { 7 | custom_logger_builder("%Y-%m-%dT%H:%M:%S.%3f", with_timestamps) 8 | .filter_level(LevelFilter::Info) 9 | .parse_default_env() 10 | .init() 11 | } 12 | 13 | fn custom_logger_builder(fmt: &'static str, with_timestamps: bool) -> Builder { 14 | let mut builder = Builder::new(); 15 | 16 | builder.format(move |f, record| { 17 | use std::io::Write as _; 18 | 19 | let target = record.target(); 20 | 21 | if !target.starts_with(clap::crate_name!()) { 22 | return Ok(()); 23 | } 24 | 25 | let time = with_timestamps.then(|| Local::now().format(fmt)); 26 | let level = record.level(); 27 | let level_text = level_text(&level); 28 | let level_style = level_style(&level); 29 | let display_target = target.strip_prefix(concat!(clap::crate_name!(), ' ')).unwrap_or("main"); 30 | let message = record.args().to_string().lines().collect::>().join("\n "); 31 | 32 | if let Some(time) = time { 33 | writeln!( 34 | f, 35 | "{} {}{}{} {} > {}", 36 | time, 37 | level_style.render(), 38 | level_text, 39 | level_style.render_reset(), 40 | display_target, 41 | message 42 | ) 43 | } else { 44 | writeln!( 45 | f, 46 | "{}{}{} {} > {}", 47 | level_style.render(), 48 | level_text, 49 | level_style.render_reset(), 50 | display_target, 51 | message 52 | ) 53 | } 54 | }); 55 | 56 | builder 57 | } 58 | 59 | fn level_text(level: &Level) -> &'static str { 60 | match level { 61 | Level::Trace => "TRACE", 62 | Level::Debug => "DEBUG", 63 | Level::Info => "INFO ", 64 | Level::Warn => "WARN ", 65 | Level::Error => "ERROR", 66 | } 67 | } 68 | 69 | fn level_style(level: &Level) -> Style { 70 | Style::new().fg_color(Some(match level { 71 | Level::Trace => Color::Ansi(AnsiColor::Magenta), 72 | Level::Debug => Color::Ansi(AnsiColor::Blue), 73 | Level::Info => Color::Ansi(AnsiColor::Green), 74 | Level::Warn => Color::Ansi(AnsiColor::Yellow), 75 | Level::Error => Color::Ansi(AnsiColor::Red), 76 | })) 77 | } 78 | 79 | /// Returns a formatted target for logging purposes. 80 | pub(crate) const fn log_default_target() -> &'static str { 81 | clap::crate_name!() 82 | } 83 | 84 | /// Returns a formatted target for logging purposes. 85 | pub(crate) fn log_seat_target(seat_name: u32) -> String { 86 | format!("{} Seat {}", clap::crate_name!(), seat_name) 87 | } 88 | 89 | /// If title_case is false, lower case is used instead of title case. 90 | pub(crate) const fn get_clipboard_type_str(is_primary_clipboard: bool, title_case: bool) -> &'static str { 91 | match (is_primary_clipboard, title_case) { 92 | (true, true) => "Primary", 93 | (true, false) => "primary", 94 | (false, true) => "Regular", 95 | (false, false) => "regular", 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod async_io; 2 | mod logger; 3 | mod settings; 4 | mod states; 5 | mod wayland; 6 | 7 | use std::time::Duration; 8 | 9 | use crate::logger::log_default_target; 10 | use crate::settings::get_settings; 11 | use crate::states::WaylandError; 12 | 13 | // One worker thread for writing the clipboard data 14 | #[tokio::main(flavor = "multi_thread", worker_threads = 1)] 15 | async fn main() { 16 | let settings = get_settings(); 17 | let mut is_reconnect = false; 18 | let mut connection_tries = 0; 19 | 20 | loop { 21 | match wayland::run(settings.clone(), is_reconnect).await { 22 | Ok(_) => unreachable!(), 23 | Err(WaylandError::ConnectError(err)) => { 24 | if is_reconnect { 25 | if settings.reconnect_tries.is_some() { 26 | connection_tries += 1; 27 | } 28 | 29 | if Some(connection_tries) == settings.reconnect_tries { 30 | log::error!( 31 | target: log_default_target(), 32 | "Wayland connect error: {}", 33 | err, 34 | ); 35 | std::process::exit(1); 36 | } else if settings.reconnect_delay == Duration::ZERO { 37 | match settings.reconnect_tries { 38 | Some(reconnect_tries) => { 39 | log::error!( 40 | target: log_default_target(), 41 | "Wayland connect error: {}\nAttempt {}/{} to reconnect will start immediately...", 42 | err, 43 | connection_tries + 1, 44 | reconnect_tries, 45 | ); 46 | } 47 | None => { 48 | log::error!( 49 | target: log_default_target(), 50 | "Wayland connect error: {}\nAttempt to reconnect will start immediately...", 51 | err, 52 | ); 53 | } 54 | } 55 | } else { 56 | match settings.reconnect_tries { 57 | Some(reconnect_tries) => { 58 | log::error!( 59 | target: log_default_target(), 60 | "Wayland connect error: {}\nAttempt {}/{} to reconnect in {} ms...", 61 | err, 62 | connection_tries + 1, 63 | reconnect_tries, 64 | settings.reconnect_delay.as_millis().max(1) 65 | ); 66 | } 67 | None => { 68 | log::error!( 69 | target: log_default_target(), 70 | "Wayland connect error: {}\nAttempt to reconnect in {} ms...", 71 | err, 72 | settings.reconnect_delay.as_millis().max(1) 73 | ); 74 | } 75 | } 76 | 77 | tokio::time::sleep(settings.reconnect_delay).await; 78 | } 79 | } else { 80 | log::error!( 81 | target: log_default_target(), 82 | "Failed to connect to wayland server: {}", 83 | err, 84 | ); 85 | std::process::exit(1); 86 | } 87 | } 88 | Err(WaylandError::IoError(err)) => { 89 | if settings.reconnect_tries.map(|tries| tries > 0).unwrap_or(true) { 90 | is_reconnect = true; 91 | connection_tries = 0; 92 | 93 | match settings.reconnect_tries { 94 | Some(reconnect_tries) => { 95 | log::error!( 96 | target: log_default_target(), 97 | "Wayland IO error: {}\nAttempt {}/{} to reconnect will start immediately...", 98 | err, 99 | connection_tries + 1, 100 | reconnect_tries, 101 | ); 102 | } 103 | None => { 104 | log::error!( 105 | target: log_default_target(), 106 | "Wayland IO error: {}\nAttempt to reconnect will start immediately...", 107 | err, 108 | ); 109 | } 110 | } 111 | } else { 112 | log::error!( 113 | target: log_default_target(), 114 | "Wayland IO error: {}", 115 | err, 116 | ); 117 | std::process::exit(1); 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU64; 2 | use std::time::Duration; 3 | 4 | use clap::builder::NonEmptyStringValueParser; 5 | use clap::{arg, crate_description, crate_name, crate_version, value_parser, ArgAction, Command}; 6 | use fancy_regex::Regex; 7 | 8 | use crate::logger::{self, log_default_target}; 9 | 10 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] 11 | pub(crate) enum ClipboardType { 12 | Regular, 13 | Primary, 14 | Both, 15 | } 16 | 17 | impl ClipboardType { 18 | /// Whether the primary selection is activated. 19 | pub(crate) const fn primary(&self) -> bool { 20 | match self { 21 | Self::Primary | Self::Both => true, 22 | Self::Regular => false, 23 | } 24 | } 25 | 26 | /// Whether the regular selection is activated. 27 | pub(crate) const fn regular(&self) -> bool { 28 | match self { 29 | Self::Regular | Self::Both => true, 30 | Self::Primary => false, 31 | } 32 | } 33 | } 34 | 35 | /// The settings the program was started with. 36 | #[derive(Debug, Clone)] 37 | pub(crate) struct Settings { 38 | /// The clipboard types which are activated. 39 | pub(crate) clipboard_type: ClipboardType, 40 | /// The write timeout when writing the clipboard data to other clients. 41 | pub(crate) write_timeout: Duration, 42 | /// Whether selection events should be ignored when at least one error occurred. 43 | pub(crate) ignore_selection_event_on_error: bool, 44 | /// The selection size limit in bytes, or [`None`] if no limit. 45 | pub(crate) selection_size_limit_bytes: Option, 46 | /// If [`None`], the selection events should not be filtered by a [`Regex`]. 47 | /// Otherwise, all mime types have to match the regex for it to be not ignored. 48 | pub(crate) all_mime_type_regex: Option, 49 | /// The number of times a reconnect to the Wayland server should be tried after an error, 50 | /// or [`None`] if no limit. 51 | pub(crate) reconnect_tries: Option, 52 | /// The delay between two reconnect tries to the Wayland server. 53 | pub(crate) reconnect_delay: Duration, 54 | } 55 | 56 | /// Get the settings for the program. 57 | pub(crate) fn get_settings() -> Settings { 58 | let mut command = Command::new(crate_name!()).version(crate_version!()); 59 | let description = crate_description!(); 60 | 61 | if !description.is_empty() { 62 | command = command.about(description); 63 | } 64 | 65 | let matches = command 66 | .arg( 67 | arg!( 68 | -c --clipboard "The clipboard type to operate on" 69 | ) 70 | .required(true) 71 | .value_parser(value_parser!(ClipboardType)), 72 | ) 73 | .arg( 74 | arg!( 75 | -w --"write-timeout" "Timeout for trying to send the current clipboard to other programs" 76 | ) 77 | .required(false) 78 | .value_parser(clap::value_parser!(u64).range(1..=i32::MAX as u64)) 79 | .default_value("3000"), 80 | ) 81 | .arg( 82 | arg!( 83 | -e --"ignore-event-on-error" "Only handle selection events where no error occurred" 84 | ) 85 | .required(false) 86 | .action(ArgAction::SetTrue), 87 | ) 88 | .arg( 89 | arg!( 90 | -l --"selection-size-limit" "Only handle selection events whose total data size does not exceed the size limit" 91 | ) 92 | .required(false) 93 | .value_parser(clap::value_parser!(NonZeroU64)), 94 | ) 95 | .arg( 96 | arg!( 97 | -f --"all-mime-type-regex" "Only handle selection events where all offered MIME types have a match for the regex" 98 | ) 99 | .required(false) 100 | .value_parser(NonEmptyStringValueParser::new()), 101 | ) 102 | .arg( 103 | arg!( 104 | --"reconnect-tries" "Limit the number of tries to reconnect to the Wayland server after a Wayland error" 105 | ) 106 | .required(false) 107 | .value_parser(clap::value_parser!(u64)), 108 | ) 109 | .arg( 110 | arg!( 111 | --"reconnect-delay" "The delay between reconnect tries to the Wayland server" 112 | ) 113 | .required(false) 114 | .value_parser(clap::value_parser!(u64).range(0..=i32::MAX as u64)) 115 | .default_value("100"), 116 | ) 117 | .arg( 118 | arg!( 119 | --"disable-timestamps" "Do not show timestamps in the log messages" 120 | ) 121 | .required(false) 122 | .action(ArgAction::SetTrue), 123 | ) 124 | .get_matches(); 125 | 126 | // Initialize the logger here, because log is used to inform about invalid settings 127 | let disable_timestamps = matches.get_flag("disable-timestamps"); 128 | logger::init_logger(!disable_timestamps); 129 | 130 | let clipboard_type = *matches.get_one::("clipboard").unwrap(); 131 | let write_timeout = Duration::from_millis(*matches.get_one::("write-timeout").unwrap()); 132 | let ignore_selection_event_on_error = matches.get_flag("ignore-event-on-error"); 133 | let selection_size_limit_bytes = matches.get_one::("selection-size-limit").copied(); 134 | let all_mime_type_regex = matches 135 | .get_one::("all-mime-type-regex") 136 | .map(|s| match Regex::new(s) { 137 | Ok(regex) => regex, 138 | Err(err) => { 139 | log::error!( 140 | target: log_default_target(), 141 | "Failed to parse the mime type regex: {}", 142 | err 143 | ); 144 | std::process::exit(1); 145 | } 146 | }); 147 | let reconnect_tries = matches.get_one::("reconnect-tries").copied(); 148 | let reconnect_delay = Duration::from_millis(*matches.get_one::("reconnect-delay").unwrap()); 149 | 150 | Settings { 151 | clipboard_type, 152 | write_timeout, 153 | ignore_selection_event_on_error, 154 | selection_size_limit_bytes, 155 | all_mime_type_regex, 156 | reconnect_tries, 157 | reconnect_delay, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/states.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::ffi::CStr; 3 | use std::fs::File; 4 | use std::os::unix::fs::MetadataExt; 5 | use std::rc::Rc; 6 | 7 | use wayrs_client::global::{BindError, Global, GlobalExt as _}; 8 | use wayrs_client::object::ObjectId; 9 | use wayrs_client::protocol::WlSeat; 10 | use wayrs_client::proxy::Proxy as _; 11 | use wayrs_client::{ConnectError, Connection}; 12 | use wayrs_protocols::wlr_data_control_unstable_v1::{ 13 | ZwlrDataControlDeviceV1, ZwlrDataControlManagerV1, ZwlrDataControlOfferV1, 14 | }; 15 | 16 | use crate::logger; 17 | use crate::settings::Settings; 18 | use crate::wayland::data_control_device_cb; 19 | 20 | #[derive(Debug)] 21 | pub(crate) enum WaylandError { 22 | ConnectError(ConnectError), 23 | IoError(std::io::Error), 24 | } 25 | 26 | #[derive(Debug)] 27 | pub(crate) struct State { 28 | pub(crate) settings: Settings, 29 | pub(crate) data_control_manager: ZwlrDataControlManagerV1, 30 | pub(crate) seats: HashMap, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub(crate) struct Seat { 35 | pub(crate) seat_name: u32, 36 | pub(crate) wl_seat: WlSeat, 37 | pub(crate) data_control_device: ZwlrDataControlDeviceV1, 38 | /// We temporarily save the offers because it is unknown whether 39 | /// they are regular or primary selection offers. 40 | pub(crate) selection_offers: HashMap, 41 | /// The current regular selection state. Is [`None`], if not activated in settings. 42 | pub(crate) regular_selection: Option, 43 | /// The current primary selection state. Is [`None`], if not activated in settings. 44 | pub(crate) primary_selection: Option, 45 | /// Used to check if we got a new regular selection during the roundtrip. 46 | pub(crate) got_new_regular_selection: bool, 47 | /// Used to check if we got a new primary selection during the roundtrip. 48 | pub(crate) got_new_primary_selection: bool, 49 | } 50 | 51 | #[derive(Debug, Clone, Copy)] 52 | pub(crate) enum SelectionType { 53 | Regular, 54 | Primary, 55 | } 56 | 57 | impl SelectionType { 58 | pub(crate) const fn get_clipboard_type_str(&self, title_case: bool) -> &'static str { 59 | match self { 60 | SelectionType::Regular => logger::get_clipboard_type_str(false, title_case), 61 | SelectionType::Primary => logger::get_clipboard_type_str(true, title_case), 62 | } 63 | } 64 | } 65 | 66 | impl Seat { 67 | /// Binds a [`Global`] to create a new [`Seat`]. 68 | /// 69 | /// The global should be a seat. 70 | pub(crate) fn bind( 71 | connection: &mut Connection, 72 | data_control_manager: ZwlrDataControlManagerV1, 73 | global: &Global, 74 | settings: &Settings, 75 | ) -> Result { 76 | let seat = global.bind(connection, 1..=9)?; 77 | let seat_name = global.name; 78 | let data_control_device = 79 | data_control_manager.get_data_device_with_cb(connection, seat, move |event_context| { 80 | data_control_device_cb(seat_name, event_context) 81 | }); 82 | 83 | Ok(Self { 84 | seat_name: global.name, 85 | wl_seat: seat, 86 | data_control_device, 87 | selection_offers: HashMap::with_capacity(2), 88 | regular_selection: settings.clipboard_type.regular().then(SeatSelectionState::default), 89 | primary_selection: settings.clipboard_type.primary().then(SeatSelectionState::default), 90 | got_new_regular_selection: false, 91 | got_new_primary_selection: false, 92 | }) 93 | } 94 | 95 | /// Iterates over mutable regular and primary selection data. 96 | pub(crate) fn selections_iter_mut( 97 | &mut self, 98 | ) -> impl Iterator)> { 99 | std::iter::once((SelectionType::Regular, self.regular_selection.as_mut())).chain(std::iter::once(( 100 | SelectionType::Primary, 101 | self.primary_selection.as_mut(), 102 | ))) 103 | } 104 | 105 | /// Iterates over mutable regular and primary selection data, with the selection offers also being present. 106 | pub(crate) fn selections_iter_mut_with_selection_offers( 107 | &mut self, 108 | ) -> impl Iterator< 109 | Item = ( 110 | SelectionType, 111 | Option<&mut SeatSelectionState>, 112 | &HashMap, 113 | ), 114 | > { 115 | std::iter::once(( 116 | SelectionType::Regular, 117 | self.regular_selection.as_mut(), 118 | &self.selection_offers, 119 | )) 120 | .chain(std::iter::once(( 121 | SelectionType::Primary, 122 | self.primary_selection.as_mut(), 123 | &self.selection_offers, 124 | ))) 125 | } 126 | 127 | /// Destroys the seat. 128 | pub(crate) fn destroy(self, conn: &mut Connection) { 129 | for offer in self.selection_offers.into_values() { 130 | offer.data_control_offer.destroy(conn); 131 | } 132 | 133 | if let Some(mut regular_selection) = self.regular_selection { 134 | regular_selection.destroy(conn); 135 | } 136 | 137 | if let Some(mut primary_selection) = self.primary_selection { 138 | primary_selection.destroy(conn); 139 | } 140 | 141 | self.data_control_device.destroy(conn); 142 | 143 | if self.wl_seat.version() >= 5 { 144 | self.wl_seat.release(conn); 145 | } 146 | } 147 | } 148 | 149 | #[derive(Debug)] 150 | pub(crate) struct Offer { 151 | pub(crate) data_control_offer: ZwlrDataControlOfferV1, 152 | pub(crate) ordered_mime_types: Vec>>, 153 | pub(crate) unique_mime_types: HashSet>>, 154 | pub(crate) bytes_read: u64, 155 | pub(crate) bytes_exceeded_limit: bool, 156 | } 157 | 158 | impl From for Offer { 159 | fn from(data_control_offer: ZwlrDataControlOfferV1) -> Self { 160 | Self { 161 | data_control_offer, 162 | ordered_mime_types: Vec::with_capacity(32), 163 | unique_mime_types: HashSet::with_capacity(32), 164 | bytes_read: 0, 165 | bytes_exceeded_limit: false, 166 | } 167 | } 168 | } 169 | 170 | #[derive(Debug)] 171 | pub(crate) struct MimeTypeAndPipe { 172 | pub(crate) mime_type: Rc>, 173 | pub(crate) pipe: tokio_pipe::PipeRead, 174 | pub(crate) data_read: Option, ReadToDataError>>, 175 | pub(crate) read_finished: bool, 176 | } 177 | 178 | #[derive(Debug)] 179 | pub(crate) struct PipeDataResult<'a> { 180 | pub(crate) mime_type_and_pipe: &'a mut MimeTypeAndPipe, 181 | pub(crate) data_result: Result<(), ReadToDataError>, 182 | } 183 | 184 | #[derive(Debug)] 185 | pub(crate) enum ReadToDataError { 186 | IoError(std::io::Error), 187 | SizeLimitExceeded, 188 | } 189 | 190 | #[derive(Debug)] 191 | pub(crate) struct MimeTypesWithData<'a> { 192 | pub(crate) seat_name: u32, 193 | pub(crate) data_control_device: ZwlrDataControlDeviceV1, 194 | pub(crate) selection_offers: &'a HashMap, 195 | pub(crate) selection_state: &'a mut SeatSelectionState, 196 | pub(crate) selection_type: SelectionType, 197 | pub(crate) ordered_mime_types: Vec>>, 198 | pub(crate) data: HashMap>, Box<[u8]>>, 199 | } 200 | 201 | /// Describes the current state of handling the selection event. 202 | #[derive(Debug, Default)] 203 | pub(crate) enum SeatSelectionState { 204 | /// Waiting for new offers. 205 | #[default] 206 | WaitingForNewOffers, 207 | /// We read the mime types of the offer. 208 | ReadMimes { 209 | data_control_offer: ZwlrDataControlOfferV1, 210 | ordered_mime_types: Vec>>, 211 | unique_mime_types: HashSet>>, 212 | bytes_read: u64, 213 | fd_from_own_app: HashMap, 214 | }, 215 | /// We got the pipes for the current offer. 216 | /// The pipes are not from ourselves. 217 | GotPipes { 218 | ordered_mime_types: Vec>>, 219 | pipes: Vec, 220 | bytes_read: u64, 221 | }, 222 | /// We got the data, but cannot set the clipboard yet. 223 | /// We use this state, to minimize the risk of data races. 224 | /// The data race we avoid would be triggered by the following sequence of events: 225 | /// 226 | /// 1. Read pipes of new clipboard event 227 | /// 2. We receive a new offer, but do not know whether the offer will be a regular or primary selection event yet 228 | /// 3. While we wait for that info, the pipes have been fully read, and we set the data as the new clipboard 229 | /// => By doing that, we overwrite the new offer if it is from the same selection type 230 | /// => Therefore, if there is an offer whose selection type is unknown, we should wait before setting the clipboard 231 | GotData { 232 | ordered_mime_types: Vec>>, 233 | data: HashMap>, Box<[u8]>>, 234 | }, 235 | /// The selection was cleared. 236 | GotClear, 237 | /// The selection was updated, but we will not save the data in memory. 238 | GotIgnoredEvent, 239 | } 240 | 241 | impl SeatSelectionState { 242 | /// Switches to the initial state [`SeatSelectionState::WaitingForNewOffers`]. 243 | pub(crate) fn reset(&mut self, conn: &mut Connection) { 244 | // Destroy old wayland objects 245 | self.destroy(conn); 246 | 247 | *self = SeatSelectionState::WaitingForNewOffers; 248 | } 249 | 250 | /// Switches to the [`SeatSelectionState::ReadMimes`] state. 251 | pub(crate) fn read_mimes( 252 | &mut self, 253 | conn: &mut Connection, 254 | data_control_offer: ZwlrDataControlOfferV1, 255 | ordered_mime_types: Vec>>, 256 | unique_mime_types: HashSet>>, 257 | bytes_read: u64, 258 | ) { 259 | // Destroy old wayland objects 260 | self.destroy(conn); 261 | 262 | *self = SeatSelectionState::ReadMimes { 263 | data_control_offer, 264 | ordered_mime_types, 265 | unique_mime_types, 266 | bytes_read, 267 | fd_from_own_app: HashMap::with_capacity(32), 268 | }; 269 | } 270 | 271 | /// Switches to the [`SeatSelectionState::GotPipes`] state. 272 | pub(crate) fn got_pipes(&mut self, conn: &mut Connection, pipes: Vec) { 273 | let SeatSelectionState::ReadMimes { 274 | data_control_offer, 275 | ordered_mime_types, 276 | unique_mime_types: _, 277 | bytes_read, 278 | fd_from_own_app: _, 279 | } = self 280 | else { 281 | // The state already got updated to something newer, therefore return early 282 | return; 283 | }; 284 | 285 | // Destroy unneeded wayland object 286 | data_control_offer.destroy(conn); 287 | 288 | *self = SeatSelectionState::GotPipes { 289 | ordered_mime_types: std::mem::take(ordered_mime_types), 290 | pipes, 291 | bytes_read: *bytes_read, 292 | }; 293 | } 294 | 295 | /// Switches to the [`SeatSelectionState::GotData`] state. 296 | pub(crate) fn got_data(&mut self, ordered_mime_types: Vec>>, data: HashMap>, Box<[u8]>>) { 297 | let SeatSelectionState::GotPipes { 298 | ordered_mime_types: _, // This value will be empty because of std::mem::take 299 | pipes: _, 300 | bytes_read: _, 301 | } = self 302 | else { 303 | // The state already got updated to something newer, therefore return early 304 | return; 305 | }; 306 | 307 | *self = SeatSelectionState::GotData { 308 | ordered_mime_types, 309 | data, 310 | }; 311 | } 312 | 313 | /// Switches to the [`SeatSelectionState::GotClear`] state. 314 | pub(crate) fn got_clear(&mut self, conn: &mut Connection) { 315 | // Destroy old wayland objects 316 | self.destroy(conn); 317 | 318 | *self = SeatSelectionState::GotClear; 319 | } 320 | 321 | /// Switches to the [`SeatSelectionState::GotIgnoredEvent`] state. 322 | pub(crate) fn got_ignored_event(&mut self, conn: &mut Connection) { 323 | // Destroy old wayland objects 324 | self.destroy(conn); 325 | 326 | *self = SeatSelectionState::GotIgnoredEvent; 327 | } 328 | 329 | /// Destroys wayland objects in current state. 330 | pub(crate) fn destroy(&mut self, conn: &mut Connection) { 331 | match self { 332 | SeatSelectionState::WaitingForNewOffers => {} 333 | SeatSelectionState::ReadMimes { 334 | data_control_offer, 335 | ordered_mime_types: _, 336 | unique_mime_types: _, 337 | bytes_read: _, 338 | fd_from_own_app: _, 339 | } => { 340 | data_control_offer.destroy(conn); 341 | } 342 | SeatSelectionState::GotPipes { 343 | ordered_mime_types: _, 344 | pipes: _, 345 | bytes_read: _, 346 | } => {} 347 | SeatSelectionState::GotData { 348 | ordered_mime_types: _, 349 | data: _, 350 | } => {} 351 | SeatSelectionState::GotClear => {} 352 | SeatSelectionState::GotIgnoredEvent => {} 353 | }; 354 | } 355 | } 356 | 357 | /// A unique identifier for file descriptors. 358 | /// The device id and inode number should make a 359 | /// file descriptor uniquely identifiable. 360 | #[derive(Debug, PartialEq, Eq, Hash)] 361 | pub(crate) struct FdIdentifier { 362 | /// The device id of the file descriptor 363 | dev: u64, 364 | /// The inode number of the file descriptor 365 | ino: u64, 366 | } 367 | 368 | impl TryFrom<&File> for FdIdentifier { 369 | type Error = std::io::Error; 370 | 371 | fn try_from(file: &File) -> Result { 372 | let file_metadata = file.metadata()?; 373 | Ok(FdIdentifier { 374 | dev: file_metadata.dev(), 375 | ino: file_metadata.ino(), 376 | }) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/wayland.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::convert::Infallible; 4 | use std::ffi::CStr; 5 | use std::fs::File; 6 | use std::num::NonZeroU64; 7 | use std::ops::Deref; 8 | use std::os::fd::{IntoRawFd, OwnedFd}; 9 | use std::os::unix::io::FromRawFd; 10 | use std::rc::Rc; 11 | use std::sync::Arc; 12 | 13 | use futures_util::stream::FuturesUnordered; 14 | use futures_util::StreamExt as _; 15 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 16 | use wayrs_client::global::*; 17 | use wayrs_client::object::ObjectId; 18 | use wayrs_client::protocol::*; 19 | use wayrs_client::proxy::Proxy; 20 | use wayrs_client::{Connection, EventCtx}; 21 | use wayrs_protocols::wlr_data_control_unstable_v1::*; 22 | 23 | use crate::async_io::FdWrite; 24 | use crate::logger::{log_default_target, log_seat_target}; 25 | use crate::settings::Settings; 26 | use crate::states::*; 27 | 28 | /// Runs the wayland client until a wayland error occurs. 29 | pub(crate) async fn run(settings: Settings, is_reconnect: bool) -> Result { 30 | let (mut connection, globals) = Connection::async_connect_and_collect_globals() 31 | .await 32 | .map_err(WaylandError::ConnectError)?; 33 | connection.add_registry_cb(wl_registry_cb); 34 | 35 | if is_reconnect { 36 | log::info!(target: log_default_target(), "Connection to wayland server re-established"); 37 | } else { 38 | log::trace!(target: log_default_target(), "Connection to wayland server established"); 39 | } 40 | 41 | let data_control_manager_result = if settings.clipboard_type.primary() { 42 | globals.bind(&mut connection, 2) 43 | } else { 44 | globals.bind(&mut connection, 1..=2) 45 | }; 46 | let data_control_manager = match data_control_manager_result { 47 | Ok(data_control_manager) => data_control_manager, 48 | Err(err) => { 49 | let mut default = "Failed to get clipboard manager (ZwlrDataControlManagerV1)".to_string(); 50 | 51 | if settings.clipboard_type.primary() && matches!(err, BindError::UnsupportedVersion { actual: 1, min: _ }) { 52 | default += "\nPerhaps the primary clipboard is not supported by your compositor?"; 53 | } 54 | 55 | log::error!(target: log_default_target(), "{}\nError: {}", default, err); 56 | std::process::exit(1); 57 | } 58 | }; 59 | 60 | let seats = globals 61 | .iter() 62 | .filter(|global| global.is::()) 63 | .filter_map(|seat_global| { 64 | Seat::bind(&mut connection, data_control_manager, seat_global, &settings) 65 | .inspect(|_| { 66 | log::trace!( 67 | target: &log_seat_target(seat_global.name), 68 | "Added seat" 69 | ); 70 | }) 71 | .inspect_err(|err| { 72 | log::debug!( 73 | target: &log_seat_target(seat_global.name), 74 | "Failed to bind seat: {}", 75 | err, 76 | ); 77 | }) 78 | .ok() 79 | .map(|seat| (seat_global.name, seat)) 80 | }) 81 | .collect::>(); 82 | 83 | if seats.is_empty() { 84 | log::warn!(target: log_default_target(), "No seats found on startup"); 85 | } 86 | 87 | let mut state = State { 88 | settings, 89 | data_control_manager, 90 | seats, 91 | }; 92 | 93 | // Advertise the bindings to the wayland server 94 | connection.async_flush().await.map_err(WaylandError::IoError)?; 95 | 96 | loop { 97 | let received_wayland_events = 'wait: loop { 98 | // We use FuturesUnordered for this because we want to flush the 99 | // new clipboard source after each finished read. 100 | let mut set_clipboard_futures = FuturesUnordered::new(); 101 | for seat in state.seats.values_mut() { 102 | let seat_name = seat.seat_name; 103 | let data_control_device = seat.data_control_device; 104 | 105 | for (selection_type, selection_state, selection_offers) in 106 | seat.selections_iter_mut_with_selection_offers() 107 | { 108 | let Some(selection_state) = selection_state else { 109 | continue; 110 | }; 111 | 112 | let SeatSelectionState::GotPipes { 113 | ordered_mime_types: _, 114 | pipes: _, 115 | bytes_read: _, 116 | } = selection_state 117 | else { 118 | continue; 119 | }; 120 | 121 | set_clipboard_futures.push(handle_pipes_selection_state( 122 | seat_name, 123 | data_control_device, 124 | selection_offers, 125 | selection_type, 126 | selection_state, 127 | state.settings.selection_size_limit_bytes, 128 | state.settings.ignore_selection_event_on_error, 129 | )); 130 | } 131 | } 132 | 133 | tokio::select! { 134 | biased; 135 | 136 | recv_events = connection.async_recv_events() => { 137 | recv_events.map_err(WaylandError::IoError)?; 138 | drop(set_clipboard_futures); 139 | break 'wait true; 140 | } 141 | mime_types_with_data = set_clipboard_futures.next(), if !set_clipboard_futures.is_empty() => { 142 | drop(set_clipboard_futures); 143 | 144 | match mime_types_with_data { 145 | Some(Ok(mime_types_with_data)) => { 146 | if mime_types_with_data.selection_offers.is_empty() { 147 | set_clipboard( 148 | &mut connection, 149 | data_control_manager, 150 | mime_types_with_data.seat_name, 151 | mime_types_with_data.data_control_device, 152 | mime_types_with_data.selection_state, 153 | mime_types_with_data.selection_type, 154 | mime_types_with_data.ordered_mime_types, 155 | mime_types_with_data.data, 156 | ); 157 | break 'wait false; 158 | } else { 159 | // Set clipboard later to avoid data race 160 | mime_types_with_data.selection_state.got_data( 161 | mime_types_with_data.ordered_mime_types, 162 | mime_types_with_data.data, 163 | ); 164 | } 165 | } 166 | Some(Err(selection_state)) => { 167 | // Selection event got ignored 168 | selection_state.reset(&mut connection); 169 | } 170 | None => unreachable!(), 171 | } 172 | } 173 | } 174 | }; 175 | 176 | if received_wayland_events { 177 | connection.dispatch_events(&mut state); 178 | handle_new_selection_state(&mut connection, &mut state) 179 | .await 180 | .map_err(WaylandError::IoError)?; 181 | 182 | // Now that we received and dispatched new wayland events, check if we can set the clipboard now. 183 | // Also notice, we do this before the async flush. 184 | for seat in state.seats.values_mut() { 185 | if !seat.selection_offers.is_empty() { 186 | // There are still pending offers, therefore avoid... 187 | continue; 188 | } 189 | 190 | let seat_name = seat.seat_name; 191 | let data_control_device = seat.data_control_device; 192 | 193 | for (selection_type, selection_state) in seat.selections_iter_mut() { 194 | let Some(selection_state) = selection_state else { 195 | continue; 196 | }; 197 | 198 | let SeatSelectionState::GotData { 199 | ordered_mime_types, 200 | data, 201 | } = selection_state 202 | else { 203 | continue; 204 | }; 205 | 206 | let owned_ordered_mime_types = std::mem::take(ordered_mime_types); 207 | let owned_data = std::mem::take(data); 208 | 209 | set_clipboard( 210 | &mut connection, 211 | data_control_manager, 212 | seat_name, 213 | data_control_device, 214 | selection_state, 215 | selection_type, 216 | owned_ordered_mime_types, 217 | owned_data, 218 | ); 219 | } 220 | } 221 | } 222 | 223 | connection.async_flush().await.map_err(WaylandError::IoError)?; 224 | } 225 | } 226 | 227 | /// Handles the registration of globals, in this case seats. 228 | fn wl_registry_cb(connection: &mut Connection, state: &mut State, event: &wl_registry::Event) { 229 | match event { 230 | wl_registry::Event::Global(global) if global.is::() => { 231 | match Seat::bind(connection, state.data_control_manager, global, &state.settings) { 232 | Ok(seat) => { 233 | if let Some(old_seat) = state.seats.insert(global.name, seat) { 234 | old_seat.destroy(connection); 235 | log::error!( 236 | target: &log_seat_target(global.name), 237 | "Added seat, even though seat with same id was already present" 238 | ); 239 | } else { 240 | log::trace!( 241 | target: &log_seat_target(global.name), 242 | "Added seat" 243 | ); 244 | } 245 | } 246 | Err(err) => { 247 | log::debug!( 248 | target: &log_seat_target(global.name), 249 | "Failed to bind seat: {}", 250 | err, 251 | ); 252 | } 253 | } 254 | } 255 | wl_registry::Event::Global(_) => { 256 | // Ignore all other globals that are advertised, like WlOutput 257 | } 258 | wl_registry::Event::GlobalRemove(name) => { 259 | if let Some(seat) = state.seats.remove(name) { 260 | seat.destroy(connection); 261 | log::trace!(target: &log_seat_target(*name), "Removed seat"); 262 | } 263 | } 264 | fallback => { 265 | log::debug!(target: log_default_target(), "wl_registry::Event: unhandled event: {:?}", fallback); 266 | } 267 | } 268 | } 269 | 270 | /// Handles the selection events of a seat. 271 | pub(crate) fn data_control_device_cb(seat_name: u32, event_context: EventCtx) { 272 | let maybe_seat = event_context.state.seats.get_mut(&seat_name); 273 | 274 | let Some(seat) = maybe_seat else { 275 | log::warn!( 276 | target: log_default_target(), 277 | "Received ZwlrDataControlDeviceV1 event for unknown seat {}: {:?}", 278 | seat_name, 279 | event_context.event, 280 | ); 281 | return; 282 | }; 283 | 284 | match event_context.event { 285 | zwlr_data_control_device_v1::Event::DataOffer(data_offer) => { 286 | let offer = Offer::from(data_offer); 287 | 288 | if let Some(old_offer) = seat.selection_offers.insert(data_offer.id(), offer) { 289 | old_offer.data_control_offer.destroy(event_context.conn); 290 | log::error!( 291 | target: &log_seat_target(seat_name), 292 | "New selection event: new offer {} got introduced, even though offer with same id was already present", 293 | data_offer.id().as_u32(), 294 | ); 295 | } else { 296 | log::trace!( 297 | target: &log_seat_target(seat_name), 298 | "New selection event: new offer {} got introduced", 299 | data_offer.id().as_u32(), 300 | ); 301 | } 302 | 303 | event_context 304 | .conn 305 | .set_callback_for(data_offer, move |offer_event_context| { 306 | data_control_offer_cb(seat_name, data_offer.id(), offer_event_context); 307 | }); 308 | } 309 | zwlr_data_control_device_v1::Event::Selection(maybe_offer_id) => { 310 | let maybe_offer = maybe_offer_id.and_then(|offer_id| seat.selection_offers.remove(&offer_id)); 311 | 312 | if let Some(offer_id) = &maybe_offer_id { 313 | if maybe_offer.is_some() { 314 | log::trace!( 315 | target: &log_seat_target(seat_name), 316 | "New selection event: offer {} has been advertised as regular selection", 317 | offer_id.as_u32(), 318 | ); 319 | } else { 320 | log::warn!( 321 | target: &log_seat_target(seat_name), 322 | "New selection event: unknown offer {} has been advertised as regular selection", 323 | offer_id.as_u32(), 324 | ); 325 | } 326 | } 327 | 328 | if let Some(regular_selection) = &mut seat.regular_selection { 329 | // Only set this flag to true if the selection type is activated, 330 | // to avoid unnecessary loops for handling the new state after the roundtrip 331 | seat.got_new_regular_selection = true; 332 | 333 | if maybe_offer_id.is_none() { 334 | log::trace!( 335 | target: &log_seat_target(seat_name), 336 | "New selection event: regular selection got cleared", 337 | ); 338 | } 339 | 340 | if let Some(offer) = maybe_offer { 341 | if should_ignore_offer(&event_context.state.settings, seat_name, SelectionType::Regular, &offer) { 342 | regular_selection.got_ignored_event(event_context.conn); 343 | offer.data_control_offer.destroy(event_context.conn); 344 | return; 345 | } 346 | 347 | regular_selection.read_mimes( 348 | event_context.conn, 349 | offer.data_control_offer, 350 | offer.ordered_mime_types, 351 | offer.unique_mime_types, 352 | offer.bytes_read, 353 | ); 354 | } else { 355 | regular_selection.got_clear(event_context.conn); 356 | } 357 | } else { 358 | // We are not interested in the regular clipboard, so just destroy that offer, if any, and return. 359 | if let Some(offer) = maybe_offer { 360 | offer.data_control_offer.destroy(event_context.conn); 361 | } 362 | } 363 | } 364 | zwlr_data_control_device_v1::Event::PrimarySelection(maybe_offer_id) => { 365 | let maybe_offer = maybe_offer_id.and_then(|offer_id| seat.selection_offers.remove(&offer_id)); 366 | 367 | if let Some(offer_id) = &maybe_offer_id { 368 | if maybe_offer.is_some() { 369 | log::trace!( 370 | target: &log_seat_target(seat_name), 371 | "New selection event: offer {} has been advertised as primary selection", 372 | offer_id.as_u32(), 373 | ); 374 | } else { 375 | log::warn!( 376 | target: &log_seat_target(seat_name), 377 | "New selection event: unknown offer {} has been advertised as primary selection", 378 | offer_id.as_u32(), 379 | ); 380 | } 381 | } 382 | 383 | if let Some(primary_selection) = &mut seat.primary_selection { 384 | // Only set this flag to true if the selection type is activated, 385 | // to avoid unnecessary loops for handling the new state after the roundtrip 386 | seat.got_new_primary_selection = true; 387 | 388 | if maybe_offer_id.is_none() { 389 | log::trace!( 390 | target: &log_seat_target(seat_name), 391 | "New selection event: primary selection got cleared", 392 | ); 393 | } 394 | 395 | if let Some(offer) = maybe_offer { 396 | if should_ignore_offer(&event_context.state.settings, seat_name, SelectionType::Primary, &offer) { 397 | primary_selection.got_ignored_event(event_context.conn); 398 | offer.data_control_offer.destroy(event_context.conn); 399 | return; 400 | } 401 | 402 | primary_selection.read_mimes( 403 | event_context.conn, 404 | offer.data_control_offer, 405 | offer.ordered_mime_types, 406 | offer.unique_mime_types, 407 | offer.bytes_read, 408 | ); 409 | } else { 410 | primary_selection.got_clear(event_context.conn); 411 | } 412 | } else { 413 | // We are not interested in the primary clipboard, so just destroy that offer, if any, and return. 414 | if let Some(offer) = maybe_offer { 415 | offer.data_control_offer.destroy(event_context.conn); 416 | } 417 | } 418 | } 419 | zwlr_data_control_device_v1::Event::Finished => { 420 | if let Some(seat) = event_context.state.seats.remove(&seat_name) { 421 | seat.destroy(event_context.conn); 422 | log::trace!( 423 | target: &log_seat_target(seat_name), 424 | "Removed seat: due to data control being no longer valid" 425 | ); 426 | } else { 427 | log::trace!( 428 | target: &log_seat_target(seat_name), 429 | "Data control is no longer valid" 430 | ); 431 | } 432 | } 433 | fallback => { 434 | log::debug!( 435 | target: &log_seat_target(seat_name), 436 | "zwlr_data_control_device_v1::Event: unhandled event: {:?}", 437 | fallback, 438 | ); 439 | } 440 | } 441 | } 442 | 443 | /// Handles the events for a selection offer, i.e. which mime types are offered. 444 | fn data_control_offer_cb( 445 | seat_name: u32, 446 | data_offer_id: ObjectId, 447 | event_context: EventCtx, 448 | ) { 449 | match event_context.event { 450 | zwlr_data_control_offer_v1::Event::Offer(mime_type) => { 451 | let Some(seat) = event_context.state.seats.get_mut(&seat_name) else { 452 | log::warn!( 453 | target: log_default_target(), 454 | "New advertised mime type: got event for unknown seat {}", 455 | seat_name 456 | ); 457 | return; 458 | }; 459 | 460 | let Some(offer) = seat.selection_offers.get_mut(&data_offer_id) else { 461 | log::warn!( 462 | target: &log_seat_target(seat_name), 463 | "New advertised mime type: got event for unknown offer {}", 464 | data_offer_id.as_u32() 465 | ); 466 | return; 467 | }; 468 | 469 | log::trace!( 470 | target: &log_seat_target(seat_name), 471 | "New advertised mime type: offer {} has mime type: {:?}", 472 | data_offer_id.as_u32(), 473 | mime_type, 474 | ); 475 | 476 | if offer.bytes_exceeded_limit { 477 | return; 478 | } 479 | 480 | let mime_type_bytes = mime_type.as_bytes().len(); 481 | let boxed_mime_type = mime_type.into_boxed_c_str(); 482 | let rc_mime_type = if let Some(rc_mime_type) = offer.unique_mime_types.get(&boxed_mime_type) { 483 | Rc::clone(rc_mime_type) 484 | } else { 485 | offer.bytes_read += mime_type_bytes as u64; 486 | 487 | if let Some(selection_size_limit_bytes) = event_context.state.settings.selection_size_limit_bytes { 488 | if offer.bytes_read > selection_size_limit_bytes.get() { 489 | log::trace!( 490 | target: &log_seat_target(seat_name), 491 | "New advertised mime type: exceeded specified selection size limit", 492 | ); 493 | offer.bytes_exceeded_limit = true; 494 | return; 495 | } 496 | } 497 | 498 | let rc_mime_type = Rc::new(boxed_mime_type); 499 | offer.unique_mime_types.insert(Rc::clone(&rc_mime_type)); 500 | rc_mime_type 501 | }; 502 | 503 | offer.ordered_mime_types.push(rc_mime_type); 504 | } 505 | fallback => { 506 | log::debug!( 507 | target: &log_seat_target(seat_name), 508 | "zwlr_data_control_offer_v1::Event: unhandled event: {:?}", 509 | fallback, 510 | ); 511 | } 512 | } 513 | } 514 | 515 | /// Whether the new offer should be ignored, based on mime types. 516 | /// 517 | /// The offer is ignored if one of these cases is true: 518 | /// * mime types exceed size limit 519 | /// * no mime types were offered 520 | /// * not all mime types match the regex 521 | fn should_ignore_offer(settings: &Settings, seat_name: u32, selection_type: SelectionType, offer: &Offer) -> bool { 522 | if offer.bytes_exceeded_limit { 523 | log::trace!( 524 | target: &log_seat_target(seat_name), 525 | "Ignoring {} selection event: mime types exceeded specified selection size limit", 526 | selection_type.get_clipboard_type_str(false), 527 | ); 528 | return true; 529 | } 530 | 531 | if offer.unique_mime_types.is_empty() { 532 | log::trace!( 533 | target: &log_seat_target(seat_name), 534 | "Ignoring {} selection event: no mime types were offered", 535 | selection_type.get_clipboard_type_str(false), 536 | ); 537 | return true; 538 | } 539 | 540 | // Log all available mime types. 541 | for mime_type in &offer.ordered_mime_types { 542 | log::trace!( 543 | target: &log_seat_target(seat_name), 544 | "Current {} selection event: offered mime types: {:?}", 545 | selection_type.get_clipboard_type_str(false), 546 | mime_type, 547 | ); 548 | } 549 | 550 | if let Some(regex) = settings.all_mime_type_regex.as_ref() { 551 | // Only keep this offer, if all mime types have a match for this regex. 552 | let match_all_regex = offer.unique_mime_types.iter().all(|mime_type| { 553 | // TODO: Upstream issue: https://github.com/fancy-regex/fancy-regex/issues/84 554 | match mime_type.to_str() { 555 | Ok(mime_type_as_str) => { 556 | match regex.is_match(mime_type_as_str) { 557 | Ok(has_match) => { 558 | if !has_match { 559 | log::trace!( 560 | target: &log_seat_target(seat_name), 561 | "Ignoring {} selection event: mime type does not match the regex: {:?}", 562 | selection_type.get_clipboard_type_str(false), 563 | mime_type, 564 | ); 565 | } 566 | 567 | has_match 568 | } 569 | Err(err) => { 570 | log::debug!( 571 | target: &log_seat_target(seat_name), 572 | "Current {} selection event: regex returned an error for mime type {:?}: {}", 573 | selection_type.get_clipboard_type_str(false), 574 | mime_type, 575 | err, 576 | ); 577 | 578 | // Just assume that the mime type has a match. 579 | true 580 | } 581 | } 582 | } 583 | Err(err) => { 584 | log::debug!( 585 | target: &log_seat_target(seat_name), 586 | "Current {} selection event: mime type {:?} contains invalid UTF-8: {}", 587 | selection_type.get_clipboard_type_str(false), 588 | mime_type, 589 | err, 590 | ); 591 | 592 | // Just assume that the mime type has no match. 593 | false 594 | } 595 | } 596 | }); 597 | 598 | if !match_all_regex { 599 | return true; 600 | } 601 | } 602 | 603 | false 604 | } 605 | 606 | /// Creates pipes for each mime type. 607 | /// 608 | /// If there are no mime types to return (i.e. the 609 | /// [`Vec`] is empty), `Ok(None)` is returned. 610 | /// 611 | /// # Errors 612 | /// 613 | /// If `ignore_selection_event_on_error` is `true` 614 | /// and an error occurred while creating the pipe 615 | /// or reading the metadata of the pipe, the error 616 | /// is returned immediately in the [`Err`] variant. 617 | /// 618 | /// If `ignore_selection_event_on_error` is `false` 619 | /// and an error occurred the mime type is simply 620 | /// ignored and will not appear in the return value. 621 | /// 622 | /// # Side effects 623 | /// 624 | /// The `unique_mime_types` value is always replaced with 625 | /// an empty [`Vec`]. 626 | fn create_pipes_for_mime_types( 627 | connection: &mut Connection, 628 | seat_name: u32, 629 | selection_type: SelectionType, 630 | data_control_offer: ZwlrDataControlOfferV1, 631 | unique_mime_types: &mut HashSet>>, 632 | fd_from_own_app: &mut HashMap, 633 | ignore_selection_event_on_error: bool, 634 | ) -> std::io::Result>> { 635 | let mut mime_types_and_pipes = Vec::with_capacity(unique_mime_types.len()); 636 | 637 | for mime_type in std::mem::take(unique_mime_types) { 638 | // Create a pipe to read the data for each mime type 639 | let (read, write) = match tokio_pipe::pipe() { 640 | Ok(pipe_ends) => pipe_ends, 641 | Err(err) => { 642 | if ignore_selection_event_on_error { 643 | log::debug!( 644 | target: &log_seat_target(seat_name), 645 | "Ignoring {} selection event: failed to create pipe: {}", 646 | selection_type.get_clipboard_type_str(false), 647 | err 648 | ); 649 | return Err(err); 650 | } else { 651 | log::debug!( 652 | target: &log_seat_target(seat_name), 653 | "Current {} selection event: ignoring mime type {:?}: failed to create pipe: {}", 654 | selection_type.get_clipboard_type_str(false), 655 | mime_type, 656 | err 657 | ); 658 | continue; 659 | } 660 | } 661 | }; 662 | 663 | // Save file descriptor identifier of the writable end of the pipe, 664 | // so we can check if we are writing to our own pipe. 665 | let write_file = unsafe { std::fs::File::from_raw_fd(write.into_raw_fd()) }; 666 | let fd_identifier = match FdIdentifier::try_from(&write_file) { 667 | Ok(fd_identifier) => fd_identifier, 668 | Err(err) => { 669 | if ignore_selection_event_on_error { 670 | log::debug!( 671 | target: &log_seat_target(seat_name), 672 | "Ignoring {} selection event: failed to get metadata for pipe: {}", 673 | selection_type.get_clipboard_type_str(false), 674 | err 675 | ); 676 | return Err(err); 677 | } else { 678 | log::debug!( 679 | target: &log_seat_target(seat_name), 680 | "Current {} selection event: ignoring mime type {:?}: failed to get metadata for pipe: {}", 681 | selection_type.get_clipboard_type_str(false), 682 | mime_type, 683 | err 684 | ); 685 | continue; 686 | } 687 | } 688 | }; 689 | fd_from_own_app.insert(fd_identifier, false); 690 | 691 | // We want to receive the data for this mime type 692 | data_control_offer.receive(connection, mime_type.deref().clone().into_c_string(), write_file.into()); 693 | 694 | mime_types_and_pipes.push(MimeTypeAndPipe { 695 | mime_type, 696 | pipe: read, 697 | data_read: None, 698 | read_finished: false, 699 | }); 700 | } 701 | 702 | if mime_types_and_pipes.is_empty() { 703 | log::trace!( 704 | target: &log_seat_target(seat_name), 705 | "Ignoring {} selection event: all pipe preparations resulted in an error", 706 | selection_type.get_clipboard_type_str(false) 707 | ); 708 | return Ok(None); 709 | } 710 | 711 | Ok(Some(mime_types_and_pipes)) 712 | } 713 | 714 | /// Handles the new selection state after receiving offers. 715 | /// 716 | /// Specifically, it handles the cases [`SeatSelectionState::ReadMimes`], 717 | /// [`SeatSelectionState::GotClear`] and [`SeatSelectionState::GotIgnoredEvent`]. 718 | /// 719 | /// # Errors 720 | /// 721 | /// Only Wayland errors are returned. 722 | async fn handle_new_selection_state(connection: &mut Connection, state: &mut State) -> std::io::Result<()> { 723 | 'handle_new_selection_state: loop { 724 | let selection_pipes = state 725 | .seats 726 | .iter_mut() 727 | .flat_map(|(seat_name, seat)| std::iter::repeat(seat_name).zip(seat.selections_iter_mut())) 728 | .filter_map(|(seat_name, (selection_type, selection_state))| { 729 | let selection_state = selection_state?; 730 | 731 | match selection_state { 732 | SeatSelectionState::WaitingForNewOffers => None, 733 | SeatSelectionState::ReadMimes { 734 | data_control_offer, 735 | ordered_mime_types: _, 736 | unique_mime_types, 737 | bytes_read: _, 738 | fd_from_own_app, 739 | } => { 740 | let mime_types_with_pipes_result = create_pipes_for_mime_types( 741 | connection, 742 | *seat_name, 743 | selection_type, 744 | *data_control_offer, 745 | unique_mime_types, 746 | fd_from_own_app, 747 | state.settings.ignore_selection_event_on_error, 748 | ); 749 | let mime_types_with_pipes = match mime_types_with_pipes_result { 750 | Ok(Some(mime_types_with_pipes)) => mime_types_with_pipes, 751 | Ok(None) => { 752 | // No pipes, ignore 753 | selection_state.reset(connection); 754 | return None; 755 | } 756 | Err(_) => { 757 | // Got error, ignore 758 | selection_state.reset(connection); 759 | return None; 760 | } 761 | }; 762 | Some((*seat_name, selection_type, mime_types_with_pipes)) 763 | } 764 | SeatSelectionState::GotPipes { 765 | ordered_mime_types: _, 766 | pipes: _, 767 | bytes_read: _, 768 | } => None, 769 | SeatSelectionState::GotData { 770 | ordered_mime_types: _, 771 | data: _, 772 | } => None, 773 | SeatSelectionState::GotClear => { 774 | selection_state.reset(connection); 775 | None 776 | } 777 | SeatSelectionState::GotIgnoredEvent => { 778 | selection_state.reset(connection); 779 | None 780 | } 781 | } 782 | }) 783 | .collect::>(); 784 | 785 | if selection_pipes.is_empty() { 786 | // There is nothing to do, therefore early return to avoid a roundtrip 787 | return Ok(()); 788 | } 789 | 790 | // Set flags to false so we can check later if they got true 791 | for seat in state.seats.values_mut() { 792 | seat.got_new_regular_selection = false; 793 | seat.got_new_primary_selection = false; 794 | } 795 | 796 | // We need a roundtrip to know if these pipes are from ourselves 797 | connection.async_roundtrip().await?; 798 | connection.dispatch_events(state); 799 | 800 | // We have to check all seats, because `selection_pipes` might not cover every seat. 801 | // If there is a new offer in an unchecked seat, we should handle it in the next loop! 802 | let handle_new_selection_state_again = state 803 | .seats 804 | .values() 805 | .any(|seat| seat.got_new_regular_selection || seat.got_new_primary_selection); 806 | 807 | for (seat_name, selection_type, mime_types_with_pipes) in selection_pipes { 808 | let Some(seat) = state.seats.get_mut(&seat_name) else { 809 | // Seems like the seat got removed during the roundtrip, ignore 810 | continue; 811 | }; 812 | 813 | let got_new_selection = match selection_type { 814 | SelectionType::Regular => seat.got_new_regular_selection, 815 | SelectionType::Primary => seat.got_new_primary_selection, 816 | }; 817 | 818 | if got_new_selection { 819 | // We have got a new selection offer, the temporary pipes are outdated 820 | continue; 821 | } 822 | 823 | let maybe_selection_state = match selection_type { 824 | SelectionType::Regular => seat.regular_selection.as_mut(), 825 | SelectionType::Primary => seat.primary_selection.as_mut(), 826 | }; 827 | 828 | let Some(selection_state) = maybe_selection_state else { 829 | unreachable!(); 830 | }; 831 | 832 | let SeatSelectionState::ReadMimes { 833 | data_control_offer: _, 834 | ordered_mime_types: _, 835 | unique_mime_types: _, 836 | bytes_read: _, 837 | fd_from_own_app, 838 | } = selection_state 839 | else { 840 | // This case cannot be triggered. If `got_new_selection` is false, 841 | // the state will not have changed and we will still be in the state 842 | // ReadMimes. If `got_new_selection` is true, then we would have 843 | // already `continue`-d. 844 | unreachable!(); 845 | }; 846 | 847 | if fd_from_own_app.values().next() == Some(&true) { 848 | log::trace!( 849 | target: &log_seat_target(seat_name), 850 | "Ignoring {} selection event: was triggered by ourselves", 851 | selection_type.get_clipboard_type_str(false), 852 | ); 853 | 854 | // Reset connection state 855 | selection_state.reset(connection); 856 | continue; 857 | } 858 | 859 | selection_state.got_pipes(connection, mime_types_with_pipes); 860 | } 861 | 862 | if !handle_new_selection_state_again { 863 | break 'handle_new_selection_state; 864 | } 865 | } 866 | 867 | Ok(()) 868 | } 869 | 870 | /// Reads the data from a single pipe. 871 | /// 872 | /// On success, [`PipeDataResult::data_result`] will be 873 | /// [`Ok`]. 874 | /// 875 | /// # Errors 876 | /// 877 | /// If an error while reading the pipe occurred, 878 | /// or the size limit has been exceeded, the error 879 | /// is returned in [`PipeDataResult::data_result`]. 880 | /// 881 | /// # Panics 882 | /// 883 | /// This function panics if the pipe has already been completely 884 | /// read or an error while reading it occurred before. 885 | async fn read_pipe_to_data<'a>( 886 | mime_type_and_pipe: &'a mut MimeTypeAndPipe, 887 | bytes_read: Rc>, 888 | size_limit: Option, 889 | ) -> PipeDataResult<'a> { 890 | if mime_type_and_pipe.data_read.is_none() { 891 | mime_type_and_pipe.data_read = Some(Ok(Vec::with_capacity(32))); 892 | } else if mime_type_and_pipe.read_finished || matches!(mime_type_and_pipe.data_read, Some(Err(_))) { 893 | unreachable!(); 894 | }; 895 | 896 | let data = mime_type_and_pipe.data_read.as_mut().unwrap().as_mut().unwrap(); 897 | let mut buf = [0u8; 8192]; 898 | 899 | loop { 900 | match mime_type_and_pipe.pipe.read(&mut buf).await { 901 | Ok(0) => break, 902 | Ok(size) => { 903 | // Check size limit first 904 | **bytes_read.borrow_mut() += size as u64; 905 | 906 | if let Some(size_limit) = size_limit { 907 | if **bytes_read.borrow() > size_limit.get() { 908 | mime_type_and_pipe.read_finished = true; 909 | 910 | return PipeDataResult { 911 | mime_type_and_pipe, 912 | data_result: Err(ReadToDataError::SizeLimitExceeded), 913 | }; 914 | } 915 | } 916 | 917 | // Add data 918 | data.extend_from_slice(&buf[..size]); 919 | } 920 | Err(err) => { 921 | // Size limit: subtract this size from the current total size 922 | **bytes_read.borrow_mut() -= data.len() as u64; 923 | 924 | mime_type_and_pipe.read_finished = true; 925 | 926 | return PipeDataResult { 927 | mime_type_and_pipe, 928 | data_result: Err(ReadToDataError::IoError(err)), 929 | }; 930 | } 931 | } 932 | } 933 | 934 | mime_type_and_pipe.read_finished = true; 935 | 936 | PipeDataResult { 937 | mime_type_and_pipe, 938 | data_result: Ok(()), 939 | } 940 | } 941 | 942 | /// Reads the pipes of all mime types. 943 | /// 944 | /// If all pipes have been read already, and this function is called, 945 | /// [`Ok`] is returned. 946 | /// 947 | /// # Errors 948 | /// 949 | /// If the size limit is exceeded while reading a pipe, 950 | /// the [`ReadToDataError::SizeLimitExceeded`] is immediately 951 | /// returned in the [`Err`] variant, regardless of the value 952 | /// of `ignore_selection_event_on_error`. 953 | /// 954 | /// If an error occurred while reading a pipe, and the value of 955 | /// `ignore_selection_event_on_error` is true, this function 956 | /// immediately returns the error in the [`Err`] variant. 957 | /// 958 | /// Otherwise, if an error occured while reading a pipe, and 959 | /// the value of `ignore_selection_event_on_error` is false, 960 | /// the error is saved in the [`MimeTypeAndPipe::data_read`] field. 961 | async fn read_pipes_to_data( 962 | pipes: &mut [MimeTypeAndPipe], 963 | bytes_read: &mut u64, 964 | size_limit: Option, 965 | ignore_selection_event_on_error: bool, 966 | ) -> Result<(), ReadToDataError> { 967 | let mut futures = FuturesUnordered::new(); 968 | let shared_byted_read = Rc::new(RefCell::new(bytes_read)); 969 | 970 | for mime_type_and_pipe in pipes { 971 | if mime_type_and_pipe.read_finished { 972 | continue; 973 | } 974 | 975 | futures.push(read_pipe_to_data( 976 | mime_type_and_pipe, 977 | Rc::clone(&shared_byted_read), 978 | size_limit, 979 | )); 980 | } 981 | 982 | while let Some(pipe_data_result) = futures.next().await { 983 | match pipe_data_result.data_result { 984 | Ok(_) => {} 985 | Err(err) => { 986 | if ignore_selection_event_on_error || matches!(err, ReadToDataError::SizeLimitExceeded) { 987 | return Err(err); 988 | } else { 989 | pipe_data_result.mime_type_and_pipe.data_read = Some(Err(err)); 990 | } 991 | } 992 | } 993 | } 994 | 995 | Ok(()) 996 | } 997 | 998 | /// Reads the pipes of all mime types until all are read completely. 999 | /// 1000 | /// If everything went well, the mime types are returned with the 1001 | /// corresponding data as a [`HashMap`] in the [`Ok`] variant. 1002 | /// The mime types where an error was returned while reading the pipe 1003 | /// are not included in the [`HashMap`]. 1004 | /// 1005 | /// # Errors 1006 | /// 1007 | /// If an error occurred while reading one of the pipes, 1008 | /// and `ignore_selection_event_on_error` is true, 1009 | /// we return an [`Err`]. 1010 | /// 1011 | /// If all pipes returned an error while reading, 1012 | /// we will return an [`Err`] regardless of the 1013 | /// value of `ignore_selection_event_on_error`. 1014 | /// 1015 | /// Possible errors while reading the pipe include exceeding 1016 | /// the given `size_limit`. 1017 | /// 1018 | /// # Panics 1019 | /// 1020 | /// This function panics if the given [SeatSelectionState] 1021 | /// argument is not [SeatSelectionState::GotPipes]. 1022 | /// 1023 | /// # Side effects 1024 | /// 1025 | /// After all pipes have been read successfully 1026 | /// (i.e. [`read_pipes_to_data`] returned [`Ok`]), 1027 | /// the value in [`SeatSelectionState::GotPipes::pipes`] 1028 | /// is replaced with an empty [`Vec`]. 1029 | /// 1030 | /// If this function returns [`Ok`], the value in 1031 | /// [`SeatSelectionState::GotPipes::ordered_mime_types`] 1032 | /// has been replaced with an empty [`Vec`]. 1033 | async fn handle_pipes_selection_state<'a>( 1034 | seat_name: u32, 1035 | data_control_device: ZwlrDataControlDeviceV1, 1036 | selection_offers: &'a HashMap, 1037 | selection_type: SelectionType, 1038 | selection_state: &'a mut SeatSelectionState, 1039 | size_limit: Option, 1040 | ignore_selection_event_on_error: bool, 1041 | ) -> Result, &'a mut SeatSelectionState> { 1042 | let SeatSelectionState::GotPipes { 1043 | ordered_mime_types, 1044 | pipes, 1045 | bytes_read, 1046 | } = selection_state 1047 | else { 1048 | unreachable!(); 1049 | }; 1050 | 1051 | let data_result = read_pipes_to_data(pipes, bytes_read, size_limit, ignore_selection_event_on_error).await; 1052 | let data = match data_result { 1053 | Ok(_) => std::mem::take(pipes) 1054 | .into_iter() 1055 | .filter_map(|mime_type_and_pipe| match mime_type_and_pipe.data_read { 1056 | Some(Ok(data)) => Some((mime_type_and_pipe.mime_type, data.into_boxed_slice())), 1057 | Some(Err(ReadToDataError::IoError(err))) => { 1058 | log::trace!( 1059 | target: &log_seat_target(seat_name), 1060 | "Current {} selection event: ignoring mime type {:?}: failed to read data: {}", 1061 | selection_type.get_clipboard_type_str(false), 1062 | mime_type_and_pipe.mime_type, 1063 | err, 1064 | ); 1065 | None 1066 | } 1067 | Some(Err(ReadToDataError::SizeLimitExceeded)) => { 1068 | // Impossible, because a SizeLimitExceeded error is returned immediately 1069 | unreachable!() 1070 | } 1071 | None => unreachable!(), 1072 | }) 1073 | .collect::>(), 1074 | Err(err) => { 1075 | match err { 1076 | ReadToDataError::IoError(err) => { 1077 | log::trace!( 1078 | target: &log_seat_target(seat_name), 1079 | "Ignoring {} selection event: failed to read data: {}", 1080 | selection_type.get_clipboard_type_str(false), 1081 | err 1082 | ); 1083 | } 1084 | ReadToDataError::SizeLimitExceeded => { 1085 | log::trace!( 1086 | target: &log_seat_target(seat_name), 1087 | "Ignoring {} selection event: offer exceeded specified selection size limit", 1088 | selection_type.get_clipboard_type_str(false) 1089 | ); 1090 | } 1091 | } 1092 | 1093 | return Err(selection_state); 1094 | } 1095 | }; 1096 | 1097 | if data.is_empty() { 1098 | log::trace!( 1099 | target: &log_seat_target(seat_name), 1100 | "Ignoring {} selection event: all data reads returned an error", 1101 | selection_type.get_clipboard_type_str(false), 1102 | ); 1103 | return Err(selection_state); 1104 | } 1105 | 1106 | let owned_ordered_mime_types = std::mem::take(ordered_mime_types); 1107 | 1108 | Ok(MimeTypesWithData { 1109 | seat_name, 1110 | data_control_device, 1111 | selection_offers, 1112 | selection_state, 1113 | selection_type, 1114 | ordered_mime_types: owned_ordered_mime_types, 1115 | data, 1116 | }) 1117 | } 1118 | 1119 | /// Handles clipboard data requests. 1120 | fn data_source_cb( 1121 | seat_name: u32, 1122 | selection_type: SelectionType, 1123 | event_context: EventCtx, 1124 | data_map: &Arc, Box<[u8]>>>, 1125 | ) { 1126 | match event_context.event { 1127 | zwlr_data_control_source_v1::Event::Send(send) => { 1128 | log::trace!( 1129 | target: &log_seat_target(seat_name), 1130 | "{} clipboard data source {}: received new request for mime type {:?}", 1131 | selection_type.get_clipboard_type_str(true), 1132 | event_context.proxy.id().as_u32(), 1133 | send.mime_type, 1134 | ); 1135 | 1136 | // Check if the file descriptor comes from our own app 1137 | let selection_state = event_context 1138 | .state 1139 | .seats 1140 | .get_mut(&seat_name) 1141 | .and_then(|seat| match selection_type { 1142 | SelectionType::Regular => seat.regular_selection.as_mut(), 1143 | SelectionType::Primary => seat.primary_selection.as_mut(), 1144 | }); 1145 | 1146 | let fd_file = File::from(send.fd); 1147 | let fd_identifier = match FdIdentifier::try_from(&fd_file) { 1148 | Ok(fd_identifier) => fd_identifier, 1149 | Err(err) => { 1150 | log::debug!( 1151 | target: &log_seat_target(seat_name), 1152 | "{} clipboard data source {}: could not get file metadata for mime type {:?}: {}", 1153 | selection_type.get_clipboard_type_str(true), 1154 | event_context.proxy.id().as_u32(), 1155 | send.mime_type, 1156 | err 1157 | ); 1158 | drop(fd_file); // Explicitly close file descriptor 1159 | return; 1160 | } 1161 | }; 1162 | 1163 | if let Some(SeatSelectionState::ReadMimes { 1164 | data_control_offer: _, 1165 | ordered_mime_types: _, 1166 | unique_mime_types: _, 1167 | bytes_read: _, 1168 | fd_from_own_app, 1169 | }) = selection_state 1170 | { 1171 | if let Some(is_from_own_app) = fd_from_own_app.get_mut(&fd_identifier) { 1172 | // File descriptor is from our own app, so we update the information 1173 | *is_from_own_app = true; 1174 | // Explicitly close file descriptor 1175 | drop(fd_file); 1176 | // Also we do not send any data, because we do not want to read it anyway. 1177 | // Therefore return. 1178 | return; 1179 | } 1180 | } 1181 | 1182 | if !data_map.contains_key(send.mime_type.as_c_str()) { 1183 | // Mime type not available, so return 1184 | log::trace!( 1185 | target: &log_seat_target(seat_name), 1186 | "{} clipboard data source {}: mime type {:?} is not available", 1187 | selection_type.get_clipboard_type_str(true), 1188 | event_context.proxy.id().as_u32(), 1189 | send.mime_type, 1190 | ); 1191 | drop(fd_file); // Explicitly close file descriptor 1192 | return; 1193 | } 1194 | 1195 | // Write the data to the file descriptor in a worker thread, 1196 | // so we do not block the main thread. 1197 | let data_map_clone = Arc::clone(data_map); 1198 | let mut write_handle = match FdWrite::try_from(OwnedFd::from(fd_file)) { 1199 | Ok(write_handle) => write_handle, 1200 | Err(err) => { 1201 | log::warn!( 1202 | target: &log_seat_target(seat_name), 1203 | "{} clipboard data source {}: failed to create write handle: {}", 1204 | selection_type.get_clipboard_type_str(true), 1205 | event_context.proxy.id().as_u32(), 1206 | err, 1207 | ); 1208 | return; 1209 | } 1210 | }; 1211 | let write_timeout = event_context.state.settings.write_timeout; 1212 | 1213 | tokio::spawn(async move { 1214 | let data = data_map_clone.get(send.mime_type.as_c_str()).unwrap().deref(); 1215 | 1216 | enum TimeoutResult { 1217 | Ok, 1218 | IoError(std::io::Error), 1219 | Timeout, 1220 | } 1221 | 1222 | let write_result = tokio::select! { 1223 | biased; 1224 | 1225 | res = write_handle.write_all(data) => { 1226 | match res { 1227 | Ok(()) => TimeoutResult::Ok, 1228 | Err(err) => TimeoutResult::IoError(err), 1229 | } 1230 | } 1231 | _ = tokio::time::sleep(write_timeout) => { 1232 | TimeoutResult::Timeout 1233 | } 1234 | }; 1235 | 1236 | drop(write_handle); // Explicitly close file descriptor 1237 | drop(data_map_clone); 1238 | 1239 | match write_result { 1240 | TimeoutResult::Ok => { 1241 | // Since FdWrite uses libc::write directly, there is no need for flushing 1242 | } 1243 | TimeoutResult::IoError(err) => { 1244 | log::warn!( 1245 | target: &log_seat_target(seat_name), 1246 | "{} clipboard data source {}: failed to write clipboard data for mime type {:?}: {}", 1247 | selection_type.get_clipboard_type_str(true), 1248 | event_context.proxy.id().as_u32(), 1249 | send.mime_type, 1250 | err, 1251 | ); 1252 | } 1253 | TimeoutResult::Timeout => { 1254 | log::debug!( 1255 | target: &log_seat_target(seat_name), 1256 | "{} clipboard data source {}: failed to write clipboard data for mime type {:?}: timed out", 1257 | selection_type.get_clipboard_type_str(true), 1258 | event_context.proxy.id().as_u32(), 1259 | send.mime_type, 1260 | ); 1261 | } 1262 | } 1263 | }); 1264 | } 1265 | zwlr_data_control_source_v1::Event::Cancelled => { 1266 | event_context.proxy.destroy(event_context.conn); 1267 | 1268 | log::trace!( 1269 | target: &log_seat_target(seat_name), 1270 | "{} clipboard data source {}: received cancelled event and destroyed clipboard data source", 1271 | selection_type.get_clipboard_type_str(true), 1272 | event_context.proxy.id().as_u32(), 1273 | ); 1274 | } 1275 | fallback => { 1276 | log::debug!( 1277 | target: &log_seat_target(seat_name), 1278 | "zwlr_data_control_source_v1::Event: unhandled event: {:?}", 1279 | fallback 1280 | ); 1281 | } 1282 | } 1283 | } 1284 | 1285 | /// Sets the clipboard for a specific seat and selection type. 1286 | #[allow(clippy::too_many_arguments)] 1287 | fn set_clipboard( 1288 | connection: &mut Connection, 1289 | data_control_manager: ZwlrDataControlManagerV1, 1290 | seat_name: u32, 1291 | data_control_device: ZwlrDataControlDeviceV1, 1292 | selection_state: &mut SeatSelectionState, 1293 | selection_type: SelectionType, 1294 | ordered_mime_types: Vec>>, 1295 | data: HashMap>, Box<[u8]>>, 1296 | ) { 1297 | let source = data_control_manager.create_data_source(connection); 1298 | for mime_type in ordered_mime_types { 1299 | // Some mime types might have gotten ignored due to errors. 1300 | // Only offer the mime types for which we have the data. 1301 | if data.contains_key(&mime_type) { 1302 | source.offer(connection, mime_type.deref().clone().into_c_string()); 1303 | } 1304 | } 1305 | 1306 | let mut boxed_data = HashMap::with_capacity(data.len()); 1307 | for (key, value) in data { 1308 | // Now that in the ordered_mime_types loop above all remaining Rc's have been dropped, 1309 | // the ones in the HashMap should be unique. Therefore, unwrapping Rc::into_inner cannot fail. 1310 | boxed_data.insert(Rc::into_inner(key).unwrap(), value); 1311 | } 1312 | 1313 | let arc_data = Arc::new(boxed_data); 1314 | connection.set_callback_for(source, move |event_context| { 1315 | data_source_cb(seat_name, selection_type, event_context, &arc_data); 1316 | }); 1317 | 1318 | let source_id = source.id().as_u32(); 1319 | 1320 | match selection_type { 1321 | SelectionType::Regular => data_control_device.set_selection(connection, Some(source)), 1322 | SelectionType::Primary => data_control_device.set_primary_selection(connection, Some(source)), 1323 | } 1324 | 1325 | log::trace!( 1326 | target: &log_seat_target(seat_name), 1327 | "Created {} clipboard data source {}", 1328 | selection_type.get_clipboard_type_str(false), 1329 | source_id, 1330 | ); 1331 | 1332 | selection_state.reset(connection); 1333 | } 1334 | --------------------------------------------------------------------------------