├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix └── src ├── error.rs ├── main.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | 4 | **/*.rs.bk 5 | 6 | /result 7 | -------------------------------------------------------------------------------- /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 = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "bitflags" 16 | version = "1.3.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 19 | 20 | [[package]] 21 | name = "cc" 22 | version = "1.0.77" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "clap" 34 | version = "4.0.29" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" 37 | dependencies = [ 38 | "bitflags", 39 | "clap_derive", 40 | "clap_lex", 41 | "is-terminal", 42 | "once_cell", 43 | "strsim", 44 | "termcolor", 45 | ] 46 | 47 | [[package]] 48 | name = "clap_derive" 49 | version = "4.0.21" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" 52 | dependencies = [ 53 | "heck", 54 | "proc-macro-error", 55 | "proc-macro2", 56 | "quote", 57 | "syn", 58 | ] 59 | 60 | [[package]] 61 | name = "clap_lex" 62 | version = "0.3.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" 65 | dependencies = [ 66 | "os_str_bytes", 67 | ] 68 | 69 | [[package]] 70 | name = "doc-comment" 71 | version = "0.3.3" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 74 | 75 | [[package]] 76 | name = "env_logger" 77 | version = "0.10.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 80 | dependencies = [ 81 | "humantime", 82 | "is-terminal", 83 | "log", 84 | "regex", 85 | "termcolor", 86 | ] 87 | 88 | [[package]] 89 | name = "errno" 90 | version = "0.2.8" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 93 | dependencies = [ 94 | "errno-dragonfly", 95 | "libc", 96 | "winapi", 97 | ] 98 | 99 | [[package]] 100 | name = "errno-dragonfly" 101 | version = "0.1.2" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 104 | dependencies = [ 105 | "cc", 106 | "libc", 107 | ] 108 | 109 | [[package]] 110 | name = "heck" 111 | version = "0.4.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 114 | 115 | [[package]] 116 | name = "hermit-abi" 117 | version = "0.2.6" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 120 | dependencies = [ 121 | "libc", 122 | ] 123 | 124 | [[package]] 125 | name = "humantime" 126 | version = "2.1.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 129 | 130 | [[package]] 131 | name = "io-lifetimes" 132 | version = "1.0.3" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 135 | dependencies = [ 136 | "libc", 137 | "windows-sys", 138 | ] 139 | 140 | [[package]] 141 | name = "is-terminal" 142 | version = "0.4.1" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" 145 | dependencies = [ 146 | "hermit-abi", 147 | "io-lifetimes", 148 | "rustix", 149 | "windows-sys", 150 | ] 151 | 152 | [[package]] 153 | name = "itoa" 154 | version = "1.0.4" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 157 | 158 | [[package]] 159 | name = "libc" 160 | version = "0.2.137" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 163 | 164 | [[package]] 165 | name = "linux-raw-sys" 166 | version = "0.1.3" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" 169 | 170 | [[package]] 171 | name = "log" 172 | version = "0.4.17" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 175 | dependencies = [ 176 | "cfg-if", 177 | ] 178 | 179 | [[package]] 180 | name = "memchr" 181 | version = "2.5.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 184 | 185 | [[package]] 186 | name = "once_cell" 187 | version = "1.16.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 190 | 191 | [[package]] 192 | name = "os_str_bytes" 193 | version = "6.4.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 196 | 197 | [[package]] 198 | name = "proc-macro-error" 199 | version = "1.0.4" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 202 | dependencies = [ 203 | "proc-macro-error-attr", 204 | "proc-macro2", 205 | "quote", 206 | "syn", 207 | "version_check", 208 | ] 209 | 210 | [[package]] 211 | name = "proc-macro-error-attr" 212 | version = "1.0.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 215 | dependencies = [ 216 | "proc-macro2", 217 | "quote", 218 | "version_check", 219 | ] 220 | 221 | [[package]] 222 | name = "proc-macro2" 223 | version = "1.0.47" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 226 | dependencies = [ 227 | "unicode-ident", 228 | ] 229 | 230 | [[package]] 231 | name = "quote" 232 | version = "1.0.21" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 235 | dependencies = [ 236 | "proc-macro2", 237 | ] 238 | 239 | [[package]] 240 | name = "regex" 241 | version = "1.7.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 244 | dependencies = [ 245 | "aho-corasick", 246 | "memchr", 247 | "regex-syntax", 248 | ] 249 | 250 | [[package]] 251 | name = "regex-syntax" 252 | version = "0.6.28" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 255 | 256 | [[package]] 257 | name = "rustix" 258 | version = "0.36.4" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" 261 | dependencies = [ 262 | "bitflags", 263 | "errno", 264 | "io-lifetimes", 265 | "libc", 266 | "linux-raw-sys", 267 | "windows-sys", 268 | ] 269 | 270 | [[package]] 271 | name = "ryu" 272 | version = "1.0.11" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 275 | 276 | [[package]] 277 | name = "serde" 278 | version = "1.0.148" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" 281 | dependencies = [ 282 | "serde_derive", 283 | ] 284 | 285 | [[package]] 286 | name = "serde_derive" 287 | version = "1.0.148" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" 290 | dependencies = [ 291 | "proc-macro2", 292 | "quote", 293 | "syn", 294 | ] 295 | 296 | [[package]] 297 | name = "serde_json" 298 | version = "1.0.89" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" 301 | dependencies = [ 302 | "itoa", 303 | "ryu", 304 | "serde", 305 | ] 306 | 307 | [[package]] 308 | name = "snafu" 309 | version = "0.7.4" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "cb0656e7e3ffb70f6c39b3c2a86332bb74aa3c679da781642590f3c1118c5045" 312 | dependencies = [ 313 | "doc-comment", 314 | "snafu-derive", 315 | ] 316 | 317 | [[package]] 318 | name = "snafu-derive" 319 | version = "0.7.4" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "475b3bbe5245c26f2d8a6f62d67c1f30eb9fffeccee721c45d162c3ebbdf81b2" 322 | dependencies = [ 323 | "heck", 324 | "proc-macro2", 325 | "quote", 326 | "syn", 327 | ] 328 | 329 | [[package]] 330 | name = "strsim" 331 | version = "0.10.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 334 | 335 | [[package]] 336 | name = "swayipc" 337 | version = "3.0.1" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "ab1dcff328b223d85d7ca767b2e4aadbc13dad550d36b4c6b929b9ad4d26ee9a" 340 | dependencies = [ 341 | "serde", 342 | "serde_json", 343 | "swayipc-types", 344 | ] 345 | 346 | [[package]] 347 | name = "swayipc-types" 348 | version = "1.3.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "44b43b4059d825ccc04adf9726f944d0e3aa20938f4cff3b5c6b53198afcd6b3" 351 | dependencies = [ 352 | "serde", 353 | "serde_json", 354 | "thiserror", 355 | ] 356 | 357 | [[package]] 358 | name = "swayws" 359 | version = "1.3.0" 360 | dependencies = [ 361 | "clap", 362 | "env_logger", 363 | "log", 364 | "snafu", 365 | "swayipc", 366 | ] 367 | 368 | [[package]] 369 | name = "syn" 370 | version = "1.0.105" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 373 | dependencies = [ 374 | "proc-macro2", 375 | "quote", 376 | "unicode-ident", 377 | ] 378 | 379 | [[package]] 380 | name = "termcolor" 381 | version = "1.1.3" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 384 | dependencies = [ 385 | "winapi-util", 386 | ] 387 | 388 | [[package]] 389 | name = "thiserror" 390 | version = "1.0.37" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 393 | dependencies = [ 394 | "thiserror-impl", 395 | ] 396 | 397 | [[package]] 398 | name = "thiserror-impl" 399 | version = "1.0.37" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 402 | dependencies = [ 403 | "proc-macro2", 404 | "quote", 405 | "syn", 406 | ] 407 | 408 | [[package]] 409 | name = "unicode-ident" 410 | version = "1.0.5" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 413 | 414 | [[package]] 415 | name = "version_check" 416 | version = "0.9.4" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 419 | 420 | [[package]] 421 | name = "winapi" 422 | version = "0.3.9" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 425 | dependencies = [ 426 | "winapi-i686-pc-windows-gnu", 427 | "winapi-x86_64-pc-windows-gnu", 428 | ] 429 | 430 | [[package]] 431 | name = "winapi-i686-pc-windows-gnu" 432 | version = "0.4.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 435 | 436 | [[package]] 437 | name = "winapi-util" 438 | version = "0.1.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 441 | dependencies = [ 442 | "winapi", 443 | ] 444 | 445 | [[package]] 446 | name = "winapi-x86_64-pc-windows-gnu" 447 | version = "0.4.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 450 | 451 | [[package]] 452 | name = "windows-sys" 453 | version = "0.42.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 456 | dependencies = [ 457 | "windows_aarch64_gnullvm", 458 | "windows_aarch64_msvc", 459 | "windows_i686_gnu", 460 | "windows_i686_msvc", 461 | "windows_x86_64_gnu", 462 | "windows_x86_64_gnullvm", 463 | "windows_x86_64_msvc", 464 | ] 465 | 466 | [[package]] 467 | name = "windows_aarch64_gnullvm" 468 | version = "0.42.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 471 | 472 | [[package]] 473 | name = "windows_aarch64_msvc" 474 | version = "0.42.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 477 | 478 | [[package]] 479 | name = "windows_i686_gnu" 480 | version = "0.42.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 483 | 484 | [[package]] 485 | name = "windows_i686_msvc" 486 | version = "0.42.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 489 | 490 | [[package]] 491 | name = "windows_x86_64_gnu" 492 | version = "0.42.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 495 | 496 | [[package]] 497 | name = "windows_x86_64_gnullvm" 498 | version = "0.42.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 501 | 502 | [[package]] 503 | name = "windows_x86_64_msvc" 504 | version = "0.42.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 507 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swayws" 3 | version = "1.3.0" 4 | authors = ["Hendrik Wolff "] 5 | license = "MIT" 6 | description = "SwayWs allows easy moving of workspaces to and from outputs" 7 | repository = "https://gitlab.com/w0lff/swayws" 8 | edition = "2018" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | clap = { version = "4.0", features = ["derive"] } 13 | env_logger = "0.10.0" 14 | log = "0.4.17" 15 | snafu = "0.7.4" 16 | swayipc = "3.0" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hendrik Wolff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwayWs 2 | A tool which allows easy moving of workspaces to and from outputs 3 | 4 | Developed for use with the [Sway](https://swaywm.org/) compositor 5 | 6 | ## Features 7 | - Move numeric ranges of workspaces at once 8 | - When a moved workspace is focused, it will always be opened at the specified output 9 | - The focus is returned by default to the workspace that was focused before SwayWs was invoked 10 | - The previously visible workspaces are visible again by default after moving one or more workspaces 11 | 12 | ## Usage 13 | 14 | ``` 15 | SwayWs allows easy moving of workspaces to and from outputs 16 | 17 | Usage: swayws 18 | 19 | Commands: 20 | focus Focus a workspace 21 | list Lists infos about sway 22 | move Moves a workspace to a specified output 23 | range Moves a range of workspaces to a specified output 24 | swap Swaps two workspaces with each other 25 | ``` 26 | 27 | ### `swayws m[ove]` 28 | ``` 29 | Moves a workspace to a specified output 30 | 31 | Usage: swayws move [OPTIONS] 32 | 33 | Arguments: 34 | Workspace to move 35 | Name of the output 36 | 37 | Options: 38 | -a, --away Moves workspace to output that does not match the specified output name 39 | --not Excludes outputs to move workspace to, must be used with --away 40 | -f, --focus Focuses specified workspace after moving it 41 | ``` 42 | 43 | ### `swayws r[ange]` 44 | ``` 45 | Moves a range of workspaces to a specified output 46 | 47 | Usage: swayws range [OPTIONS] 48 | 49 | Arguments: 50 | First workspace in range 51 | Last workspace in range 52 | Name of the output 53 | 54 | Options: 55 | -a, --away Moves workspace to output that does not match the specified output name 56 | --not Excludes outputs to move workspace to, must be used with --away 57 | -n, --numeric Assumes and are numbers and binds all workspaces in between them to the specified output 58 | ``` 59 | 60 | ### `swayws s[wap]` 61 | ``` 62 | Swaps two workspaces with each other 63 | 64 | Usage: swayws swap 65 | ``` 66 | 67 | ## Examples 68 | ```sh 69 | swayws move 1 eDP-1 70 | 71 | swayws range 11 20 DP-3 72 | swayws range 11 20 eDP-1 --away 73 | swayws range 11 20 eDP-1 --away --not DP-3 --not DP-5 74 | 75 | swayws list 76 | 77 | swayws focus 1 78 | 79 | swayws swap 4 17 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1698166613, 11 | "narHash": "sha256-y4rdN4flxRiROqNi1waMYIZj/Fs7L2OrszFk/1ry9vU=", 12 | "owner": "ipetkov", 13 | "repo": "crane", 14 | "rev": "b7db46f0f1751f7b1d1911f6be7daf568ad5bc65", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "ipetkov", 19 | "repo": "crane", 20 | "type": "github" 21 | } 22 | }, 23 | "fenix": { 24 | "inputs": { 25 | "nixpkgs": [ 26 | "nixpkgs" 27 | ], 28 | "rust-analyzer-src": "rust-analyzer-src" 29 | }, 30 | "locked": { 31 | "lastModified": 1698906096, 32 | "narHash": "sha256-xXsjyprR3xhv7V+0Tf4IDFEbGafwAvoK40900UYLkDY=", 33 | "owner": "nix-community", 34 | "repo": "fenix", 35 | "rev": "2634e2ffc5501957922285a87a83e430d8bc29cb", 36 | "type": "github" 37 | }, 38 | "original": { 39 | "owner": "nix-community", 40 | "repo": "fenix", 41 | "type": "github" 42 | } 43 | }, 44 | "flake-utils": { 45 | "inputs": { 46 | "systems": "systems" 47 | }, 48 | "locked": { 49 | "lastModified": 1694529238, 50 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 51 | "owner": "numtide", 52 | "repo": "flake-utils", 53 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "numtide", 58 | "repo": "flake-utils", 59 | "type": "github" 60 | } 61 | }, 62 | "nixpkgs": { 63 | "locked": { 64 | "lastModified": 1698890957, 65 | "narHash": "sha256-DJ+SppjpPBoJr0Aro9TAcP3sxApCSieY6BYBCoWGUX8=", 66 | "owner": "NixOS", 67 | "repo": "nixpkgs", 68 | "rev": "c082856b850ec60cda9f0a0db2bc7bd8900d708c", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "NixOS", 73 | "ref": "nixpkgs-unstable", 74 | "repo": "nixpkgs", 75 | "type": "github" 76 | } 77 | }, 78 | "root": { 79 | "inputs": { 80 | "crane": "crane", 81 | "fenix": "fenix", 82 | "flake-utils": "flake-utils", 83 | "nixpkgs": "nixpkgs" 84 | } 85 | }, 86 | "rust-analyzer-src": { 87 | "flake": false, 88 | "locked": { 89 | "lastModified": 1698762780, 90 | "narHash": "sha256-WzuwMjpitp41dacdNzrdGjjP72Z0fFyGuQR2PJk48pE=", 91 | "owner": "rust-lang", 92 | "repo": "rust-analyzer", 93 | "rev": "99e94d2938a743f8f48c6b729de4c517eeced99d", 94 | "type": "github" 95 | }, 96 | "original": { 97 | "owner": "rust-lang", 98 | "ref": "nightly", 99 | "repo": "rust-analyzer", 100 | "type": "github" 101 | } 102 | }, 103 | "systems": { 104 | "locked": { 105 | "lastModified": 1681028828, 106 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "nix-systems", 114 | "repo": "default", 115 | "type": "github" 116 | } 117 | } 118 | }, 119 | "root": "root", 120 | "version": 7 121 | } 122 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | crane.url = "github:ipetkov/crane"; 5 | crane.inputs.nixpkgs.follows = "nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | fenix = { 8 | url = "github:nix-community/fenix"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, crane, flake-utils, fenix, ... }: 14 | flake-utils.lib.eachDefaultSystem 15 | (system: 16 | let 17 | pkgs = import nixpkgs { 18 | inherit system; 19 | }; 20 | 21 | fenixChannel = fenix.packages.${system}.stable; 22 | fenixToolchain = (fenixChannel.withComponents [ 23 | "rustc" 24 | "cargo" 25 | "rustfmt" 26 | "clippy" 27 | "rust-analysis" 28 | "rust-src" 29 | "llvm-tools-preview" 30 | ]); 31 | craneLib = crane.lib.${system}.overrideToolchain fenixToolchain; 32 | 33 | commonArgs = { 34 | src = craneLib.cleanCargoSource ./.; 35 | doCheck = false; 36 | cargoVendorDir = craneLib.vendorCargoDeps { cargoLock = ./Cargo.lock; }; 37 | }; 38 | cargoArtifacts = craneLib.buildDepsOnly (commonArgs); 39 | swayws = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; }); 40 | in 41 | rec 42 | { 43 | packages = { 44 | default = swayws; 45 | swayws = swayws; 46 | }; 47 | 48 | devShells.default = pkgs.mkShell { 49 | nativeBuildInputs = (with packages.swayws; nativeBuildInputs ++ buildInputs) ++ [ fenixToolchain ]; 50 | RUST_SRC_PATH = "${fenixChannel.rust-src}/lib/rustlib/src/rust/library"; 51 | }; 52 | }) // { 53 | overlays.default = (final: prev: { 54 | inherit (self.packages.${prev.system}) 55 | swayws; 56 | }); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use snafu::prelude::*; 2 | use snafu::Location; 3 | 4 | #[derive(Debug, Snafu)] 5 | #[snafu(context(suffix(Ctx)))] 6 | #[snafu(visibility(pub))] 7 | pub enum SwayWsError { 8 | #[snafu(display("[{location}] Error while communicating with sway"))] 9 | SwayIpc { 10 | source: swayipc::Error, 11 | location: Location, 12 | }, 13 | 14 | #[snafu(display("[{location}] Cannot parse integer"))] 15 | Parse { 16 | source: std::num::ParseIntError, 17 | location: Location, 18 | }, 19 | 20 | #[snafu(display("[{location}] No output can be matched against the specified parameters"))] 21 | NoOutputMatched { location: Location }, 22 | } 23 | 24 | pub(crate) fn report(error: &dyn snafu::Error) -> String { 25 | let sources = snafu::ChainCompat::new(error); 26 | let sources: Vec<&dyn snafu::Error> = sources.collect(); 27 | let sources = sources.iter().rev(); 28 | let mut s = String::new(); 29 | for (i, source) in sources.enumerate() { 30 | s = match i { 31 | 0 => format!("{source}"), 32 | _ => format!("{source} ({s})"), 33 | } 34 | } 35 | s 36 | } 37 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use clap::{Parser, Subcommand}; 4 | use snafu::prelude::*; 5 | use swayipc::Connection; 6 | use swayipc::Workspace; 7 | 8 | mod error; 9 | mod util; 10 | use error::*; 11 | use util::*; 12 | 13 | /// SwayWs 14 | /// allows easy moving of workspaces to and from outputs 15 | #[derive(Debug, Parser)] 16 | #[clap(name = "swayws", version)] 17 | struct SwayWs { 18 | #[clap(subcommand)] 19 | cmd: Command, 20 | } 21 | 22 | #[derive(Debug, Subcommand)] 23 | enum Command { 24 | /// Focus a workspace 25 | #[clap(alias = "f")] 26 | Focus { 27 | /// Workspace to focus 28 | workspace: String, 29 | }, 30 | /// Lists infos about sway 31 | #[clap(alias = "l")] 32 | List { 33 | // todo: add options to list first and last entry 34 | /// List outputs 35 | #[clap(short, long)] 36 | outputs: bool, 37 | 38 | /// List workspaces 39 | #[clap(short, long)] 40 | workspaces: bool, 41 | }, 42 | /// Moves a workspace to a specified output 43 | #[clap(alias = "m")] 44 | Move { 45 | /// Moves workspace to output that does not match the specified output name 46 | #[clap(short, long)] 47 | away: bool, 48 | /// Excludes outputs to move workspace to, 49 | /// must be used with --away 50 | #[clap(long, requires("away"))] 51 | not: Option>, 52 | /// Focuses specified workspace after moving it 53 | #[clap(short, long)] 54 | focus: bool, 55 | 56 | /// Workspace to move 57 | workspace: String, 58 | 59 | /// Name of the output 60 | output: String, 61 | }, 62 | /// Moves a range of workspaces to a specified output 63 | #[clap(alias = "r")] 64 | Range { 65 | /// Moves workspace to output that does not match the specified output name 66 | #[clap(short, long)] 67 | away: bool, 68 | /// Excludes outputs to move workspace to, 69 | /// must be used with --away 70 | #[clap(long, requires("away"))] 71 | not: Option>, 72 | 73 | /// Assumes and are numbers and binds all workspaces in between them to the specified output 74 | #[clap(short, long)] 75 | numeric: bool, 76 | 77 | /// First workspace in range 78 | start: String, 79 | /// Last workspace in range 80 | end: String, 81 | 82 | /// Name of the output 83 | output: String, 84 | }, 85 | /// Swaps two workspaces with each other 86 | #[clap(alias = "s")] 87 | Swap { 88 | #[clap(value_name = "WORKSPACE", hide = true)] 89 | ws_l: String, 90 | #[clap(value_name = "WORKSPACE", hide = true)] 91 | ws_r: String, 92 | }, 93 | } 94 | 95 | fn main() { 96 | env_logger::builder().format_timestamp(None).init(); 97 | 98 | if let Err(err) = run() { 99 | log::error!("{}", report(&err)); 100 | } 101 | } 102 | 103 | fn run() -> Result<(), SwayWsError> { 104 | let opt: SwayWs = SwayWs::parse(); 105 | 106 | let mut connection = Connection::new().context(SwayIpcCtx)?; 107 | 108 | let workspaces = connection.get_workspaces().context(SwayIpcCtx)?; 109 | 110 | let mut previously_visible_workspaces: Vec = vec![]; 111 | let mut previously_focused_workspace: Option = None; 112 | let mut restore_visible_workspaces: bool = true; 113 | 114 | // Store the currently visible and focused workspaces 115 | // Note: n > 0 workspaces can be visible but only 1 workspace can be focused 116 | for ws in workspaces.into_iter() { 117 | if ws.visible { 118 | previously_visible_workspaces.push(ws.name.clone()); 119 | } 120 | if ws.focused { 121 | previously_focused_workspace = Some(ws.name); 122 | } 123 | } 124 | 125 | match opt.cmd { 126 | Command::Focus { workspace } => { 127 | cmd_focus(&mut connection, &workspace)?; 128 | restore_visible_workspaces = false; 129 | } 130 | Command::Move { 131 | away, 132 | not, 133 | focus, 134 | workspace, 135 | output, 136 | } => { 137 | cmd_move(&mut connection, &output, &workspace, &away, ¬)?; 138 | if focus { 139 | restore_visible_workspaces = false; 140 | } 141 | } 142 | Command::Range { 143 | away, 144 | not, 145 | numeric, 146 | start, 147 | end, 148 | output, 149 | } => cmd_range(&mut connection, &output, &start, &end, &away, &numeric, not)?, 150 | Command::List { 151 | workspaces, 152 | outputs, 153 | } => { 154 | cmd_list(&mut connection, outputs, workspaces)?; 155 | restore_visible_workspaces = false; 156 | } 157 | Command::Swap { ws_l, ws_r } => { 158 | cmd_swap(&mut connection, ws_l.clone(), ws_r.clone())?; 159 | if previously_focused_workspace == Some(ws_l.clone()) { 160 | previously_focused_workspace = Some(ws_r) 161 | } else if previously_focused_workspace == Some(ws_r) { 162 | previously_focused_workspace = Some(ws_l) 163 | } 164 | } 165 | } 166 | 167 | // Make the same workspaces visible again that were visible before rearranging the 168 | // workspace-to-output mapping and focus the previously focused workspace 169 | if restore_visible_workspaces { 170 | // First, visit all previously visible workspaces by focusing them 171 | for ws_name in previously_visible_workspaces { 172 | focus_workspace(&mut connection, &ws_name)?; 173 | } 174 | // At last, focus the saved workspace 175 | if let Some(ws_name) = previously_focused_workspace { 176 | focus_workspace(&mut connection, &ws_name)?; 177 | } 178 | } 179 | 180 | Ok(()) 181 | } 182 | 183 | fn cmd_focus(connection: &mut Connection, workspace: &str) -> Result<(), SwayWsError> { 184 | focus_workspace(connection, workspace) 185 | } 186 | 187 | fn cmd_list( 188 | connection: &mut Connection, 189 | outputs: bool, 190 | workspaces: bool, 191 | ) -> Result<(), SwayWsError> { 192 | if outputs { 193 | print_outputs(connection)?; 194 | } 195 | if workspaces { 196 | print_workspaces(connection)?; 197 | } 198 | if !outputs && !workspaces { 199 | print_outputs(connection)?; 200 | print_workspaces(connection)?; 201 | } 202 | Ok(()) 203 | } 204 | 205 | fn cmd_move( 206 | connection: &mut Connection, 207 | output_name: &str, 208 | workspace: &str, 209 | away: &bool, 210 | not: &Option>, 211 | ) -> Result<(), SwayWsError> { 212 | if *away { 213 | let second_output = match not { 214 | None => get_second_output(connection, &[output_name.into()])?, 215 | Some(not_list) => { 216 | let mut list = vec![output_name.into()]; 217 | list.append(&mut not_list.clone()); 218 | 219 | get_second_output(connection, &list)? 220 | } 221 | }; 222 | move_workspace_to_output(connection, workspace, &second_output.name)?; 223 | } else { 224 | move_workspace_to_output(connection, workspace, output_name)?; 225 | } 226 | Ok(()) 227 | } 228 | 229 | fn cmd_range( 230 | connection: &mut Connection, 231 | output_name: &str, 232 | start: &str, 233 | end: &str, 234 | away: &bool, 235 | numeric: &bool, 236 | not: Option>, 237 | ) -> Result<(), SwayWsError> { 238 | if *numeric { 239 | let start_i: i32 = i32::from_str(start).context(ParseCtx)?; 240 | let end_i: i32 = i32::from_str(end).context(ParseCtx)?; 241 | 242 | for i in start_i..=end_i { 243 | cmd_move(connection, output_name, &i.to_string(), away, ¬)?; 244 | } 245 | 246 | return Ok(()); 247 | } 248 | 249 | let mut ws_list: Vec = vec![]; 250 | let mut fill_ws_list: bool = false; 251 | 252 | // collect workspaces between start and end in a vector 253 | let workspaces: Vec = connection.get_workspaces().context(SwayIpcCtx)?; 254 | for ws in workspaces.into_iter() { 255 | if start.cmp(&ws.name).is_eq() { 256 | fill_ws_list = true; 257 | } 258 | if fill_ws_list { 259 | ws_list.push(ws.name.clone()); 260 | } 261 | if end.cmp(&ws.name).is_eq() { 262 | fill_ws_list = false; 263 | } 264 | } 265 | 266 | for ws in ws_list.into_iter() { 267 | cmd_move(connection, output_name, &ws, away, ¬)?; 268 | } 269 | Ok(()) 270 | } 271 | 272 | fn cmd_swap(connection: &mut Connection, ws_l: String, ws_r: String) -> Result<(), SwayWsError> { 273 | let tmp = "swayws-swap"; 274 | let o_l = connection 275 | .get_workspaces() 276 | .context(SwayIpcCtx)? 277 | .iter() 278 | .find(|ws| ws.name == ws_l) 279 | .cloned(); 280 | let o_r = connection 281 | .get_workspaces() 282 | .context(SwayIpcCtx)? 283 | .iter() 284 | .find(|ws| ws.name == ws_r) 285 | .cloned(); 286 | 287 | rename_workspace(connection, &ws_l, tmp)?; 288 | rename_workspace(connection, &ws_r, &ws_l)?; 289 | rename_workspace(connection, tmp, &ws_r)?; 290 | 291 | if let Some(o_l) = o_l { 292 | move_workspace_to_output(connection, &ws_l, &o_l.output)?; 293 | } 294 | if let Some(o_r) = o_r { 295 | move_workspace_to_output(connection, &ws_r, &o_r.output)?; 296 | } 297 | 298 | Ok(()) 299 | } 300 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use snafu::prelude::*; 2 | use swayipc::Connection; 3 | use swayipc::Workspace; 4 | 5 | use crate::error::SwayWsError; 6 | use crate::error::*; 7 | 8 | pub fn focus_workspace( 9 | connection: &mut Connection, 10 | workspace_name: &str, 11 | ) -> Result<(), SwayWsError> { 12 | let command_text = format!("workspace {}", workspace_name); 13 | send_ipc_command(connection, &command_text) 14 | } 15 | 16 | pub fn move_workspace_to_output( 17 | connection: &mut Connection, 18 | workspace_name: &str, 19 | output_name: &str, 20 | ) -> Result<(), SwayWsError> { 21 | let command_text = format!( 22 | "workspace {0} output {1},\ 23 | workspace {0},\ 24 | move workspace to {1}", 25 | workspace_name, output_name 26 | ); 27 | send_ipc_command(connection, &command_text) 28 | } 29 | 30 | pub fn rename_workspace( 31 | connection: &mut Connection, 32 | from: &str, 33 | to: &str, 34 | ) -> Result<(), SwayWsError> { 35 | let command_text = format!("rename workspace {from} to {to}"); 36 | send_ipc_command(connection, &command_text) 37 | } 38 | 39 | pub fn send_ipc_command( 40 | connection: &mut Connection, 41 | command_text: &str, 42 | ) -> Result<(), SwayWsError> { 43 | let outcomes: Result<(), swayipc::Error> = connection 44 | .run_command(command_text) 45 | .context(SwayIpcCtx)? 46 | .into_iter() 47 | .collect(); 48 | outcomes.context(SwayIpcCtx) 49 | } 50 | 51 | pub fn print_outputs(connection: &mut Connection) -> Result<(), SwayWsError> { 52 | let outputs = connection.get_outputs().context(SwayIpcCtx)?; 53 | println!("Outputs (name):"); 54 | for monitor in outputs.into_iter() { 55 | println!("{}", monitor.name); 56 | } 57 | Ok(()) 58 | } 59 | 60 | pub fn print_workspaces(connection: &mut Connection) -> Result<(), SwayWsError> { 61 | let workspaces: Vec = connection.get_workspaces().context(SwayIpcCtx)?; 62 | println!("Workspaces (id, name):"); 63 | let fill = match workspaces.last() { 64 | Some(ws) => ws.num.to_string().len(), 65 | None => 1, 66 | }; 67 | for ws in workspaces.into_iter() { 68 | println!("{0:>width$} {1:>width$}", ws.num, ws.name, width = fill); 69 | } 70 | Ok(()) 71 | } 72 | 73 | pub fn get_second_output( 74 | connection: &mut Connection, 75 | output_names: &[String], 76 | ) -> Result { 77 | let outputs = connection.get_outputs().context(SwayIpcCtx)?; 78 | if outputs.len() == 1 { 79 | return NoOutputMatchedCtx.fail(); 80 | } 81 | outputs 82 | .into_iter() 83 | .find(|monitor| is_not_in_list(&monitor.name, output_names)) 84 | .ok_or(NoOutputMatchedCtx.build()) 85 | } 86 | 87 | pub fn is_not_in_list(v: &V, list: &[V]) -> bool { 88 | for value in list.iter() { 89 | if *value == *v { 90 | return false; 91 | } 92 | } 93 | true 94 | } 95 | --------------------------------------------------------------------------------