├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── help.rs ├── main.rs ├── parser.rs └── templates.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install system dependencies 20 | run: | 21 | sudo apt-get update \ 22 | && sudo apt-get install -y libdbus-1-dev 23 | - name: Build 24 | run: cargo build --verbose 25 | # - name: Run tests 26 | # run: cargo test --verbose 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | release/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## Next Release (TBD) 4 | 5 | Added support for `no_border` to `windowstate` 6 | Added support for `minimized` to `windowstate` 7 | 8 | ## v0.2.1 (2023-11-23) 9 | 10 | Reduced binary size. 11 | 12 | ## v0.2.0 (2023-11-23) 13 | 14 | ### Added 15 | 16 | Global options: 17 | 18 | - `--version` 19 | 20 | New global commands: 21 | 22 | - `savewindowstack` 23 | - `loadwindowstack` 24 | - `set_desktop` 25 | - `get_desktop` 26 | - `set_num_desktops` (KDE 5 only) 27 | - `get_num_desktops` 28 | 29 | New window actions: 30 | 31 | - `set_desktop_for_window` 32 | - `get_desktop_for_window` 33 | - `windowstate` 34 | - Supported properties: 35 | - above 36 | - below 37 | - skip_taskbar 38 | - skip_pager 39 | - fullscreen 40 | - shaded 41 | - demands_attention 42 | - MISSING: 43 | - modal 44 | - sticky 45 | - hidden 46 | - maximized_vert 47 | - maximized_horz 48 | 49 | 50 | New command options: 51 | 52 | - `search` 53 | - `--desktop` 54 | - `--screen` (KDE 5 only) 55 | - `windowmove` and `windowsize` 56 | - size in percentage 57 | 58 | ### Internal Changes 59 | 60 | - Script output is now sent via dbus, instead of parsing KWin logs. 61 | 62 | ## v0.1.0 (2023-11-17) 63 | 64 | Initial release 65 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.75" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.1.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "1.3.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.4.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 52 | 53 | [[package]] 54 | name = "block-buffer" 55 | version = "0.10.4" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 58 | dependencies = [ 59 | "generic-array", 60 | ] 61 | 62 | [[package]] 63 | name = "bumpalo" 64 | version = "3.14.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 67 | 68 | [[package]] 69 | name = "cargo-husky" 70 | version = "1.5.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" 73 | 74 | [[package]] 75 | name = "cc" 76 | version = "1.0.83" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 79 | dependencies = [ 80 | "libc", 81 | ] 82 | 83 | [[package]] 84 | name = "cfg-if" 85 | version = "1.0.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 88 | 89 | [[package]] 90 | name = "chrono" 91 | version = "0.4.31" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 94 | dependencies = [ 95 | "android-tzdata", 96 | "iana-time-zone", 97 | "js-sys", 98 | "num-traits", 99 | "wasm-bindgen", 100 | "windows-targets", 101 | ] 102 | 103 | [[package]] 104 | name = "core-foundation-sys" 105 | version = "0.8.4" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 108 | 109 | [[package]] 110 | name = "cpufeatures" 111 | version = "0.2.11" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 114 | dependencies = [ 115 | "libc", 116 | ] 117 | 118 | [[package]] 119 | name = "crypto-common" 120 | version = "0.1.6" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 123 | dependencies = [ 124 | "generic-array", 125 | "typenum", 126 | ] 127 | 128 | [[package]] 129 | name = "dbus" 130 | version = "0.9.7" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 133 | dependencies = [ 134 | "libc", 135 | "libdbus-sys", 136 | "winapi", 137 | ] 138 | 139 | [[package]] 140 | name = "digest" 141 | version = "0.10.7" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 144 | dependencies = [ 145 | "block-buffer", 146 | "crypto-common", 147 | ] 148 | 149 | [[package]] 150 | name = "env_logger" 151 | version = "0.10.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" 154 | dependencies = [ 155 | "humantime", 156 | "is-terminal", 157 | "log", 158 | "regex", 159 | "termcolor", 160 | ] 161 | 162 | [[package]] 163 | name = "errno" 164 | version = "0.3.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" 167 | dependencies = [ 168 | "libc", 169 | "windows-sys", 170 | ] 171 | 172 | [[package]] 173 | name = "fastrand" 174 | version = "2.0.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 177 | 178 | [[package]] 179 | name = "generic-array" 180 | version = "0.14.7" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 183 | dependencies = [ 184 | "typenum", 185 | "version_check", 186 | ] 187 | 188 | [[package]] 189 | name = "handlebars" 190 | version = "5.1.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" 193 | dependencies = [ 194 | "log", 195 | "pest", 196 | "pest_derive", 197 | "serde", 198 | "serde_json", 199 | "thiserror", 200 | ] 201 | 202 | [[package]] 203 | name = "hermit-abi" 204 | version = "0.3.3" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 207 | 208 | [[package]] 209 | name = "humantime" 210 | version = "2.1.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 213 | 214 | [[package]] 215 | name = "iana-time-zone" 216 | version = "0.1.58" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 219 | dependencies = [ 220 | "android_system_properties", 221 | "core-foundation-sys", 222 | "iana-time-zone-haiku", 223 | "js-sys", 224 | "wasm-bindgen", 225 | "windows-core", 226 | ] 227 | 228 | [[package]] 229 | name = "iana-time-zone-haiku" 230 | version = "0.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 233 | dependencies = [ 234 | "cc", 235 | ] 236 | 237 | [[package]] 238 | name = "is-terminal" 239 | version = "0.4.9" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 242 | dependencies = [ 243 | "hermit-abi", 244 | "rustix", 245 | "windows-sys", 246 | ] 247 | 248 | [[package]] 249 | name = "itoa" 250 | version = "1.0.9" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 253 | 254 | [[package]] 255 | name = "js-sys" 256 | version = "0.3.65" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 259 | dependencies = [ 260 | "wasm-bindgen", 261 | ] 262 | 263 | [[package]] 264 | name = "kdotool" 265 | version = "0.2.1" 266 | dependencies = [ 267 | "anyhow", 268 | "cargo-husky", 269 | "chrono", 270 | "dbus", 271 | "env_logger", 272 | "handlebars", 273 | "lexopt", 274 | "log", 275 | "phf", 276 | "serde", 277 | "serde_json", 278 | "tempfile", 279 | ] 280 | 281 | [[package]] 282 | name = "lexopt" 283 | version = "0.3.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" 286 | 287 | [[package]] 288 | name = "libc" 289 | version = "0.2.150" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 292 | 293 | [[package]] 294 | name = "libdbus-sys" 295 | version = "0.2.5" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 298 | dependencies = [ 299 | "pkg-config", 300 | ] 301 | 302 | [[package]] 303 | name = "linux-raw-sys" 304 | version = "0.4.11" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 307 | 308 | [[package]] 309 | name = "log" 310 | version = "0.4.20" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 313 | 314 | [[package]] 315 | name = "memchr" 316 | version = "2.6.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 319 | 320 | [[package]] 321 | name = "num-traits" 322 | version = "0.2.17" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 325 | dependencies = [ 326 | "autocfg", 327 | ] 328 | 329 | [[package]] 330 | name = "once_cell" 331 | version = "1.18.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 334 | 335 | [[package]] 336 | name = "pest" 337 | version = "2.7.5" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" 340 | dependencies = [ 341 | "memchr", 342 | "thiserror", 343 | "ucd-trie", 344 | ] 345 | 346 | [[package]] 347 | name = "pest_derive" 348 | version = "2.7.5" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" 351 | dependencies = [ 352 | "pest", 353 | "pest_generator", 354 | ] 355 | 356 | [[package]] 357 | name = "pest_generator" 358 | version = "2.7.5" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" 361 | dependencies = [ 362 | "pest", 363 | "pest_meta", 364 | "proc-macro2", 365 | "quote", 366 | "syn", 367 | ] 368 | 369 | [[package]] 370 | name = "pest_meta" 371 | version = "2.7.5" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" 374 | dependencies = [ 375 | "once_cell", 376 | "pest", 377 | "sha2", 378 | ] 379 | 380 | [[package]] 381 | name = "phf" 382 | version = "0.11.2" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 385 | dependencies = [ 386 | "phf_macros", 387 | "phf_shared", 388 | ] 389 | 390 | [[package]] 391 | name = "phf_generator" 392 | version = "0.11.2" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 395 | dependencies = [ 396 | "phf_shared", 397 | "rand", 398 | ] 399 | 400 | [[package]] 401 | name = "phf_macros" 402 | version = "0.11.2" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 405 | dependencies = [ 406 | "phf_generator", 407 | "phf_shared", 408 | "proc-macro2", 409 | "quote", 410 | "syn", 411 | ] 412 | 413 | [[package]] 414 | name = "phf_shared" 415 | version = "0.11.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 418 | dependencies = [ 419 | "siphasher", 420 | ] 421 | 422 | [[package]] 423 | name = "pkg-config" 424 | version = "0.3.27" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 427 | 428 | [[package]] 429 | name = "proc-macro2" 430 | version = "1.0.69" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 433 | dependencies = [ 434 | "unicode-ident", 435 | ] 436 | 437 | [[package]] 438 | name = "quote" 439 | version = "1.0.33" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 442 | dependencies = [ 443 | "proc-macro2", 444 | ] 445 | 446 | [[package]] 447 | name = "rand" 448 | version = "0.8.5" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 451 | dependencies = [ 452 | "rand_core", 453 | ] 454 | 455 | [[package]] 456 | name = "rand_core" 457 | version = "0.6.4" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 460 | 461 | [[package]] 462 | name = "redox_syscall" 463 | version = "0.4.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 466 | dependencies = [ 467 | "bitflags 1.3.2", 468 | ] 469 | 470 | [[package]] 471 | name = "regex" 472 | version = "1.10.2" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 475 | dependencies = [ 476 | "aho-corasick", 477 | "memchr", 478 | "regex-automata", 479 | "regex-syntax", 480 | ] 481 | 482 | [[package]] 483 | name = "regex-automata" 484 | version = "0.4.3" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 487 | dependencies = [ 488 | "aho-corasick", 489 | "memchr", 490 | "regex-syntax", 491 | ] 492 | 493 | [[package]] 494 | name = "regex-syntax" 495 | version = "0.8.2" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 498 | 499 | [[package]] 500 | name = "rustix" 501 | version = "0.38.25" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" 504 | dependencies = [ 505 | "bitflags 2.4.1", 506 | "errno", 507 | "libc", 508 | "linux-raw-sys", 509 | "windows-sys", 510 | ] 511 | 512 | [[package]] 513 | name = "ryu" 514 | version = "1.0.15" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 517 | 518 | [[package]] 519 | name = "serde" 520 | version = "1.0.193" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 523 | dependencies = [ 524 | "serde_derive", 525 | ] 526 | 527 | [[package]] 528 | name = "serde_derive" 529 | version = "1.0.193" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 532 | dependencies = [ 533 | "proc-macro2", 534 | "quote", 535 | "syn", 536 | ] 537 | 538 | [[package]] 539 | name = "serde_json" 540 | version = "1.0.108" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 543 | dependencies = [ 544 | "itoa", 545 | "ryu", 546 | "serde", 547 | ] 548 | 549 | [[package]] 550 | name = "sha2" 551 | version = "0.10.8" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 554 | dependencies = [ 555 | "cfg-if", 556 | "cpufeatures", 557 | "digest", 558 | ] 559 | 560 | [[package]] 561 | name = "siphasher" 562 | version = "0.3.11" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 565 | 566 | [[package]] 567 | name = "syn" 568 | version = "2.0.39" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 571 | dependencies = [ 572 | "proc-macro2", 573 | "quote", 574 | "unicode-ident", 575 | ] 576 | 577 | [[package]] 578 | name = "tempfile" 579 | version = "3.8.1" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" 582 | dependencies = [ 583 | "cfg-if", 584 | "fastrand", 585 | "redox_syscall", 586 | "rustix", 587 | "windows-sys", 588 | ] 589 | 590 | [[package]] 591 | name = "termcolor" 592 | version = "1.4.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" 595 | dependencies = [ 596 | "winapi-util", 597 | ] 598 | 599 | [[package]] 600 | name = "thiserror" 601 | version = "1.0.50" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 604 | dependencies = [ 605 | "thiserror-impl", 606 | ] 607 | 608 | [[package]] 609 | name = "thiserror-impl" 610 | version = "1.0.50" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 613 | dependencies = [ 614 | "proc-macro2", 615 | "quote", 616 | "syn", 617 | ] 618 | 619 | [[package]] 620 | name = "typenum" 621 | version = "1.17.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 624 | 625 | [[package]] 626 | name = "ucd-trie" 627 | version = "0.1.6" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 630 | 631 | [[package]] 632 | name = "unicode-ident" 633 | version = "1.0.12" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 636 | 637 | [[package]] 638 | name = "version_check" 639 | version = "0.9.4" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 642 | 643 | [[package]] 644 | name = "wasm-bindgen" 645 | version = "0.2.88" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 648 | dependencies = [ 649 | "cfg-if", 650 | "wasm-bindgen-macro", 651 | ] 652 | 653 | [[package]] 654 | name = "wasm-bindgen-backend" 655 | version = "0.2.88" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 658 | dependencies = [ 659 | "bumpalo", 660 | "log", 661 | "once_cell", 662 | "proc-macro2", 663 | "quote", 664 | "syn", 665 | "wasm-bindgen-shared", 666 | ] 667 | 668 | [[package]] 669 | name = "wasm-bindgen-macro" 670 | version = "0.2.88" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 673 | dependencies = [ 674 | "quote", 675 | "wasm-bindgen-macro-support", 676 | ] 677 | 678 | [[package]] 679 | name = "wasm-bindgen-macro-support" 680 | version = "0.2.88" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "syn", 687 | "wasm-bindgen-backend", 688 | "wasm-bindgen-shared", 689 | ] 690 | 691 | [[package]] 692 | name = "wasm-bindgen-shared" 693 | version = "0.2.88" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 696 | 697 | [[package]] 698 | name = "winapi" 699 | version = "0.3.9" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 702 | dependencies = [ 703 | "winapi-i686-pc-windows-gnu", 704 | "winapi-x86_64-pc-windows-gnu", 705 | ] 706 | 707 | [[package]] 708 | name = "winapi-i686-pc-windows-gnu" 709 | version = "0.4.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 712 | 713 | [[package]] 714 | name = "winapi-util" 715 | version = "0.1.6" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 718 | dependencies = [ 719 | "winapi", 720 | ] 721 | 722 | [[package]] 723 | name = "winapi-x86_64-pc-windows-gnu" 724 | version = "0.4.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 727 | 728 | [[package]] 729 | name = "windows-core" 730 | version = "0.51.1" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 733 | dependencies = [ 734 | "windows-targets", 735 | ] 736 | 737 | [[package]] 738 | name = "windows-sys" 739 | version = "0.48.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 742 | dependencies = [ 743 | "windows-targets", 744 | ] 745 | 746 | [[package]] 747 | name = "windows-targets" 748 | version = "0.48.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 751 | dependencies = [ 752 | "windows_aarch64_gnullvm", 753 | "windows_aarch64_msvc", 754 | "windows_i686_gnu", 755 | "windows_i686_msvc", 756 | "windows_x86_64_gnu", 757 | "windows_x86_64_gnullvm", 758 | "windows_x86_64_msvc", 759 | ] 760 | 761 | [[package]] 762 | name = "windows_aarch64_gnullvm" 763 | version = "0.48.5" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 766 | 767 | [[package]] 768 | name = "windows_aarch64_msvc" 769 | version = "0.48.5" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 772 | 773 | [[package]] 774 | name = "windows_i686_gnu" 775 | version = "0.48.5" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 778 | 779 | [[package]] 780 | name = "windows_i686_msvc" 781 | version = "0.48.5" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 784 | 785 | [[package]] 786 | name = "windows_x86_64_gnu" 787 | version = "0.48.5" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 790 | 791 | [[package]] 792 | name = "windows_x86_64_gnullvm" 793 | version = "0.48.5" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 796 | 797 | [[package]] 798 | name = "windows_x86_64_msvc" 799 | version = "0.48.5" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 802 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kdotool" 3 | version = "0.2.1" 4 | description = "A xdotool-like tool to manipulate windows on KDE Wayland" 5 | authors = ["Jin Liu "] 6 | repository = "https://github.com/jinliu/kdotool" 7 | license = "Apache-2.0" 8 | keywords = ["xdotool", "wayland", "kde"] 9 | categories = ["command-line-utilities"] 10 | edition = "2021" 11 | 12 | [profile.release] 13 | strip = true # Automatically strip symbols from the binary. 14 | opt-level = "z" # Optimize for size. 15 | lto = true 16 | codegen-units = 1 17 | 18 | [dependencies] 19 | anyhow = "1.0.75" 20 | chrono = "0.4.31" 21 | dbus = "0.9.7" 22 | env_logger = "0.10.1" 23 | handlebars = "5.1.2" 24 | lexopt = "0.3.0" 25 | log = "0.4.20" 26 | phf = { version = "0.11.2", features = ["macros"] } 27 | serde = { version = "1.0.193", features = ["derive"] } 28 | serde_json = "1.0.108" 29 | tempfile = "3.8.1" 30 | 31 | [dev-dependencies.cargo-husky] 32 | version = "1" 33 | default-features = true # Disable features which are enabled by default 34 | features = ["precommit-hook", "run-cargo-check", "run-cargo-clippy", "run-cargo-fmt"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kdotool - a `xdotool` clone for KDE Wayland 2 | 3 | ## Introduction 4 | 5 | Wayland, for security concerns, removed most of the X11 APIs that 6 | [xdotool](https://github.com/jordansissel/xdotool) uses to simulate 7 | user input and control windows. [ydotool](https://github.com/ReimuNotMoe/ydotool) 8 | solves the input part by talking directly to the kernel input device. However, 9 | for the window control part, you have to use each Wayland compositor's own APIs. 10 | 11 | This program uses KWin's scripting API to control windows. In each invocation, 12 | it generates a KWin script on-the-fly, loads it into KWin, runs it, and then 13 | deletes it, using KWin's DBus interface. 14 | 15 | This program should work with both KDE 5 and the upcoming KDE 6. It should work 16 | with both Wayland and X11 sessions. (But you can use the original `xdotool` in 17 | X11, anyway. So this is mainly for Wayland.) 18 | 19 | Not all `xdotool` commands are supported. Some are not available through the KWin 20 | API. Some might be not even possible in Wayland. See below for details. 21 | 22 | Please note that the `window id` this program uses is KWin's internal window id, 23 | which looks like a UUID (`{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`). It's not 24 | a X11 window id. 25 | 26 | ## Global Options 27 | 28 | - `--help` Show help. 29 | - `--version` Show version. 30 | 31 | Options not in xdotool: 32 | 33 | - `--dry-run` Just print the generated KWin script. Don't run it. 34 | - `--debug` Print debug messages. 35 | - `--shortcut _shortcut_` Specify a shortcut to run the generated KWin script. 36 | The shortcut must be in the format of `modifier+key`, e.g. `Alt+Shift+X`. 37 | The shortcut will be registered in KWin. The script is not run immediately. 38 | You must press the shortcut to run it. 39 | - `--name _name_` Specify a name for the shortcut, So you can remove it 40 | later with `--remove`. This option is only valid with `--shortcut`. 41 | - --`remove _name_` Remove a previously registered shortcut. 42 | 43 | ## New Commands Not In xdotool 44 | 45 | The following can be used in chained commands: 46 | 47 | - `savewindowstack _name_` Save the current window stack to a variable 48 | - `loadwindowstack _name_` Load a previously saved window stack 49 | - `getwindowid` Print the window id of a window in the window stack 50 | 51 | ## Supported xdotool Commands 52 | 53 | ### Window Queries 54 | 55 | These commands generate a window stack that following _window action_ commands can refer to. 56 | 57 | - `search` 58 | - MISSING: 59 | - `--maxdepth` 60 | - `--onlyvisible` 61 | - `--sync` 62 | - NOTE: 63 | - `--screen` (KDE 5 only) 64 | - `getactivewindow` 65 | - `getmouselocation [--shell]` 66 | - Window stack contains the topmost window under the mouse pointer. 67 | 68 | ### Window Actions 69 | 70 | These commands either take a window-id argument, or use the window stack. 71 | 72 | - `getwindowname` 73 | - `getwindowclassname` 74 | - `getwindowpid` 75 | - `getwindowgeometry` 76 | - MISSING: `--shell` 77 | - NOTE: shows screen number only in KDE 5 78 | - `windowsize` 79 | - MISSING: 80 | - `--usehints` 81 | - `--sync` 82 | - `windowmove` 83 | - MISSING: 84 | - `--sync` 85 | - `windowminimize` 86 | - MISSING: `--sync` 87 | - `windowraise` (KDE 6 only) 88 | - Use `windowactivate` instead? 89 | - `windowactivate` 90 | - MISSING: `--sync` 91 | - windowclose 92 | - `set_desktop_for_window` 93 | - NOTE: use "current_desktop" to refer to the current desktop 94 | - `get_desktop_for_window` 95 | - `windowstate` 96 | - Supported properties: 97 | - above 98 | - below 99 | - skip_taskbar 100 | - skip_pager 101 | - fullscreen 102 | - shaded 103 | - demands_attention 104 | - no_border 105 | - minimized 106 | - MISSING: 107 | - modal 108 | - sticky 109 | - hidden 110 | - maximized_vert 111 | - maximized_horz 112 | 113 | ### Global Actions 114 | 115 | These actions aren't targeting a specific window, but the whole desktop. 116 | 117 | - `set_desktop` 118 | - MISSING: --relative 119 | - `get_desktop` 120 | - `set_num_desktops` (KDE 5 only) 121 | - `get_num_desktops` 122 | 123 | ## Won't support 124 | 125 | You can use `ydotool`, `dotool`, `wtype`, etc. for these: 126 | 127 | - Keyboard commands 128 | - Mouse commands 129 | 130 | KWin doesn't have such functionality: 131 | 132 | - `set_desktop_viewport` 133 | - `get_desktop_viewport` 134 | 135 | X11-specific: 136 | 137 | - `windowreparent` 138 | - `windowmap` 139 | - `windowunmap` 140 | 141 | ## Unclear if we can support 142 | 143 | - behave window action command 144 | - `exec` 145 | - `sleep` 146 | - scripts 147 | 148 | KWin has such functionality, but not exposed to the js API: 149 | 150 | - `selectwindow` 151 | - `windowlower` 152 | - `windowquit` 153 | - `windowkill` 154 | - `getwindowfocus`: use `getactivewindow` instead? 155 | - `windowfocus`: use `windowactivate` instead? 156 | - `set_window` 157 | 158 | ## Troubleshooting 159 | 160 | If anything fails to work, you can re-run the command with `--debug` option. 161 | It will print the generated KWin script, and the output of the script from 162 | KWin. If you think it's a bug, please create an issue in [GitHub](https://github.com/jinliu/kdotool/issues). 163 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | pub fn print_version() { 2 | println!("kdotool v{}", env!("CARGO_PKG_VERSION")); 3 | } 4 | 5 | pub fn help() { 6 | print_version(); 7 | print!( 8 | r#" 9 | kdotool is a xdotool-like window control utility for KDE 5 and 6. 10 | 11 | USAGE: 12 | kdotool [OPTIONS] COMMAND [ARGS] [COMMAND [ARGS]]... 13 | 14 | Options: 15 | -h, --help Show this help 16 | -v, --version Show program version 17 | -d, --debug Enable debug output 18 | -n, --dry-run Don't actually run the script. Just print it to stdout. 19 | 20 | --shortcut SHORTCUT [--name NAME] 21 | Register a shortcut to run the script. 22 | Optionally set a name for the shortcut, so you can remove it later. 23 | 24 | --remove NAME Remove a previously registered shortcut. 25 | 26 | Window Query Commands: 27 | search [OPTIONS] PATTERN 28 | Search for windows with titles, names, or classes matching a regular 29 | expression pattern. 30 | 31 | The default options are --name --class --classname --role (unless you 32 | specify one or more of --name, --class, --classname, or --role). 33 | 34 | OPTIONS: 35 | --class 36 | Match against the window class. 37 | --classname 38 | Match against the window classname. 39 | --role 40 | Match against the window role. 41 | --name 42 | Match against the window name. This is the same string that is 43 | displayed in the window titlebar. 44 | --pid PID 45 | Match windows that belong to a specific process id. This may not 46 | work for some X applications that do not set this metadata on its 47 | windows. 48 | --screen NUMBER (KDE 5 only) 49 | Select windows only on a specific screen. Default is to search all 50 | screens. 51 | --desktop NUMBER 52 | Only match windows on a certain desktop. The default is to search 53 | all desktops. 54 | --limit NUMBER 55 | Stop searching after finding NUMBER matching windows. The default 56 | is no search limit (which is equivalent to '--limit 0') 57 | --all 58 | Require that all conditions be met. 59 | --any 60 | Match windows that match any condition (logically, 'or'). This is 61 | on by default. 62 | 63 | getactivewindow 64 | Select the currently active window. 65 | 66 | getmouselocation [--shell] 67 | Outputs the x, y, screen, and window id of the mouse cursor. 68 | 69 | OPTIONS: 70 | --shell 71 | output shell data you can eval. 72 | 73 | Window Action Commands: 74 | 75 | General Syntax: 76 | COMMAND [OPTIONS] [WINDOW] [ARGS...] 77 | 78 | WINDOW can be specified as: 79 | %N - the Nth window in the stack (result from the previous Window Query 80 | Command) 81 | %@ - all windows in the stack 82 | {{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}} - the window with the given ID 83 | 84 | If not specified, it defaults to %1. I.e. the first result from the 85 | previous window query. 86 | 87 | getwindowname [WINDOW] 88 | Output the name of a window. This is the same string that is displayed 89 | in the window titlebar. 90 | 91 | getwindowclassname [WINDOW] 92 | Output the class name of a window. 93 | 94 | getwindowgeometry [WINDOW] 95 | Output the geometry (location and position) of a window. The values 96 | include: x, y, width, height, and (KDE 5 only) screen number. 97 | 98 | getwindowid [WINDOW] 99 | Output the ID of a window. 100 | 101 | getwindowpid [WINDOW] 102 | Output the PID owning a window. This requires effort from the 103 | application owning a window and may not work for all windows. 104 | 105 | windowactivate [WINDOW] 106 | Activate a window. If the window is on another desktop, we will switch 107 | to that desktop. 108 | 109 | windowraise [WINDOW] (KDE 6 only) 110 | Raise a window to the top of the window stack. 111 | 112 | windowminimize [WINDOW] 113 | Minimize a window. 114 | 115 | windowclose [WINDOW] 116 | Close a window. 117 | 118 | windowsize [WINDOW] WIDTH HEIGHT 119 | Resize a window. Percentages are valid for WIDTH and HEIGHT. They are 120 | relative to the geometry of the screen the window is on. 121 | 122 | If the given WIDTH is literally 'x', then the window's current width 123 | will be unchanged. The same applies for 'y' for HEIGHT. 124 | 125 | windowmove [--relative] [WINDOW] X Y 126 | Move a window. Percentages are valid for X and Y. They are relative to 127 | relative to the geometry of the screen the window is on. 128 | 129 | If the given x coordinate is literally 'x', then the window's current 130 | x position will be unchanged. The same applies for 'y'. 131 | 132 | --relative 133 | Make movement relative to the current window position. 134 | 135 | windowstate [--add PROPERTY] [--remove PROPERTY] [--toggle PROPERTY] [WINDOW] 136 | Change a property on a window. 137 | 138 | PROPERTY can be any of: 139 | 140 | ABOVE - Show window above all others (always on top) 141 | BELOW - Show window below all others 142 | SKIP_TASKBAR - hides the window from the taskbar 143 | SKIP_PAGER - hides the window from the window pager 144 | FULLSCREEN - makes window fullscreen 145 | SHADED - rolls the window up 146 | DEMANDS_ATTENTION - marks window urgent or needing attention 147 | NO_BORDER - window has no border 148 | MINIMIZED - set minimized state, can toggle between minimize or maximize. 149 | 150 | get_desktop_for_window [WINDOW] 151 | Output the desktop number that a window is on. 152 | 153 | set_desktop_for_window [WINDOW] NUMBER 154 | Move a window to a different desktop. 155 | 156 | Global Commands: 157 | get_desktop 158 | Output the current desktop number. 159 | 160 | set_desktop 161 | Change the current desktop to . 162 | 163 | get_num_desktops 164 | Output the current number of desktops. 165 | 166 | set_num_desktops (KDE 5 only) 167 | Change the number of desktops to . 168 | "# 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod templates; 2 | use templates::*; 3 | 4 | mod parser; 5 | use parser::*; 6 | 7 | mod help; 8 | use help::*; 9 | 10 | use std::io::Write; 11 | use std::process::Command; 12 | use std::sync::RwLock; 13 | use std::time::Duration; 14 | 15 | use anyhow::{anyhow, Context}; 16 | use dbus::{ 17 | blocking::{Connection, SyncConnection}, 18 | channel::MatchingReceiver, 19 | message::MatchRule, 20 | }; 21 | use serde::Serialize; 22 | 23 | #[derive(Default, Serialize)] 24 | struct Globals { 25 | dbus_addr: String, 26 | cmdline: String, 27 | debug: bool, 28 | kde5: bool, 29 | marker: String, 30 | script_name: String, 31 | shortcut: String, 32 | } 33 | 34 | struct StepResult { 35 | script: String, 36 | is_query: bool, 37 | next_arg: Option, 38 | } 39 | 40 | static MESSAGES: RwLock> = RwLock::new(vec![]); 41 | 42 | fn add_context(render_context: &mut handlebars::Context, key: &str, value: T) 43 | where 44 | serde_json::Value: From, 45 | { 46 | render_context 47 | .data_mut() 48 | .as_object_mut() 49 | .unwrap() 50 | .insert(key.into(), serde_json::Value::from(value)); 51 | } 52 | 53 | fn generate_script( 54 | globals: &Globals, 55 | mut parser: Parser, 56 | next_arg: &str, 57 | ) -> anyhow::Result { 58 | use lexopt::prelude::*; 59 | 60 | let mut full_script = String::new(); 61 | let mut reg = handlebars::Handlebars::new(); 62 | reg.set_strict_mode(true); 63 | let render_context = handlebars::Context::wraps(globals)?; 64 | 65 | full_script.push_str(®.render_template_with_context(SCRIPT_HEADER, &render_context)?); 66 | 67 | let mut last_step_is_query; 68 | let mut command: String = next_arg.into(); 69 | 70 | loop { 71 | parser = reset_parser(parser)?; 72 | 73 | let step_result = generate_step(&command, &mut parser, ®, &render_context, globals) 74 | .with_context(|| format!("in command '{command}'"))?; 75 | 76 | full_script.push_str(&step_result.script); 77 | last_step_is_query = step_result.is_query; 78 | 79 | if let Some(next_arg) = step_result.next_arg { 80 | command = next_arg; 81 | } else { 82 | match parser.next()? { 83 | Some(Value(val)) => { 84 | command = val.string()?; 85 | } 86 | 87 | None => { 88 | break; 89 | } 90 | 91 | Some(arg) => { 92 | return Err(arg.unexpected().into()); 93 | } 94 | } 95 | } 96 | } 97 | 98 | if last_step_is_query { 99 | full_script.push_str(®.render_template_with_context(STEP_LAST_OUTPUT, &render_context)?); 100 | } 101 | 102 | full_script.push_str(®.render_template_with_context(SCRIPT_FOOTER, &render_context)?); 103 | 104 | Ok(full_script) 105 | } 106 | 107 | fn generate_step( 108 | command: &str, 109 | parser: &mut Parser, 110 | reg: &handlebars::Handlebars, 111 | render_context: &handlebars::Context, 112 | globals: &Globals, 113 | ) -> anyhow::Result { 114 | use lexopt::prelude::*; 115 | 116 | let step_script; 117 | let mut is_query = false; 118 | let mut next_arg = None; 119 | let mut render_context = render_context.clone(); 120 | add_context(&mut render_context, "step_name", command); 121 | 122 | match command { 123 | "search" => { 124 | return step_search(parser, reg, &render_context); 125 | } 126 | 127 | "getactivewindow" => { 128 | step_script = 129 | reg.render_template_with_context(STEP_GETACTIVEWINDOW, &render_context)?; 130 | is_query = true; 131 | } 132 | 133 | "savewindowstack" | "loadwindowstack" => { 134 | let mut arg_name = None; 135 | while let Some(arg) = parser.next()? { 136 | match arg { 137 | Value(val) if arg_name.is_none() => { 138 | arg_name = Some(val.string()?); 139 | } 140 | Value(val) => { 141 | next_arg = Some(val.string()?); 142 | break; 143 | } 144 | _ => { 145 | return Err(arg.unexpected().into()); 146 | } 147 | } 148 | } 149 | let mut render_context = render_context.clone(); 150 | add_context( 151 | &mut render_context, 152 | "name", 153 | arg_name.ok_or(anyhow!("missing argument 'name'"))?.as_str(), 154 | ); 155 | step_script = reg.render_template_with_context( 156 | if command == "savewindowstack" { 157 | STEP_SAVEWINDOWSTACK 158 | } else { 159 | STEP_LOADWINDOWSTACK 160 | }, 161 | &render_context, 162 | )?; 163 | is_query = command == "loadwindowstack"; 164 | } 165 | 166 | _ => { 167 | if WINDOW_ACTIONS.contains_key(command) { 168 | let mut arg_window_id: Option = None; 169 | 170 | let action_script; 171 | match command { 172 | "windowstate" => { 173 | let mut opt_windowstate = String::new(); 174 | 175 | while let Some(arg) = parser.next()? { 176 | match arg { 177 | Long(option) 178 | if option == "add" 179 | || option == "remove" 180 | || option == "toggle" => 181 | { 182 | let option: String = option.into(); 183 | let key = parser.value()?.string()?.to_lowercase(); 184 | if let Some(prop) = WINDOWSTATE_PROPERTIES.get(&key) { 185 | let js = match option.as_str() { 186 | "add" => format!("w.{prop} = true; "), 187 | "remove" => { 188 | format!("w.{prop} = false; ") 189 | } 190 | "toggle" => { 191 | format!("w.{prop} = !w.{prop}; ") 192 | } 193 | _ => unreachable!(), 194 | }; 195 | opt_windowstate.push_str(&js); 196 | } else { 197 | return Err(anyhow!("unsupported property '{key}'")); 198 | } 199 | } 200 | Value(val) if arg_window_id.is_none() => { 201 | let s = val.string()?; 202 | if let Some(id) = to_window_id(&s) { 203 | arg_window_id = Some(id); 204 | } else { 205 | next_arg = Some(s); 206 | break; 207 | } 208 | } 209 | Value(val) => { 210 | next_arg = Some(val.string()?); 211 | break; 212 | } 213 | _ => { 214 | return Err(arg.unexpected().into()); 215 | } 216 | } 217 | } 218 | 219 | let mut render_context = render_context.clone(); 220 | add_context(&mut render_context, "windowstate", opt_windowstate); 221 | action_script = reg.render_template_with_context( 222 | WINDOW_ACTIONS.get(command).unwrap(), 223 | &render_context, 224 | )?; 225 | } 226 | 227 | "windowmove" | "windowsize" => { 228 | let mut opt_relative = false; 229 | let mut arg_x: Option = None; 230 | let mut arg_y: Option = None; 231 | 232 | while let Some(arg) = next_maybe_num(parser)? { 233 | match arg { 234 | Long("relative") if command == "windowmove" => { 235 | opt_relative = true; 236 | } 237 | Value(val) if arg_window_id.is_none() && arg_x.is_none() => { 238 | let s = val.string()?; 239 | if let Some(id) = to_window_id(&s) { 240 | arg_window_id = Some(id); 241 | } else { 242 | arg_x = Some(s); 243 | } 244 | } 245 | Value(val) if arg_x.is_none() => { 246 | arg_x = Some(val.string()?); 247 | } 248 | Value(val) if arg_y.is_none() => { 249 | arg_y = Some(val.string()?); 250 | } 251 | Value(val) => { 252 | next_arg = Some(val.string()?); 253 | break; 254 | } 255 | _ => { 256 | return Err(arg.unexpected().into()); 257 | } 258 | } 259 | } 260 | 261 | let mut x = String::new(); 262 | let mut y = String::new(); 263 | let mut x_percent = String::new(); 264 | let mut y_percent = String::new(); 265 | 266 | if let Some(arg) = arg_x { 267 | if arg != "x" { 268 | if arg.ends_with('%') { 269 | let s = arg.strip_suffix('%').unwrap(); 270 | _ = s.parse::()?; 271 | x_percent = s.into(); 272 | } else { 273 | _ = arg.parse::()?; 274 | x = arg; 275 | } 276 | } 277 | } else { 278 | return Err(anyhow!("missing argument 'x'")); 279 | } 280 | 281 | if let Some(arg) = arg_y { 282 | if arg != "y" { 283 | if arg.ends_with('%') { 284 | let s = arg.strip_suffix('%').unwrap(); 285 | _ = s.parse::()?; 286 | y_percent = s.into(); 287 | } else { 288 | _ = arg.parse::()?; 289 | y = arg; 290 | } 291 | } 292 | } else { 293 | return Err(anyhow!("missing argument 'y'")); 294 | } 295 | 296 | let mut render_context = render_context.clone(); 297 | add_context(&mut render_context, "relative", opt_relative); 298 | add_context(&mut render_context, "x", x); 299 | add_context(&mut render_context, "y", y); 300 | add_context(&mut render_context, "x_percent", x_percent); 301 | add_context(&mut render_context, "y_percent", y_percent); 302 | action_script = reg.render_template_with_context( 303 | WINDOW_ACTIONS.get(command).unwrap(), 304 | &render_context, 305 | )?; 306 | } 307 | 308 | "set_desktop_for_window" => { 309 | let mut arg_desktop_id: Option = None; 310 | while let Some(arg) = next_maybe_num(parser)? { 311 | match arg { 312 | Value(val) 313 | if arg_window_id.is_none() && arg_desktop_id.is_none() => 314 | { 315 | let s = val.string()?; 316 | if let Some(id) = to_window_id(&s) { 317 | arg_window_id = Some(id); 318 | } else { 319 | arg_desktop_id = Some(s); 320 | } 321 | } 322 | Value(val) if arg_desktop_id.is_none() => { 323 | arg_desktop_id = Some(val.string()?); 324 | } 325 | Value(val) => { 326 | next_arg = Some(val.string()?); 327 | break; 328 | } 329 | _ => { 330 | return Err(arg.unexpected().into()); 331 | } 332 | } 333 | } 334 | let desktop_id = match arg_desktop_id { 335 | Some(id) => { 336 | if let Ok(n) = id.parse::() { 337 | if n >= 0 { 338 | n 339 | } else { 340 | return Err(anyhow!("invalid desktop id '{id}'")); 341 | } 342 | } else if id.to_lowercase() == "current_desktop" { 343 | -1 344 | } else { 345 | return Err(anyhow!("invalid desktop id '{id}'")); 346 | } 347 | } 348 | None => return Err(anyhow!("missing argument 'desktop_id'")), 349 | }; 350 | let mut render_context = render_context.clone(); 351 | add_context(&mut render_context, "desktop_id", desktop_id); 352 | action_script = reg.render_template_with_context( 353 | WINDOW_ACTIONS.get(command).unwrap(), 354 | &render_context, 355 | )?; 356 | } 357 | 358 | _ => { 359 | while let Some(arg) = next_maybe_num(parser)? { 360 | match arg { 361 | Value(val) if arg_window_id.is_none() => { 362 | let s = val.string()?; 363 | if let Some(id) = to_window_id(&s) { 364 | arg_window_id = Some(id); 365 | } else { 366 | next_arg = Some(s); 367 | break; 368 | } 369 | } 370 | Value(val) => { 371 | next_arg = Some(val.string()?); 372 | break; 373 | } 374 | _ => { 375 | return Err(arg.unexpected().into()); 376 | } 377 | } 378 | } 379 | action_script = reg.render_template_with_context( 380 | WINDOW_ACTIONS.get(command).unwrap(), 381 | &render_context, 382 | )?; 383 | } 384 | }; 385 | 386 | let window_id = arg_window_id.unwrap_or("%1".into()); 387 | let mut render_context = render_context.clone(); 388 | add_context(&mut render_context, "action", action_script); 389 | 390 | if window_id == "%@" { 391 | step_script = reg 392 | .render_template_with_context(STEP_ACTION_ON_STACK_ALL, &render_context)?; 393 | } else if let Some(s) = window_id.strip_prefix('%') { 394 | let index = s.parse::()?; 395 | let mut render_context = render_context.clone(); 396 | add_context(&mut render_context, "item_index", index); 397 | step_script = reg 398 | .render_template_with_context(STEP_ACTION_ON_STACK_ITEM, &render_context)?; 399 | } else { 400 | let mut render_context = render_context.clone(); 401 | add_context(&mut render_context, "window_id", window_id); 402 | step_script = reg 403 | .render_template_with_context(STEP_ACTION_ON_WINDOW_ID, &render_context)?; 404 | } 405 | } else if GLOBAL_ACTIONS.contains_key(command.as_ref()) { 406 | let action_script; 407 | match command { 408 | "set_desktop" | "set_num_desktops" => { 409 | let mut arg_n: Option = None; 410 | while let Some(arg) = next_maybe_num(parser)? { 411 | match arg { 412 | Value(val) if arg_n.is_none() => { 413 | arg_n = Some(val.parse()?); 414 | } 415 | Value(val) => { 416 | next_arg = Some(val.string()?); 417 | break; 418 | } 419 | _ => { 420 | return Err(arg.unexpected().into()); 421 | } 422 | } 423 | } 424 | 425 | if let Some(n) = arg_n { 426 | let mut render_context = render_context.clone(); 427 | add_context(&mut render_context, "n", n); 428 | action_script = reg.render_template_with_context( 429 | GLOBAL_ACTIONS.get(command).unwrap(), 430 | &render_context, 431 | )?; 432 | } else if command == "set_desktop" { 433 | return Err(anyhow!("missing argument 'desktop_id'")); 434 | } else { 435 | return Err(anyhow!("missing argument 'num'")); 436 | } 437 | } 438 | 439 | "getmouselocation" => { 440 | if globals.kde5 { 441 | return Err(anyhow!("'getmouselocation' is not supported in KDE 5")); 442 | } 443 | 444 | let mut opt_shell = false; 445 | while let Some(arg) = next_maybe_num(parser)? { 446 | match arg { 447 | Long("shell") => { 448 | opt_shell = true; 449 | } 450 | Value(val) => { 451 | next_arg = Some(val.string()?); 452 | break; 453 | } 454 | _ => { 455 | return Err(arg.unexpected().into()); 456 | } 457 | } 458 | } 459 | add_context(&mut render_context, "shell", opt_shell); 460 | action_script = reg.render_template_with_context( 461 | GLOBAL_ACTIONS.get(command).unwrap(), 462 | &render_context, 463 | )?; 464 | } 465 | 466 | _ => { 467 | action_script = reg.render_template_with_context( 468 | GLOBAL_ACTIONS.get(command).unwrap(), 469 | &render_context, 470 | )?; 471 | } 472 | }; 473 | 474 | let mut render_context = render_context.clone(); 475 | add_context(&mut render_context, "action", action_script); 476 | step_script = 477 | reg.render_template_with_context(STEP_GLOBAL_ACTION, &render_context)?; 478 | } else { 479 | return Err(anyhow!("Unknown command: {command}")); 480 | } 481 | } 482 | } 483 | 484 | Ok(StepResult { 485 | script: step_script, 486 | is_query, 487 | next_arg, 488 | }) 489 | } 490 | 491 | fn step_search( 492 | parser: &mut Parser, 493 | reg: &handlebars::Handlebars, 494 | render_context: &handlebars::Context, 495 | ) -> anyhow::Result { 496 | use lexopt::prelude::*; 497 | 498 | #[derive(Default, Serialize)] 499 | struct Options { 500 | debug: bool, 501 | kde5: bool, 502 | match_class: bool, 503 | match_classname: bool, 504 | match_role: bool, 505 | match_name: bool, 506 | match_pid: bool, 507 | pid: i32, 508 | match_desktop: bool, 509 | desktop: i32, 510 | match_screen: bool, 511 | screen: i32, 512 | limit: u32, 513 | match_all: bool, 514 | search_term: String, 515 | } 516 | 517 | let mut opt = Options { 518 | debug: render_context 519 | .data() 520 | .as_object() 521 | .unwrap() 522 | .get("debug") 523 | .unwrap() 524 | .as_bool() 525 | .unwrap(), 526 | kde5: render_context 527 | .data() 528 | .as_object() 529 | .unwrap() 530 | .get("debug") 531 | .unwrap() 532 | .as_bool() 533 | .unwrap(), 534 | ..Default::default() 535 | }; 536 | 537 | let mut next_arg = None; 538 | while let Some(arg) = parser.next()? { 539 | match arg { 540 | Long("class") => { 541 | opt.match_class = true; 542 | } 543 | Long("classname") => { 544 | opt.match_classname = true; 545 | } 546 | Long("role") => { 547 | opt.match_role = true; 548 | } 549 | Long("name") => { 550 | opt.match_name = true; 551 | } 552 | Long("pid") => { 553 | opt.match_pid = true; 554 | opt.pid = parser.value()?.parse()?; 555 | } 556 | Long("desktop") => { 557 | opt.match_desktop = true; 558 | opt.desktop = parser.value()?.parse()?; 559 | } 560 | Long("screen") => { 561 | opt.match_screen = true; 562 | opt.screen = parser.value()?.parse()?; 563 | } 564 | Long("limit") => { 565 | opt.limit = parser.value()?.parse()?; 566 | } 567 | Long("all") => { 568 | opt.match_all = true; 569 | } 570 | Long("any") => { 571 | opt.match_all = false; 572 | } 573 | Value(val) if opt.search_term.is_empty() => { 574 | opt.search_term = val.string()?; 575 | } 576 | Value(val) => { 577 | next_arg = Some(val.string()?); 578 | break; 579 | } 580 | _ => { 581 | return Err(arg.unexpected().into()); 582 | } 583 | } 584 | } 585 | if !(opt.match_class || opt.match_classname || opt.match_role || opt.match_name) { 586 | opt.match_class = true; 587 | opt.match_classname = true; 588 | opt.match_role = true; 589 | opt.match_name = true; 590 | } 591 | let render_context = handlebars::Context::wraps(opt)?; 592 | Ok(StepResult { 593 | script: reg.render_template_with_context(STEP_SEARCH, &render_context)?, 594 | is_query: true, 595 | next_arg, 596 | }) 597 | } 598 | 599 | fn main() -> anyhow::Result<()> { 600 | let mut context = Globals { 601 | cmdline: std::env::args().collect::>().join(" "), 602 | ..Default::default() 603 | }; 604 | 605 | let mut parser = Parser::from_env(); 606 | 607 | if let Ok(version) = std::env::var("KDE_SESSION_VERSION") { 608 | if version == "5" { 609 | context.kde5 = true; 610 | } 611 | } 612 | 613 | // Parse global options 614 | let mut next_arg: Option = None; 615 | let mut opt_help = false; 616 | let mut opt_version = false; 617 | let mut opt_dry_run = false; 618 | let mut opt_remove = false; 619 | 620 | while let Some(arg) = parser.next()? { 621 | use lexopt::prelude::*; 622 | match arg { 623 | Short('h') | Long("help") => { 624 | opt_help = true; 625 | } 626 | Short('v') | Long("version") => { 627 | opt_version = true; 628 | } 629 | Short('d') | Long("debug") => { 630 | context.debug = true; 631 | } 632 | Short('n') | Long("dry-run") => { 633 | opt_dry_run = true; 634 | } 635 | Long("shortcut") => { 636 | context.shortcut = parser.value()?.string()?; 637 | } 638 | Long("name") => { 639 | context.script_name = parser.value()?.string()?; 640 | } 641 | Long("remove") => { 642 | opt_remove = true; 643 | context.script_name = parser.value()?.string()?; 644 | } 645 | Value(os_string) => { 646 | next_arg = Some(os_string.string()?); 647 | break; 648 | } 649 | _ => { 650 | return Err(arg.unexpected().into()); 651 | } 652 | } 653 | } 654 | 655 | if !opt_remove && next_arg.is_none() || opt_help { 656 | help(); 657 | return Ok(()); 658 | } 659 | 660 | if opt_version { 661 | print_version(); 662 | return Ok(()); 663 | } 664 | 665 | env_logger::Builder::from_default_env() 666 | .filter( 667 | Some("kdotool"), 668 | if context.debug { 669 | log::LevelFilter::Debug 670 | } else { 671 | log::LevelFilter::Info 672 | }, 673 | ) 674 | .init(); 675 | 676 | let kwin_conn = Connection::new_session()?; 677 | let kwin_proxy = 678 | kwin_conn.with_proxy("org.kde.KWin", "/Scripting", Duration::from_millis(5000)); 679 | 680 | if opt_remove { 681 | let _: () = kwin_proxy.method_call( 682 | "org.kde.kwin.Scripting", 683 | "unloadScript", 684 | (&context.script_name,), 685 | )?; 686 | return Ok(()); 687 | } 688 | 689 | let self_conn = SyncConnection::new_session()?; 690 | context.dbus_addr = self_conn.unique_name().to_string(); 691 | 692 | log::debug!("===== Generate KWin script ====="); 693 | let mut script_file = tempfile::NamedTempFile::with_prefix("kdotool-")?; 694 | context.marker = script_file 695 | .path() 696 | .file_name() 697 | .unwrap() 698 | .to_string_lossy() 699 | .into(); 700 | if context.script_name.is_empty() { 701 | context.script_name.clone_from(&context.marker); 702 | } 703 | 704 | let script_contents = generate_script(&context, parser, &next_arg.unwrap())?; 705 | 706 | log::debug!("Script:{script_contents}"); 707 | script_file.write_all(script_contents.as_bytes())?; 708 | let script_file_path = script_file.into_temp_path(); 709 | 710 | if opt_dry_run { 711 | println!("{}", script_contents.trim()); 712 | return Ok(()); 713 | } 714 | 715 | log::debug!("===== Load script into KWin ====="); 716 | let script_id: i32; 717 | (script_id,) = kwin_proxy.method_call( 718 | "org.kde.kwin.Scripting", 719 | "loadScript", 720 | (script_file_path.to_str().unwrap(), &context.script_name), 721 | )?; 722 | if script_id < 0 { 723 | return Err(anyhow!("Failed to load script. A script with the same name may already exist. Please use `--remove` to remove it first.")); 724 | } 725 | 726 | log::debug!("Script ID: {script_id}"); 727 | log::debug!("Script name: {}", context.script_name); 728 | 729 | log::debug!("===== Run script ====="); 730 | let script_proxy = kwin_conn.with_proxy( 731 | "org.kde.KWin", 732 | if context.kde5 { 733 | format!("/{script_id}") 734 | } else { 735 | format!("/Scripting/Script{script_id}") 736 | }, 737 | Duration::from_millis(5000), 738 | ); 739 | 740 | // setup message receiver 741 | let _receiver_thread = std::thread::spawn(move || { 742 | let _receiver = self_conn.start_receive( 743 | MatchRule::new_method_call(), 744 | Box::new(|message, _connection| -> bool { 745 | log::debug!("dbus message: {:?}", message); 746 | if let Some(member) = message.member() { 747 | if let Some(arg) = message.get1() { 748 | let mut messages = MESSAGES.write().unwrap(); 749 | messages.push((member.to_string(), arg)); 750 | } 751 | } 752 | true 753 | }), 754 | ); 755 | loop { 756 | self_conn.process(Duration::from_millis(1000)).unwrap(); 757 | } 758 | //FIXME: shut down this thread when the script is finished 759 | }); 760 | 761 | let start_time = chrono::Local::now(); 762 | let _: () = script_proxy.method_call("org.kde.kwin.Script", "run", ())?; 763 | if context.shortcut.is_empty() { 764 | let _: () = script_proxy.method_call("org.kde.kwin.Script", "stop", ())?; 765 | } 766 | 767 | if context.debug { 768 | if let Ok(journal) = Command::new("journalctl") 769 | .arg(format!( 770 | "--since={}", 771 | start_time.format("%Y-%m-%d %H:%M:%S") 772 | )) 773 | .arg("--user") 774 | .arg("--user-unit=plasma-kwin_wayland.service") 775 | .arg("--user-unit=plasma-kwin_x11.service") 776 | .arg("QT_CATEGORY=js") 777 | .arg("QT_CATEGORY=kwin_scripting") 778 | .arg("--output=cat") 779 | .output() 780 | { 781 | let output = String::from_utf8(journal.stdout)?; 782 | log::debug!("KWin log from the systemd journal:\n{}", output.trim_end()); 783 | } else { 784 | log::debug!("Failed getting KWin log from the systemd journal."); 785 | } 786 | } 787 | 788 | log::debug!("===== Output ====="); 789 | let messages = MESSAGES.read().unwrap(); 790 | for (msgtype, message) in messages.iter() { 791 | if msgtype == "result" { 792 | println!("{message}"); 793 | } else if msgtype == "error" { 794 | eprintln!("ERROR: {message}"); 795 | } else { 796 | println!("{msgtype}: {message}"); 797 | } 798 | } 799 | 800 | if !context.shortcut.is_empty() { 801 | println!("Shortcut registered: {}", context.shortcut); 802 | println!("Script ID: {script_id}"); 803 | println!("Script name: {}", context.script_name); 804 | } 805 | 806 | Ok(()) 807 | } 808 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | pub use lexopt::Parser; 2 | 3 | // Reset the parser at the current position, but with a new context. 4 | pub fn reset_parser(mut parser: Parser) -> anyhow::Result { 5 | Ok(lexopt::Parser::from_args(parser.raw_args()?)) 6 | } 7 | 8 | pub fn next_maybe_num(parser: &mut Parser) -> anyhow::Result> { 9 | if let Some(number) = try_get_number(parser) { 10 | Ok(Some(lexopt::Arg::Value(number.into()))) 11 | } else { 12 | Ok(parser.next()?) 13 | } 14 | } 15 | 16 | pub fn try_get_number(parser: &mut Parser) -> Option { 17 | let mut raw = parser.try_raw_args()?; 18 | let arg = raw.peek()?.to_str()?; 19 | if arg.starts_with('-') && arg[1..].starts_with(|c: char| c.is_ascii_digit()) { 20 | raw.next() 21 | .map(|os_string| os_string.to_string_lossy().into()) 22 | } else { 23 | None 24 | } 25 | } 26 | 27 | #[allow(dead_code)] 28 | pub fn positional(parser: &mut Parser, name: &str) -> anyhow::Result 29 | where 30 | T::Err: std::error::Error + Send + Sync + 'static, 31 | Result: From::Err>>, 32 | { 33 | if let Some(os_string) = parser.raw_args()?.next() { 34 | Ok(os_string.to_string_lossy().parse::()?) 35 | } else { 36 | Err(anyhow::Error::msg(format!( 37 | "missing positional argument '{name}'" 38 | ))) 39 | } 40 | } 41 | 42 | pub fn to_window_id(s: &str) -> Option { 43 | if s.starts_with('%') || s.starts_with('{') { 44 | Some(s.into()) 45 | } else { 46 | None 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/templates.rs: -------------------------------------------------------------------------------- 1 | pub const SCRIPT_HEADER: &str = r#" 2 | {{#if debug}} 3 | print("{{{marker}}} START"); 4 | {{/if}} 5 | 6 | function output_debug(message) { 7 | {{#if debug}} 8 | print("{{{marker}}} DEBUG", message); 9 | callDBus("{{{dbus_addr}}}", "/", "", "debug", message.toString()); 10 | {{/if}} 11 | } 12 | 13 | function output_error(message) { 14 | print("{{{marker}}} ERROR", message); 15 | callDBus("{{{dbus_addr}}}", "/", "", "error", message.toString()); 16 | } 17 | 18 | function output_result(message) { 19 | if (message == null) { 20 | message = "null"; 21 | } 22 | {{#if debug}} 23 | print("{{{marker}}} RESULT", message); 24 | {{/if}} 25 | callDBus("{{{dbus_addr}}}", "/", "", "result", message.toString()); 26 | } 27 | 28 | {{#if kde5}} 29 | workspace_windowList = () => workspace.clientList(); 30 | workspace_activeWindow = () => workspace.activeClient; 31 | workspace_setActiveWindow = (window) => { workspace.activeClient = window; }; 32 | workspace_raiseWindow = (window) => { output_error("`windowraise` unsupported in KDE 5"); }; 33 | workspace_currentDesktop = () => workspace.currentDesktop; 34 | workspace_setCurrentDesktop = (desktop) => { workspace.currentDesktop = desktop; }; 35 | workspace_numDesktops = () => workspace.desktops; 36 | workspace_setNumDesktops = (n) => { workspace.desktops = n }; 37 | window_x11DesktopIds = (window) => window.x11DesktopIds; 38 | window_setX11DesktopId = (window, id) => { window.desktop = id; }; 39 | window_screen = (window) => window.screen; 40 | {{else}} 41 | workspace_windowList = () => workspace.windowList(); 42 | workspace_activeWindow = () => workspace.activeWindow; 43 | workspace_setActiveWindow = (window) => { workspace.activeWindow = window; }; 44 | workspace_raiseWindow = (window) => { workspace.raiseWindow(window); }; 45 | workspace_currentDesktop = () => workspace.currentDesktop.x11DesktopNumber; 46 | workspace_setCurrentDesktop = (id) => { 47 | let d = workspace.desktops.find((d) => d.x11DesktopNumber == id); 48 | if (d) { 49 | workspace.currentDesktop = d; 50 | } else { 51 | output_error(`Invalid desktop number ${id}`); 52 | } 53 | }; 54 | workspace_numDesktops = () => workspace.desktops.length; 55 | workspace_setNumDesktops = (n) => { output_error("`set_num_desktops` unsupported in KDE 6"); }; 56 | window_x11DesktopIds = (window) => window.desktops.map((d) => d.x11DesktopNumber); 57 | window_setX11DesktopId = (window, id) => { 58 | if (id < 0) { 59 | window.desktops = [workspace.currentDesktop]; 60 | } else { 61 | let d = workspace.desktops.find((d) => d.x11DesktopNumber == id); 62 | if (d) { 63 | window.desktops = [d]; 64 | } else { 65 | output_error(`Invalid desktop number ${id}`); 66 | } 67 | } 68 | }; 69 | window_screen = (window) => { output_error("`search --screen` unsupported in KDE 6"); }; 70 | {{/if}} 71 | 72 | function run() { 73 | var window_stack = []; 74 | "#; 75 | 76 | pub const SCRIPT_FOOTER: &str = r#" 77 | } 78 | 79 | {{#if shortcut}} 80 | registerShortcut("{{#if script_name}}{{{script_name}}}{{else}}{{{marker}}}{{/if}}", "{{#if script_name}}{{{script_name}}}{{else}}{{{cmdline}}}{{/if}}", "{{{shortcut}}}", run); 81 | {{else}} 82 | run(); 83 | {{/if}} 84 | 85 | {{#if debug}} 86 | print("{{{marker}}} FINISH"); 87 | {{/if}} 88 | "#; 89 | 90 | pub const STEP_SEARCH: &str = r#" 91 | output_debug("STEP search {{{search_term}}}") 92 | const re = new RegExp(String.raw`{{{search_term}}}`, "i"); 93 | var t = workspace_windowList(); 94 | window_stack = []; 95 | for (var i=0; i= 0 101 | {{/if}} 102 | {{#if match_classname}} 103 | {{#if match_all}}&&{{else}}||{{/if}} 104 | w.resourceName.search(re) >= 0 105 | {{/if}} 106 | {{#if match_role}} 107 | {{#if match_all}}&&{{else}}||{{/if}} 108 | w.windowRole.search(re) >= 0 109 | {{/if}} 110 | {{#if match_name}} 111 | {{#if match_all}}&&{{else}}||{{/if}} 112 | w.caption.search(re) >= 0 113 | {{/if}} 114 | {{#if match_pid}} 115 | {{#if match_all}}&&{{else}}||{{/if}} 116 | w.pid == {{{pid}}} 117 | {{/if}} 118 | ) { 119 | {{#if match_desktop}} 120 | if (window_x11DesktopIds(w).indexOf({{{desktop}}}) < 0) continue; 121 | {{/if}} 122 | {{#if match_screen}} 123 | if (window_screen(w) != {{{screen}}}) continue; 124 | {{/if}} 125 | window_stack.push(w); 126 | if ({{{limit}}} > 0 && window_stack.length >= {{{limit}}}) { 127 | break; 128 | } 129 | } 130 | } 131 | "#; 132 | 133 | pub const STEP_GETACTIVEWINDOW: &str = r#" 134 | output_debug("STEP getactivewindow") 135 | var window_stack = [workspace_activeWindow()]; 136 | "#; 137 | 138 | pub const STEP_SAVEWINDOWSTACK: &str = r#" 139 | output_debug("STEP savewindowstack") 140 | var window_stack_{{{name}}} = window_stack; 141 | "#; 142 | 143 | pub const STEP_LOADWINDOWSTACK: &str = r#" 144 | output_debug("STEP loadwindowstack") 145 | var window_stack = window_stack_{{{name}}}; 146 | "#; 147 | 148 | pub const STEP_ACTION_ON_WINDOW_ID: &str = r#" 149 | output_debug("STEP {{{step_name}}}") 150 | var t = workspace_windowList(); 151 | for (var i=0; i 0) { 163 | if ({{{item_index}}} > window_stack.length || {{{item_index}}} < 1) { 164 | output_error("Invalid window stack selection '{{{item_index}}}' (out of range)"); 165 | } else { 166 | let w = window_stack[{{{item_index}}}-1]; 167 | {{{action}}} 168 | } 169 | } 170 | "#; 171 | 172 | pub const STEP_ACTION_ON_STACK_ALL: &str = r#" 173 | output_debug("STEP {{{step_name}}}") 174 | for (var i=0; i = phf::phf_map! { 187 | "getwindowname" => "output_result(w.caption);", 188 | "getwindowclassname" => "output_result(w.resourceClass);", 189 | "getwindowgeometry" => "output_result(`Window ${w.internalId}`); output_result(` Position: ${w.x},${w.y}{{#if kde5}} (screen: ${window_screen(w)}){{/if}}`); output_result(` Geometry: ${w.width}x${w.height}`);", 190 | "getwindowid" => "output_result(w.internalId);", 191 | "getwindowpid" => "output_result(w.pid);", 192 | "windowminimize" => "w.minimized = true;", 193 | "windowraise" => "workspace_raiseWindow(w);", 194 | "windowclose" => "w.closeWindow();", 195 | "windowactivate" => "workspace_setActiveWindow(w);", 196 | "windowsize" => r#" 197 | output_debug(`Window: ${w.frameGeometry}`); 198 | output_debug(`Screen: ${workspace.virtualScreenSize}`); 199 | let q = Object.assign({}, w.frameGeometry); 200 | {{#if x_percent}}q.width=workspace.virtualScreenSize.width*{{{x_percent}}}/100;{{/if}} 201 | {{#if y_percent}}q.height=workspace.virtualScreenSize.height*{{{y_percent}}}/100;{{/if}} 202 | {{#if x}}q.width={{{x}}};{{/if}} 203 | {{#if y}}q.height={{{y}}};{{/if}} 204 | w.frameGeometry = q; 205 | "#, 206 | "windowmove" => r#" 207 | output_debug(`Window: ${w.frameGeometry}`); 208 | output_debug(`Screen: ${workspace.virtualScreenSize}`); 209 | {{#if x_percent}}w.frameGeometry.x={{#if relative}}w.x+{{/if}}workspace.virtualScreenSize.width*{{{x_percent}}}/100;{{/if}} 210 | {{#if y_percent}}w.frameGeometry.y={{#if relative}}w.y+{{/if}}workspace.virtualScreenSize.height*{{{y_percent}}}/100;{{/if}} 211 | {{#if x}}w.frameGeometry.x={{#if relative}}w.x+{{/if}}{{{x}}};{{/if}} 212 | {{#if y}}w.frameGeometry.y={{#if relative}}w.y+{{/if}}{{{y}}};{{/if}} 213 | "#, 214 | "windowstate" => "{{{windowstate}}}", 215 | "get_desktop_for_window"=> "output_result(window_x11DesktopIds(w)[0]);", 216 | "set_desktop_for_window"=> "window_setX11DesktopId(w, {{{desktop_id}}})", 217 | }; 218 | 219 | pub const WINDOWSTATE_PROPERTIES: phf::Map<&'static str, &'static str> = phf::phf_map! { 220 | "above" => "keepAbove", 221 | "below" => "keepBelow", 222 | "skip_taskbar" => "skipTaskbar", 223 | "skip_pager" => "skipPager", 224 | "fullscreen" => "fullScreen", 225 | "shaded" => "shade", 226 | "demands_attention" => "demandsAttention", 227 | "no_border" => "noBorder", 228 | "minimized" => "minimized", 229 | }; 230 | 231 | pub const STEP_GLOBAL_ACTION: &str = r#" 232 | output_debug("STEP {{{step_name}}}") 233 | {{{action}}} 234 | "#; 235 | 236 | pub const GLOBAL_ACTIONS: phf::Map<&'static str, &'static str> = phf::phf_map! { 237 | "get_desktop" => "output_result(workspace_currentDesktop());", 238 | "set_desktop" => "workspace_setCurrentDesktop({{{n}}});", 239 | "get_num_desktops" => "output_result(workspace_numDesktops());", 240 | "set_num_desktops" => "workspace_setNumDesktops({{{n}}})", 241 | "getmouselocation" => r#" 242 | let p = workspace.cursorPos; 243 | let screen = workspace.screenAt(p); 244 | let screen_id = workspace.screens.indexOf(screen); 245 | let window_list = workspace.windowAt(p); 246 | let window_id = ""; 247 | window_stack = []; 248 | if (window_list.length > 0) { 249 | window_id = window_list[0].internalId; 250 | window_stack.push(window_list[0]); 251 | } 252 | {{#if shell}} 253 | output_result("X="+p.x); 254 | output_result("Y="+p.y); 255 | output_result("SCREEN="+screen_id); 256 | output_result("WINDOW="+window_id); 257 | {{else}} 258 | output_result(`x:${p.x} y:${p.y} screen:${screen_id} window:${window_id}`); 259 | {{/if}} 260 | "#, 261 | }; 262 | --------------------------------------------------------------------------------