├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── scripts ├── active_watcher ├── deploy └── lswin └── src ├── dbus.rs ├── main.rs ├── toplevel.rs ├── topmaid.rs ├── util.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 = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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 = "autocfg" 31 | version = "1.4.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 34 | 35 | [[package]] 36 | name = "backtrace" 37 | version = "0.3.74" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 40 | dependencies = [ 41 | "addr2line", 42 | "cfg-if", 43 | "libc", 44 | "miniz_oxide", 45 | "object", 46 | "rustc-demangle", 47 | "windows-targets", 48 | ] 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "2.9.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 55 | 56 | [[package]] 57 | name = "byteorder" 58 | version = "1.5.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 61 | 62 | [[package]] 63 | name = "cc" 64 | version = "1.2.18" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" 67 | dependencies = [ 68 | "shlex", 69 | ] 70 | 71 | [[package]] 72 | name = "cfg-if" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 76 | 77 | [[package]] 78 | name = "dbus" 79 | version = "0.9.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 82 | dependencies = [ 83 | "futures-channel", 84 | "futures-util", 85 | "libc", 86 | "libdbus-sys", 87 | "winapi", 88 | ] 89 | 90 | [[package]] 91 | name = "dbus-crossroads" 92 | version = "0.5.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0" 95 | dependencies = [ 96 | "dbus", 97 | ] 98 | 99 | [[package]] 100 | name = "dbus-tokio" 101 | version = "0.7.6" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" 104 | dependencies = [ 105 | "dbus", 106 | "libc", 107 | "tokio", 108 | ] 109 | 110 | [[package]] 111 | name = "downcast-rs" 112 | version = "1.2.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 115 | 116 | [[package]] 117 | name = "errno" 118 | version = "0.3.11" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 121 | dependencies = [ 122 | "libc", 123 | "windows-sys 0.59.0", 124 | ] 125 | 126 | [[package]] 127 | name = "eyre" 128 | version = "0.6.12" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 131 | dependencies = [ 132 | "indenter", 133 | "once_cell", 134 | ] 135 | 136 | [[package]] 137 | name = "futures" 138 | version = "0.3.31" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 141 | dependencies = [ 142 | "futures-channel", 143 | "futures-core", 144 | "futures-executor", 145 | "futures-io", 146 | "futures-sink", 147 | "futures-task", 148 | "futures-util", 149 | ] 150 | 151 | [[package]] 152 | name = "futures-channel" 153 | version = "0.3.31" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 156 | dependencies = [ 157 | "futures-core", 158 | "futures-sink", 159 | ] 160 | 161 | [[package]] 162 | name = "futures-core" 163 | version = "0.3.31" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 166 | 167 | [[package]] 168 | name = "futures-executor" 169 | version = "0.3.31" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 172 | dependencies = [ 173 | "futures-core", 174 | "futures-task", 175 | "futures-util", 176 | ] 177 | 178 | [[package]] 179 | name = "futures-io" 180 | version = "0.3.31" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 183 | 184 | [[package]] 185 | name = "futures-macro" 186 | version = "0.3.31" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 189 | dependencies = [ 190 | "proc-macro2", 191 | "quote", 192 | "syn", 193 | ] 194 | 195 | [[package]] 196 | name = "futures-sink" 197 | version = "0.3.31" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 200 | 201 | [[package]] 202 | name = "futures-task" 203 | version = "0.3.31" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 206 | 207 | [[package]] 208 | name = "futures-util" 209 | version = "0.3.31" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 212 | dependencies = [ 213 | "futures-channel", 214 | "futures-core", 215 | "futures-io", 216 | "futures-macro", 217 | "futures-sink", 218 | "futures-task", 219 | "memchr", 220 | "pin-project-lite", 221 | "pin-utils", 222 | "slab", 223 | ] 224 | 225 | [[package]] 226 | name = "gimli" 227 | version = "0.31.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 230 | 231 | [[package]] 232 | name = "indenter" 233 | version = "0.3.3" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 236 | 237 | [[package]] 238 | name = "lazy_static" 239 | version = "1.5.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 242 | 243 | [[package]] 244 | name = "libc" 245 | version = "0.2.171" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 248 | 249 | [[package]] 250 | name = "libdbus-sys" 251 | version = "0.2.5" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 254 | dependencies = [ 255 | "pkg-config", 256 | ] 257 | 258 | [[package]] 259 | name = "linux-raw-sys" 260 | version = "0.4.15" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 263 | 264 | [[package]] 265 | name = "log" 266 | version = "0.4.27" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 269 | 270 | [[package]] 271 | name = "matchers" 272 | version = "0.1.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 275 | dependencies = [ 276 | "regex-automata 0.1.10", 277 | ] 278 | 279 | [[package]] 280 | name = "memchr" 281 | version = "2.7.4" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 284 | 285 | [[package]] 286 | name = "miniz_oxide" 287 | version = "0.8.7" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" 290 | dependencies = [ 291 | "adler2", 292 | ] 293 | 294 | [[package]] 295 | name = "mio" 296 | version = "1.0.3" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 299 | dependencies = [ 300 | "libc", 301 | "wasi", 302 | "windows-sys 0.52.0", 303 | ] 304 | 305 | [[package]] 306 | name = "nu-ansi-term" 307 | version = "0.46.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 310 | dependencies = [ 311 | "overload", 312 | "winapi", 313 | ] 314 | 315 | [[package]] 316 | name = "object" 317 | version = "0.36.7" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 320 | dependencies = [ 321 | "memchr", 322 | ] 323 | 324 | [[package]] 325 | name = "once_cell" 326 | version = "1.21.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 329 | 330 | [[package]] 331 | name = "overload" 332 | version = "0.1.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 335 | 336 | [[package]] 337 | name = "pin-project-lite" 338 | version = "0.2.16" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 341 | 342 | [[package]] 343 | name = "pin-utils" 344 | version = "0.1.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 347 | 348 | [[package]] 349 | name = "pkg-config" 350 | version = "0.3.32" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 353 | 354 | [[package]] 355 | name = "proc-macro2" 356 | version = "1.0.94" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 359 | dependencies = [ 360 | "unicode-ident", 361 | ] 362 | 363 | [[package]] 364 | name = "quick-xml" 365 | version = "0.37.4" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" 368 | dependencies = [ 369 | "memchr", 370 | ] 371 | 372 | [[package]] 373 | name = "quote" 374 | version = "1.0.40" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 377 | dependencies = [ 378 | "proc-macro2", 379 | ] 380 | 381 | [[package]] 382 | name = "regex" 383 | version = "1.11.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 386 | dependencies = [ 387 | "aho-corasick", 388 | "memchr", 389 | "regex-automata 0.4.9", 390 | "regex-syntax 0.8.5", 391 | ] 392 | 393 | [[package]] 394 | name = "regex-automata" 395 | version = "0.1.10" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 398 | dependencies = [ 399 | "regex-syntax 0.6.29", 400 | ] 401 | 402 | [[package]] 403 | name = "regex-automata" 404 | version = "0.4.9" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 407 | dependencies = [ 408 | "aho-corasick", 409 | "memchr", 410 | "regex-syntax 0.8.5", 411 | ] 412 | 413 | [[package]] 414 | name = "regex-syntax" 415 | version = "0.6.29" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 418 | 419 | [[package]] 420 | name = "regex-syntax" 421 | version = "0.8.5" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 424 | 425 | [[package]] 426 | name = "rustc-demangle" 427 | version = "0.1.24" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 430 | 431 | [[package]] 432 | name = "rustix" 433 | version = "0.38.44" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 436 | dependencies = [ 437 | "bitflags", 438 | "errno", 439 | "libc", 440 | "linux-raw-sys", 441 | "windows-sys 0.59.0", 442 | ] 443 | 444 | [[package]] 445 | name = "sharded-slab" 446 | version = "0.1.7" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 449 | dependencies = [ 450 | "lazy_static", 451 | ] 452 | 453 | [[package]] 454 | name = "shlex" 455 | version = "1.3.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 458 | 459 | [[package]] 460 | name = "slab" 461 | version = "0.4.9" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 464 | dependencies = [ 465 | "autocfg", 466 | ] 467 | 468 | [[package]] 469 | name = "smallvec" 470 | version = "1.15.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 473 | 474 | [[package]] 475 | name = "socket2" 476 | version = "0.5.9" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 479 | dependencies = [ 480 | "libc", 481 | "windows-sys 0.52.0", 482 | ] 483 | 484 | [[package]] 485 | name = "syn" 486 | version = "2.0.100" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 489 | dependencies = [ 490 | "proc-macro2", 491 | "quote", 492 | "unicode-ident", 493 | ] 494 | 495 | [[package]] 496 | name = "taskmaid" 497 | version = "0.2.0" 498 | dependencies = [ 499 | "byteorder", 500 | "dbus", 501 | "dbus-crossroads", 502 | "dbus-tokio", 503 | "eyre", 504 | "futures", 505 | "tokio", 506 | "tracing", 507 | "tracing-subscriber", 508 | "wayland-client", 509 | "wayland-protocols", 510 | "wayland-protocols-wlr", 511 | ] 512 | 513 | [[package]] 514 | name = "thread_local" 515 | version = "1.1.8" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 518 | dependencies = [ 519 | "cfg-if", 520 | "once_cell", 521 | ] 522 | 523 | [[package]] 524 | name = "tokio" 525 | version = "1.44.2" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 528 | dependencies = [ 529 | "backtrace", 530 | "libc", 531 | "mio", 532 | "pin-project-lite", 533 | "socket2", 534 | "tokio-macros", 535 | "windows-sys 0.52.0", 536 | ] 537 | 538 | [[package]] 539 | name = "tokio-macros" 540 | version = "2.5.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 543 | dependencies = [ 544 | "proc-macro2", 545 | "quote", 546 | "syn", 547 | ] 548 | 549 | [[package]] 550 | name = "tracing" 551 | version = "0.1.41" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 554 | dependencies = [ 555 | "pin-project-lite", 556 | "tracing-attributes", 557 | "tracing-core", 558 | ] 559 | 560 | [[package]] 561 | name = "tracing-attributes" 562 | version = "0.1.28" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 565 | dependencies = [ 566 | "proc-macro2", 567 | "quote", 568 | "syn", 569 | ] 570 | 571 | [[package]] 572 | name = "tracing-core" 573 | version = "0.1.33" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 576 | dependencies = [ 577 | "once_cell", 578 | "valuable", 579 | ] 580 | 581 | [[package]] 582 | name = "tracing-log" 583 | version = "0.2.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 586 | dependencies = [ 587 | "log", 588 | "once_cell", 589 | "tracing-core", 590 | ] 591 | 592 | [[package]] 593 | name = "tracing-subscriber" 594 | version = "0.3.19" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 597 | dependencies = [ 598 | "matchers", 599 | "nu-ansi-term", 600 | "once_cell", 601 | "regex", 602 | "sharded-slab", 603 | "smallvec", 604 | "thread_local", 605 | "tracing", 606 | "tracing-core", 607 | "tracing-log", 608 | ] 609 | 610 | [[package]] 611 | name = "unicode-ident" 612 | version = "1.0.18" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 615 | 616 | [[package]] 617 | name = "valuable" 618 | version = "0.1.1" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 621 | 622 | [[package]] 623 | name = "wasi" 624 | version = "0.11.0+wasi-snapshot-preview1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 627 | 628 | [[package]] 629 | name = "wayland-backend" 630 | version = "0.3.8" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" 633 | dependencies = [ 634 | "cc", 635 | "downcast-rs", 636 | "rustix", 637 | "smallvec", 638 | "wayland-sys", 639 | ] 640 | 641 | [[package]] 642 | name = "wayland-client" 643 | version = "0.31.8" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" 646 | dependencies = [ 647 | "bitflags", 648 | "rustix", 649 | "wayland-backend", 650 | "wayland-scanner", 651 | ] 652 | 653 | [[package]] 654 | name = "wayland-protocols" 655 | version = "0.32.6" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" 658 | dependencies = [ 659 | "bitflags", 660 | "wayland-backend", 661 | "wayland-client", 662 | "wayland-scanner", 663 | ] 664 | 665 | [[package]] 666 | name = "wayland-protocols-wlr" 667 | version = "0.3.6" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" 670 | dependencies = [ 671 | "bitflags", 672 | "wayland-backend", 673 | "wayland-client", 674 | "wayland-protocols", 675 | "wayland-scanner", 676 | ] 677 | 678 | [[package]] 679 | name = "wayland-scanner" 680 | version = "0.31.6" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quick-xml", 686 | "quote", 687 | ] 688 | 689 | [[package]] 690 | name = "wayland-sys" 691 | version = "0.31.6" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" 694 | dependencies = [ 695 | "pkg-config", 696 | ] 697 | 698 | [[package]] 699 | name = "winapi" 700 | version = "0.3.9" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 703 | dependencies = [ 704 | "winapi-i686-pc-windows-gnu", 705 | "winapi-x86_64-pc-windows-gnu", 706 | ] 707 | 708 | [[package]] 709 | name = "winapi-i686-pc-windows-gnu" 710 | version = "0.4.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 713 | 714 | [[package]] 715 | name = "winapi-x86_64-pc-windows-gnu" 716 | version = "0.4.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 719 | 720 | [[package]] 721 | name = "windows-sys" 722 | version = "0.52.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 725 | dependencies = [ 726 | "windows-targets", 727 | ] 728 | 729 | [[package]] 730 | name = "windows-sys" 731 | version = "0.59.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 734 | dependencies = [ 735 | "windows-targets", 736 | ] 737 | 738 | [[package]] 739 | name = "windows-targets" 740 | version = "0.52.6" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 743 | dependencies = [ 744 | "windows_aarch64_gnullvm", 745 | "windows_aarch64_msvc", 746 | "windows_i686_gnu", 747 | "windows_i686_gnullvm", 748 | "windows_i686_msvc", 749 | "windows_x86_64_gnu", 750 | "windows_x86_64_gnullvm", 751 | "windows_x86_64_msvc", 752 | ] 753 | 754 | [[package]] 755 | name = "windows_aarch64_gnullvm" 756 | version = "0.52.6" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 759 | 760 | [[package]] 761 | name = "windows_aarch64_msvc" 762 | version = "0.52.6" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 765 | 766 | [[package]] 767 | name = "windows_i686_gnu" 768 | version = "0.52.6" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 771 | 772 | [[package]] 773 | name = "windows_i686_gnullvm" 774 | version = "0.52.6" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 777 | 778 | [[package]] 779 | name = "windows_i686_msvc" 780 | version = "0.52.6" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 783 | 784 | [[package]] 785 | name = "windows_x86_64_gnu" 786 | version = "0.52.6" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 789 | 790 | [[package]] 791 | name = "windows_x86_64_gnullvm" 792 | version = "0.52.6" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 795 | 796 | [[package]] 797 | name = "windows_x86_64_msvc" 798 | version = "0.52.6" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 801 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "taskmaid" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | eyre = "*" 10 | tracing = "*" 11 | 12 | byteorder = "*" 13 | 14 | futures = "*" 15 | dbus = "*" 16 | dbus-crossroads = "*" 17 | dbus-tokio = "*" 18 | 19 | wayland-client = "*" 20 | 21 | [dependencies.tracing-subscriber] 22 | version = "*" 23 | features = ["env-filter"] 24 | 25 | [dependencies.wayland-protocols] 26 | version = "*" 27 | features = ["client"] 28 | 29 | [dependencies.wayland-protocols-wlr] 30 | version = "*" 31 | features = ["client"] 32 | 33 | [dependencies.tokio] 34 | version = "*" 35 | features = ["net", "rt", "sync", "macros"] 36 | 37 | [profile.release] 38 | lto = true 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, 依云 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This program exports useful APIs for Wayland desktop integration. 2 | 3 | Implemented: 4 | 5 | * Active window title, app_id and on which output (monitor) 6 | * Close active window 7 | * List all windows 8 | 9 | For integration with [waybar](https://github.com/Alexays/Waybar), you can see [my configuration](https://github.com/lilydjwg/dotconfig/tree/master/waybar). 10 | -------------------------------------------------------------------------------- /scripts/active_watcher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import dbus 4 | from dbus.mainloop.glib import DBusGMainLoop 5 | from gi.repository import GLib 6 | 7 | def handle_active(a): 8 | title, app_id, output = a 9 | print(title, app_id, output) 10 | 11 | def prop_changed(_iface, x, _sig): 12 | if a := x.get('active'): 13 | handle_active(a) 14 | 15 | def main(): 16 | DBusGMainLoop(set_as_default=True) 17 | 18 | bus = dbus.SessionBus() 19 | obj = bus.get_object('me.lilydjwg.taskmaid', '/taskmaid') 20 | prop = dbus.Interface(obj, dbus_interface='org.freedesktop.DBus.Properties') 21 | a = prop.Get('me.lilydjwg.taskmaid', 'active') 22 | handle_active(a) 23 | prop.connect_to_signal('PropertiesChanged', prop_changed) 24 | 25 | loop = GLib.MainLoop() 26 | loop.run() 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "$(dirname "$0")/.." 4 | 5 | git pull --rebase 6 | target_dir="$(cargo metadata --format-version 1 --no-deps | jq -r .target_directory)" 7 | bin=taskmaid 8 | 9 | cargo update 10 | cargo build --release 11 | 12 | install -Dsm755 "$target_dir"/release/$bin ~/bin/ 13 | tar c ~/bin/$bin | ssh l.lilydjwg.me tar xvU -C / 14 | 15 | git add . 16 | git commit -m 'update deps' 17 | git push 18 | -------------------------------------------------------------------------------- /scripts/lswin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import enum 4 | 5 | import dbus 6 | 7 | class State(enum.IntEnum): 8 | Maximized = 0 9 | Minimized = 1 10 | Active = 2 11 | Fullscreen = 3 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | def main(): 17 | bus = dbus.SessionBus() 18 | obj = bus.get_object('me.lilydjwg.taskmaid', '/taskmaid') 19 | maid = dbus.Interface(obj, dbus_interface='me.lilydjwg.taskmaid') 20 | wins = [] 21 | for win in maid.List(): 22 | id, title, app_id, output, states = win 23 | states = [State(x) for x in states] 24 | if states: 25 | states_str = ', '.join(str(x) for x in states) 26 | states_str = f'[{states_str}]' 27 | else: 28 | states_str = '' 29 | wins.append((id, title, app_id, output, states_str)) 30 | 31 | wins.sort() 32 | output_width = max(len(w[3]) for w in wins) 33 | app_id_width = max(len(w[2]) for w in wins) 34 | 35 | for w in wins: 36 | id, title, app_id, output, states_str = w 37 | print(f'{id} {output:{output_width}} {app_id:{app_id_width}} {title} {states_str}') 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /src/dbus.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex, RwLock}; 2 | 3 | use dbus_tokio::connection; 4 | use dbus::channel::{Sender, MatchingReceiver}; 5 | use dbus::message::MatchRule; 6 | use dbus_crossroads::{MethodErr, Crossroads, IfaceBuilder}; 7 | use eyre::Result; 8 | use tokio::sync::mpsc::Receiver; 9 | use tracing::{debug, error}; 10 | 11 | use super::topmaid::{TopMaid, Signal}; 12 | 13 | pub async fn dbus_run( 14 | maid: Arc>, 15 | mut rx: Receiver, 16 | ) -> Result<(), Box> { 17 | let (resource, c) = connection::new_session_sync()?; 18 | 19 | let _handle = tokio::spawn(async { 20 | let err = resource.await; 21 | panic!("Lost connection to D-Bus: {err}"); 22 | }); 23 | 24 | let cr = Arc::new(Mutex::new(Crossroads::new())); 25 | let mut active_changed = None; 26 | let token = cr.lock().unwrap().register( 27 | "me.lilydjwg.taskmaid", |b: &mut IfaceBuilder>>| { 28 | let cb = b.property("active") 29 | .get(|_, maid| { 30 | maid.read().unwrap().get_active() 31 | .map(|a| (a.title, a.app_id, a.output_name)) 32 | .ok_or_else(||MethodErr::failed("no toplevel active")) 33 | }) 34 | .changed_msg_fn(); 35 | b.method("List", (), ("reply",), move |_, maid, _: ()| { 36 | Ok((maid.read().unwrap().list(),)) 37 | }); 38 | b.method("CloseActive", (), (), move |_, maid, _: ()| { 39 | maid.read().unwrap().close_active(); 40 | Ok(()) 41 | }); 42 | active_changed = Some(cb); 43 | }); 44 | cr.lock().unwrap().insert("/taskmaid", &[token], maid); 45 | 46 | c.request_name("me.lilydjwg.taskmaid", false, true, false).await?; 47 | 48 | c.start_receive(MatchRule::new_method_call(), Box::new(move |msg, conn| { 49 | cr.lock().unwrap().handle_message(msg, conn).unwrap(); 50 | true 51 | })); 52 | 53 | while let Some(sig) = rx.recv().await { 54 | match sig { 55 | Signal::ActiveChanged(a) => { 56 | debug!("active toplevel changed to {:?}", a); 57 | if let Some(f) = &active_changed { 58 | if let Some(msg) = f(&"/taskmaid".into(), &(a.title, a.app_id, a.output_name)) { 59 | if let Err(()) = c.send(msg) { 60 | error!("failed to send out D-Bus signal."); 61 | } 62 | } 63 | } 64 | } 65 | }; 66 | } 67 | unreachable!() 68 | } 69 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | use std::io::IsTerminal; 3 | 4 | use eyre::Result; 5 | use tracing_subscriber::EnvFilter; 6 | use futures::future; 7 | use tokio::sync::mpsc::channel; 8 | 9 | mod toplevel; 10 | mod topmaid; 11 | mod dbus; 12 | mod wayland; 13 | mod util; 14 | 15 | use topmaid::TopMaid; 16 | 17 | fn main() -> Result<()> { 18 | // default RUST_LOG=warn 19 | let filter = EnvFilter::try_from_default_env() 20 | .unwrap_or_else(|_| EnvFilter::from("warn")); 21 | let isatty = std::io::stderr().is_terminal(); 22 | let fmt = tracing_subscriber::fmt::fmt() 23 | .with_writer(std::io::stderr) 24 | .with_env_filter(filter) 25 | .with_ansi(isatty); 26 | if isatty { 27 | fmt.init(); 28 | } else { 29 | fmt.without_time().init(); 30 | } 31 | 32 | // keep it large since one toplevel may generate several events 33 | // and we receive all of them at startup 34 | let (toplevel_tx, toplevel_rx) = channel(10240); 35 | let (action_tx, action_rx) = channel(10); 36 | let (dbus_tx, dbus_rx) = channel(10); 37 | let fu1 = wayland::run(toplevel_tx, action_rx); 38 | let maid = Arc::new(RwLock::new(TopMaid::new(dbus_tx, action_tx))); 39 | let fu2 = TopMaid::run(Arc::clone(&maid), toplevel_rx); 40 | let fu3 = dbus::dbus_run(maid, dbus_rx); 41 | 42 | let fu = async || { 43 | let _ = future::join(future::join(fu1, fu2), fu3).await; 44 | }; 45 | let rt = tokio::runtime::Builder::new_current_thread() 46 | .enable_all() 47 | .build() 48 | .unwrap(); 49 | rt.block_on(fu()); 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/toplevel.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Toplevel { 3 | pub id: u32, 4 | pub title: Option, 5 | pub app_id: Option, 6 | pub output: Option, 7 | pub state: Vec, 8 | } 9 | 10 | impl Toplevel { 11 | pub fn new(id: u32) -> Self { 12 | Self { 13 | id, 14 | title: None, 15 | app_id: None, 16 | output: None, 17 | state: vec![], 18 | } 19 | } 20 | } 21 | 22 | use std::io::Cursor; 23 | use byteorder::{NativeEndian, ReadBytesExt}; 24 | 25 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 26 | pub enum State { 27 | Maximized = 0, 28 | Minimized = 1, 29 | Active = 2, 30 | Fullscreen = 3, 31 | } 32 | 33 | impl State { 34 | pub fn from_bytes(bytes: &[u8]) -> Vec { 35 | bytes.chunks(4).map(|buf| { 36 | let mut r = Cursor::new(buf); 37 | let a = r.read_u32::().unwrap(); 38 | State::from_u32(a) 39 | }).collect() 40 | } 41 | 42 | fn from_u32(a: u32) -> State { 43 | match a { 44 | 0 => State::Maximized, 45 | 1 => State::Minimized, 46 | 2 => State::Active, 47 | 3 => State::Fullscreen, 48 | _ => panic!("unknown state: {a}"), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Debug)] 54 | pub enum Event { 55 | New(u32), 56 | Title(u32, String), 57 | AppId(u32, String), 58 | State(u32, Vec), 59 | Output(u32, Option), 60 | /// it's time to generate D-Bus signals 61 | Done(u32), 62 | Closed(u32), 63 | /// Associate a name with an output 64 | OutputNew(u32, String), 65 | OutputRemoved(u32), 66 | } 67 | -------------------------------------------------------------------------------- /src/topmaid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, RwLock}; 3 | 4 | use tokio::sync::mpsc::{Sender, Receiver}; 5 | use tracing::debug; 6 | 7 | use super::toplevel::{Toplevel, State, Event}; 8 | use super::util::send_event; 9 | 10 | pub struct TopMaid { 11 | toplevels: HashMap, 12 | active_changed: bool, 13 | last_active_toplevel: u32, 14 | /// active toplevel just closed, we need its remaining information to show last active output 15 | just_closed: Option, 16 | dbus_tx: Sender, 17 | action_tx: Sender, 18 | no_active: bool, 19 | output_map: HashMap, 20 | } 21 | 22 | impl TopMaid { 23 | pub fn new(dbus_tx: Sender, action_tx: Sender) -> Self { 24 | Self { 25 | dbus_tx, 26 | action_tx, 27 | toplevels: HashMap::new(), 28 | active_changed: false, 29 | last_active_toplevel: 0, 30 | just_closed: None, 31 | no_active: true, 32 | output_map: HashMap::new(), 33 | } 34 | } 35 | 36 | #[allow(clippy::await_holding_lock)] 37 | pub async fn run(maid: Arc>, mut rx: Receiver) { 38 | while let Some(event) = rx.recv().await { 39 | maid.write().unwrap().handle_event(event).await; 40 | } 41 | } 42 | 43 | fn output_name(&self, id: Option) -> String { 44 | id.and_then(|id| 45 | self.output_map.get(&id).cloned() 46 | ).unwrap_or_else(|| String::from("unknown")) 47 | } 48 | 49 | async fn handle_event(&mut self, event: Event) { 50 | match event { 51 | Event::New(id) => { 52 | self.toplevels.insert(id, Toplevel::new(id)); 53 | } 54 | Event::Title(id, title) => { 55 | if let Some(t) = self.toplevels.get_mut(&id) { 56 | t.title = Some(title); 57 | } 58 | if id == self.last_active_toplevel { 59 | self.active_changed = true; 60 | } 61 | } 62 | Event::AppId(id, app_id) => { 63 | if let Some(t) = self.toplevels.get_mut(&id) { 64 | t.app_id = Some(app_id); 65 | } 66 | if id == self.last_active_toplevel { 67 | self.active_changed = true; 68 | } 69 | } 70 | Event::Output(id, output_id) => { 71 | if let Some(t) = self.toplevels.get_mut(&id) { 72 | t.output = output_id; 73 | } 74 | if id == self.last_active_toplevel { 75 | self.active_changed = true; 76 | } 77 | } 78 | Event::State(id, state) => { 79 | if state.contains(&State::Active) { 80 | self.last_active_toplevel = id; 81 | self.active_changed = true; 82 | self.no_active = false; 83 | } else if id == self.last_active_toplevel { 84 | self.no_active = true; 85 | self.active_changed = true; 86 | } 87 | if let Some(t) = self.toplevels.get_mut(&id) { 88 | t.state = state; 89 | } 90 | } 91 | Event::Closed(id) => { 92 | if let Some(t) = self.toplevels.remove(&id) { 93 | if id == self.last_active_toplevel { 94 | debug!("active toplevel closed"); 95 | self.no_active = true; 96 | let a = ActiveInfo { 97 | title: String::new(), 98 | app_id: String::new(), 99 | output_name: self.output_name(t.output), 100 | }; 101 | let _ = self.dbus_tx.send(Signal::ActiveChanged(a)).await; 102 | self.active_changed = false; 103 | } 104 | self.just_closed = Some(t); 105 | } 106 | } 107 | Event::Done(id) => { 108 | if id == self.last_active_toplevel && self.active_changed { 109 | if let Some(a) = self.get_active() { 110 | debug!("active changed to {:?}", a); 111 | let _ = self.dbus_tx.send(Signal::ActiveChanged(a)).await; 112 | } 113 | self.active_changed = false; 114 | } 115 | } 116 | Event::OutputNew(id, name) => { 117 | self.output_map.insert(id, name); 118 | } 119 | Event::OutputRemoved(id) => { 120 | self.output_map.remove(&id); 121 | } 122 | } 123 | } 124 | 125 | pub fn list(&self) -> Vec<(u32, String, String, String, Vec)> { 126 | self.toplevels.values().map(|t| { 127 | let st = t.state.iter().map(|st| *st as u32).collect(); 128 | (t.id, 129 | t.title.as_ref().map(|x| x.to_owned()).unwrap_or_default(), 130 | t.app_id.as_ref().map(|x| x.to_owned()).unwrap_or_default(), 131 | self.output_name(t.output), 132 | st, 133 | ) 134 | }).collect() 135 | } 136 | 137 | pub fn get_active(&self) -> Option { 138 | if let Some(t) = self.toplevels.get(&self.last_active_toplevel) { 139 | let output_name = self.output_name(t.output); 140 | let r = if !self.no_active { 141 | ActiveInfo { 142 | title: t.title.as_ref().map(|x| x.to_owned()).unwrap_or_default(), 143 | app_id: t.app_id.as_ref().map(|x| x.to_owned()).unwrap_or_default(), 144 | output_name, 145 | } 146 | } else { 147 | // signal that no active toplevel should be shown on this output 148 | ActiveInfo { 149 | title: String::new(), 150 | app_id: String::new(), 151 | output_name, 152 | } 153 | }; 154 | return Some(r); 155 | } 156 | 157 | self.just_closed.as_ref().map(|t| 158 | ActiveInfo { 159 | title: String::new(), 160 | app_id: String::new(), 161 | output_name: self.output_name(t.output), 162 | } 163 | ) 164 | } 165 | 166 | pub fn close_active(&self) { 167 | debug!("closing active toplevel ({})", self.last_active_toplevel); 168 | send_event(&self.action_tx, Action::Close(self.last_active_toplevel)); 169 | } 170 | } 171 | 172 | #[derive(Debug)] 173 | pub struct ActiveInfo { 174 | pub title: String, 175 | pub app_id: String, 176 | pub output_name: String, 177 | } 178 | 179 | pub enum Signal { 180 | ActiveChanged(ActiveInfo), 181 | } 182 | 183 | #[derive(Debug)] 184 | pub enum Action { 185 | Close(u32), 186 | } 187 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use tracing::error; 4 | use tokio::sync::mpsc::{Sender, error::TrySendError}; 5 | 6 | pub fn send_event(tx: &Sender, t: T) { 7 | if let Err(err) = tx.try_send(t) { 8 | match err { 9 | TrySendError::Full(t) => { 10 | error!("too many events to process! Event object discarded: {:?}", t); 11 | } 12 | TrySendError::Closed(_) => { 13 | panic!("channel closed unexpectedly"); 14 | } 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/wayland.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::os::fd::AsFd; 3 | 4 | use tokio::sync::mpsc::{Sender, Receiver}; 5 | 6 | use wayland_client::{Connection, QueueHandle, Dispatch, Proxy, event_created_child}; 7 | use wayland_client::protocol::{wl_registry, wl_output, wl_callback}; 8 | use wayland_protocols_wlr::foreign_toplevel::v1::client::{ 9 | zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1, self}, 10 | zwlr_foreign_toplevel_manager_v1::{ZwlrForeignToplevelManagerV1, self}, 11 | }; 12 | use tracing::{debug, warn}; 13 | use tokio::io::unix::AsyncFd; 14 | 15 | use super::toplevel; 16 | use super::topmaid::Action; 17 | use super::util::send_event; 18 | 19 | pub async fn run( 20 | toplevel_tx: Sender, 21 | mut action_rx: Receiver, 22 | ) { 23 | let mut state = State::new(toplevel_tx); 24 | 25 | let conn = Connection::connect_to_env().unwrap(); 26 | let display = conn.display(); 27 | let mut event_queue = conn.new_event_queue(); 28 | let qh = event_queue.handle(); 29 | let _registry = display.get_registry(&qh, ()); 30 | display.sync(&qh, ()); 31 | let afd = AsyncFd::new(conn.as_fd()).unwrap(); 32 | 33 | while !state.finished { 34 | event_queue.flush().unwrap(); 35 | let read_guard = event_queue.prepare_read().unwrap(); 36 | 37 | debug!("waiting to read from wayland server..."); 38 | tokio::select! { 39 | guard = afd.readable() => { 40 | guard.unwrap().clear_ready(); 41 | read_guard.read().unwrap(); 42 | event_queue.dispatch_pending(&mut state).unwrap(); 43 | } 44 | action = action_rx.recv() => match action.unwrap() { 45 | Action::Close(id) => state.close(id) 46 | } 47 | } 48 | } 49 | } 50 | 51 | struct State { 52 | toplevels: HashMap, 53 | outputs: Vec<(u32, wl_output::WlOutput)>, 54 | manager: Option, 55 | tx: Sender, 56 | finished: bool, 57 | } 58 | 59 | impl State { 60 | fn new(tx: Sender) -> Self { 61 | Self { 62 | toplevels: HashMap::new(), 63 | outputs: Vec::new(), 64 | manager: None, 65 | tx, 66 | finished: false, 67 | } 68 | } 69 | 70 | fn close(&self, id: u32) { 71 | debug!("closing {}", id); 72 | if let Some(t) = self.toplevels.get(&id) { 73 | t.close(); 74 | } 75 | } 76 | } 77 | 78 | impl Dispatch for State { 79 | fn event( 80 | s: &mut Self, 81 | registry: &wl_registry::WlRegistry, 82 | event: wl_registry::Event, 83 | _: &(), 84 | _: &Connection, 85 | qh: &QueueHandle, 86 | ) { 87 | if let wl_registry::Event::Global { name, interface, version } = event { 88 | match &interface[..] { 89 | "wl_output" => { 90 | debug!("got a new output: {}", name); 91 | assert!(version >= 4); 92 | let output = registry.bind::(name, 4, qh, ()); 93 | s.outputs.push((name, output)); 94 | } 95 | "zwlr_foreign_toplevel_manager_v1" => { 96 | assert!(version >= 3); 97 | s.manager = Some(registry.bind::(name, 3, qh, ())); 98 | } 99 | _ => {} 100 | } 101 | } else if let wl_registry::Event::GlobalRemove { name } = event { 102 | debug!("an output has been removed: {}", name); 103 | if let Some((idx, (_, o))) = s.outputs.iter().enumerate().find(|&(_, (oname, _))| *oname == name) { 104 | o.release(); 105 | s.outputs.remove(idx); 106 | send_event(&s.tx, toplevel::Event::OutputRemoved(name)); 107 | } 108 | } 109 | } 110 | } 111 | 112 | 113 | impl Dispatch for State { 114 | fn event( 115 | s: &mut Self, 116 | _: &wl_callback::WlCallback, 117 | _event: wl_callback::Event, 118 | _: &(), 119 | _: &Connection, 120 | _qh: &QueueHandle, 121 | ) { 122 | if s.manager.is_none() { 123 | panic!("Compositor does not support wlr-foreign-toplevel-management"); 124 | } 125 | } 126 | } 127 | 128 | impl Dispatch for State { 129 | fn event( 130 | s: &mut Self, 131 | o: &wl_output::WlOutput, 132 | event: wl_output::Event, 133 | _: &(), 134 | _: &Connection, 135 | _qh: &QueueHandle, 136 | ) { 137 | if let wl_output::Event::Name { name } = event { 138 | let oid = o.id().protocol_id(); 139 | send_event(&s.tx, toplevel::Event::OutputNew(oid, name)); 140 | } 141 | } 142 | } 143 | 144 | impl Dispatch for State { 145 | fn event( 146 | s: &mut Self, 147 | _: &ZwlrForeignToplevelManagerV1, 148 | event: zwlr_foreign_toplevel_manager_v1::Event, 149 | _: &(), 150 | _: &Connection, 151 | _qh: &QueueHandle, 152 | ) { 153 | match event { 154 | zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } => { 155 | let id = toplevel.id().protocol_id(); 156 | debug!("got a toplevel id {}", id); 157 | send_event(&s.tx, toplevel::Event::New(id)); 158 | s.toplevels.insert(id, toplevel); 159 | }, 160 | zwlr_foreign_toplevel_manager_v1::Event::Finished => { 161 | warn!("finished?"); 162 | s.finished = true; 163 | }, 164 | _ => unreachable!(), 165 | } 166 | } 167 | 168 | event_created_child!(State, ZwlrForeignToplevelManagerV1, [ 169 | zwlr_foreign_toplevel_manager_v1::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ()), 170 | ]); 171 | } 172 | 173 | 174 | impl Dispatch for State { 175 | fn event( 176 | s: &mut Self, 177 | toplevel: &ZwlrForeignToplevelHandleV1, 178 | event: zwlr_foreign_toplevel_handle_v1::Event, 179 | _: &(), 180 | _: &Connection, 181 | _qh: &QueueHandle, 182 | ) { 183 | let id = toplevel.id().protocol_id(); 184 | match event { 185 | Event::Title { title } => { 186 | debug!("toplevel@{} has title {}", id, title); 187 | send_event(&s.tx, toplevel::Event::Title(id, title)); 188 | } 189 | Event::AppId { app_id } => { 190 | debug!("toplevel@{} has app_id {}", id, app_id); 191 | send_event(&s.tx, toplevel::Event::AppId(id, app_id)); 192 | } 193 | Event::State { state } => { 194 | let state = toplevel::State::from_bytes(&state); 195 | debug!("toplevel@{} has state {:?}", id, state); 196 | if state.contains(&toplevel::State::Minimized) { 197 | toplevel.unset_minimized(); 198 | } 199 | send_event(&s.tx, toplevel::Event::State(id, state)); 200 | } 201 | Event::OutputEnter { output } => { 202 | let output_id = output.id().protocol_id(); 203 | debug!("toplevel@{} entered output {}", id, output_id); 204 | send_event(&s.tx, toplevel::Event::Output(id, Some(output_id))); 205 | } 206 | Event::OutputLeave { output } => { 207 | // if we have already bound to the new output, we will miss its output_leave events 208 | // at least we can record that they are left. 209 | debug!("toplevel@{} left output {}", id, output.id().protocol_id()); 210 | send_event(&s.tx, toplevel::Event::Output(id, None)); 211 | } 212 | Event::Closed => { 213 | debug!("{} has been closed", id); 214 | send_event(&s.tx, toplevel::Event::Closed(id)); 215 | toplevel.destroy(); 216 | s.toplevels.remove(&id); 217 | } 218 | Event::Done => { 219 | debug!("{}'s info is now stable", id); 220 | send_event(&s.tx, toplevel::Event::Done(id)); 221 | } 222 | _ => { } 223 | } 224 | } 225 | } 226 | 227 | --------------------------------------------------------------------------------