Compare commits

..

87 Commits

Author SHA1 Message Date
Taeyeon Mori cce54affe7 Add sequence_timeout to README 2 years ago
Taeyeon Mori fbb824f83e Add timeout to focus change sequences (v3) 2 years ago
Taeyeon Mori fc1f0a58a0 Add focus.sequence_timeout config option 2 years ago
Tassilo Horn 3dfb962654 Release swayr-0.19.0 2 years ago
Tassilo Horn 6aa7d05d07 New command switch-to-matching-or-urgent-or-lru-window 2 years ago
Tassilo Horn f0b0f71e38 Release swayrbar-0.2.2 3 years ago
Tassilo Horn 7ac8990341 Update docs 3 years ago
Tassilo Horn 6b87e5e51d swayrbar: Add -c/--config-file command line option 3 years ago
Tassilo Horn 90f43c0ec9 Fix another typo 3 years ago
Tassilo Horn efb9f43496 Fix typo 3 years ago
Tassilo Horn 1088c95092 Release swayrbar v0.2.1 3 years ago
Tassilo Horn 3605214639 Some refactorings and improvements 3 years ago
Tassilo Horn 8f5e3fc3ed Update deps & release swayr 0.18.0 3 years ago
Tassilo Horn 23ca1f7f2f Minor doc adjustments 3 years ago
William Barsse 5682d3dadc Release 0.18.0 3 years ago
Tassilo Horn d7aa1d856c Cargo format 3 years ago
William Barsse 33f85b576c Add a "lock-in" delay to LRU window order 3 years ago
William Barsse 559bf941e5 Add a "nop" command 3 years ago
Tassilo Horn f484eb0420 further refactor 3 years ago
Tassilo Horn 55521a9c97 Use a channel for both periodic and click/sway event refresh requests 3 years ago
Tassilo Horn 7d5f1643c1 Use a VecDeque for events so that we're not missing some 3 years ago
Tassilo Horn 56c816ee62 window module: cache formatted text 3 years ago
Tassilo Horn 167b753363 Refresh window module state only when requested specifically 3 years ago
William Barsse 613b4eeaeb Use "daemon" instead of "demon" 3 years ago
Tassilo Horn 8f06f4c196 Refresh window mod on window/workspace events; release swayrbar-0.2.0 3 years ago
Tassilo Horn 039743a29a update deps 3 years ago
Tassilo Horn a98bfc16ec Update TODO 3 years ago
Tassilo Horn 02be0a83de Update README 3 years ago
Tassilo Horn 2d57b48974 Update README 3 years ago
Tassilo Horn a9ba59acd2 Update README 3 years ago
Tassilo Horn 0d4e9430b3 Update README 3 years ago
Tassilo Horn fe16a2a658 Release swayrbar-0.1.1 3 years ago
Tassilo Horn 8afb54cec9 Only refresh module which was the click receiver 3 years ago
Tassilo Horn 913235a51c Release swayrbar 0.1.0 3 years ago
Tassilo Horn c8a3bdbf8e Update README 3 years ago
Tassilo Horn f02c8bc8ca Add pactl (pulseaudio volume) module 3 years ago
Tassilo Horn 93677b195b Release swayrbar 0.0.3 3 years ago
Tassilo Horn 7550585867 Do a quick refresh after handling a click event leading to an action 3 years ago
Tassilo Horn 8545c24502 Update README 3 years ago
Tassilo Horn f95cb58060 Add swayrbar screenshot 3 years ago
Tassilo Horn 6b95202f3d Improve sysinfo format specs 3 years ago
Tassilo Horn a1d3cb21de Move swayr version badge & add one for swayrbar 3 years ago
Tassilo Horn 25ca74f2be Create symlinks for README.md 3 years ago
Tassilo Horn faa04788cc swayrbar: Add a description 3 years ago
Tassilo Horn 3cf075fbbf Release swayr 0.17.0 & swayrbar 0.0.1 3 years ago
Tassilo Horn 59b5da7bdf Mode README; enable tests in builds 3 years ago
Tassilo Horn 471a5190a2 Fix breakage in macro created by renaming 3 years ago
Tassilo Horn b6ee541008 More swayrbar docs 3 years ago
Tassilo Horn 1399e306b5 Fix README 3 years ago
Tassilo Horn 6745321eeb Some renaming 3 years ago
Tassilo Horn e15386ad8f Improve window module 3 years ago
Tassilo Horn 3a3f16554c Improve battery module 3 years ago
Tassilo Horn 96201e5fba Improve sysinfo module 3 years ago
Tassilo Horn 2b9d4bf198 Placeholder substitutions for clicks 3 years ago
Tassilo Horn 59664d3b21 Write docs for swayrbar 3 years ago
Tassilo Horn 591915e0ed Improve window module 3 years ago
Tassilo Horn 35c49cba40 Improve README.md 3 years ago
Tassilo Horn 1cb4b5cc42 Allow substitutions in on_click commands 3 years ago
Tassilo Horn 8d37fd95d1 Improve swayrbar configuration 3 years ago
Tassilo Horn 5e80619761 swayrbar config 3 years ago
Tassilo Horn a2e1b3343c Flatten swayrbar structure 3 years ago
Tassilo Horn 80a8a3a262 Split out common config code 3 years ago
Tassilo Horn 6c6d237164 Rename fmt_replace => format_placeholders; fix tests 3 years ago
Tassilo Horn 6e10aa4c24 Restructure the shared parts of the lib 3 years ago
Tassilo Horn 7035268413 Delete unneeded name() method from BarModuleFn trait 3 years ago
Tassilo Horn 726d9a0a9e Clicks work! 3 years ago
Tassilo Horn 24f5929dd8 update TODO 3 years ago
Tassilo Horn 20760c898d Refactor: workspace with separate swayr & swayrbar crates 3 years ago
Tassilo Horn 2f36e073a3 Finally, we can read click events 3 years ago
Tassilo Horn 4d4937d292 More towards handling click events 3 years ago
Tassilo Horn f92586630d Refactoring towards handling click events 3 years ago
Tassilo Horn 345c91a559 Use refresh_interval from (default) Config; battery module improvements 3 years ago
Tassilo Horn 588b7153fb Release 0.17.0-alpha.0 3 years ago
Tassilo Horn cdb62c0ce8 Config for the swayrbar 3 years ago
Tassilo Horn 160b7d645c Improve window bar module 3 years ago
Tassilo Horn 5d1fbeba06 Battery bar module 3 years ago
Tassilo Horn bf9337571a cpu / mem % for sysinfo module 3 years ago
Tassilo Horn 9dd0a1aa10 More on the sysinfo bar module 3 years ago
Tassilo Horn 37c85880e7 Split basic sway IPC into separate mod; use that from window bar module 3 years ago
Tassilo Horn 59fa701ab5 lazy_static => once_cell 3 years ago
Tassilo Horn b778869ca3 cmds::get_tree => tree::get_root_node 3 years ago
Tassilo Horn 89959253cf Enhance rtfmt 3 years ago
Tassilo Horn c330a35624 Delete bar NodeIter and use the original swayr one 3 years ago
Tassilo Horn 061c3589ac Initial swayrbar impl 3 years ago
Tassilo Horn 6a204e619e Release 0.16.1 3 years ago
Tassilo Horn 6b266902c2 Require regex-1.5.5 because of RUSTSEC-2022-0013 3 years ago
Tassilo Horn eed12e087f update deps 3 years ago
  1. 1
      .builds/arch.yml
  2. 417
      Cargo.lock
  3. 33
      Cargo.toml
  4. 413
      README.md
  5. 9
      TODO
  6. BIN
      misc/swayrbar.png
  7. 292
      src/demon.rs
  8. 103
      src/rtfmt.rs
  9. 598
      src/tree.rs
  10. 23
      swayr/Cargo.toml
  11. 27
      swayr/NEWS.md
  12. 1
      swayr/README.md
  13. 0
      swayr/etc/swayrd.service
  14. 0
      swayr/src/bin/swayr.rs
  15. 2
      swayr/src/bin/swayrd.rs
  16. 0
      swayr/src/client.rs
  17. 379
      swayr/src/cmds.rs
  18. 126
      swayr/src/config.rs
  19. 412
      swayr/src/daemon.rs
  20. 77
      swayr/src/focus.rs
  21. 18
      swayr/src/layout.rs
  22. 14
      swayr/src/lib.rs
  23. 124
      swayr/src/shared/cfg.rs
  24. 254
      swayr/src/shared/fmt.rs
  25. 175
      swayr/src/shared/ipc.rs
  26. 18
      swayr/src/shared/mod.rs
  27. 411
      swayr/src/tree.rs
  28. 23
      swayr/src/util.rs
  29. 26
      swayrbar/Cargo.toml
  30. 15
      swayrbar/NEWS.md
  31. 1
      swayrbar/README.md
  32. 317
      swayrbar/src/bar.rs
  33. 24
      swayrbar/src/bin/swayrbar.rs
  34. 80
      swayrbar/src/config.rs
  35. 19
      swayrbar/src/lib.rs
  36. 79
      swayrbar/src/module.rs
  37. 174
      swayrbar/src/module/battery.rs
  38. 94
      swayrbar/src/module/date.rs
  39. 190
      swayrbar/src/module/pactl.rs
  40. 198
      swayrbar/src/module/sysinfo.rs
  41. 162
      swayrbar/src/module/window.rs
  42. 1
      swayrbar/src/shared

@ -7,4 +7,5 @@ tasks:
- build: | - build: |
cd swayr cd swayr
cargo build cargo build
cargo test
cargo clippy cargo clippy

417
Cargo.lock generated

@ -28,30 +28,66 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "battery"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b624268937c0e0a3edb7c27843f9e547c320d730c610d3b8e6e8e95b2026e4"
dependencies = [
"cfg-if",
"core-foundation",
"lazycell",
"libc",
"mach",
"nix",
"num-traits",
"uom",
"winapi",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.1.6" version = "3.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive",
"clap_lex",
"indexmap", "indexmap",
"lazy_static", "lazy_static",
"os_str_bytes",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap", "textwrap",
@ -59,9 +95,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.1.4" version = "3.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@ -70,6 +106,82 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "clap_lex"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys 0.7.0",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "crossbeam-channel"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]] [[package]]
name = "directories" name = "directories"
version = "4.0.1" version = "4.0.1"
@ -90,6 +202,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.9.0" version = "0.9.0"
@ -104,9 +222,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -142,9 +260,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.0" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -152,9 +270,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
@ -162,36 +280,113 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.120" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad5c14e80759d0939d013e6ca49930e59fc53dd8e5009132f76240c179380c09" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]] [[package]]
name = "os_str_bytes" name = "memoffset"
version = "6.0.0" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [ dependencies = [
"memchr", "autocfg",
]
[[package]]
name = "nix"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
]
[[package]]
name = "ntapi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi",
"libc",
] ]
[[package]]
name = "once_cell"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "os_str_bytes"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.16" version = "0.2.16"
@ -224,18 +419,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.36" version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
dependencies = [ dependencies = [
"unicode-xid", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.16" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -270,20 +465,44 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rayon"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.11" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.2" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7776223e2696f1aa4c6b0170e83212f47296a00424305117d013dfe86fb0fe55" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"redox_syscall", "redox_syscall",
@ -292,9 +511,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.5" version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -303,15 +522,15 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.25" version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]] [[package]]
name = "rt-format" name = "rt-format"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953eff237fc52cbb1a78d9ef62de48422a42c49c4db65b7e7e9d3aa500b1bdae" checksum = "45087cee619d316fa4bd1675494acff4a5eaa0892fa53bc364bd246f13e452e2"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"regex", "regex",
@ -319,24 +538,30 @@ dependencies = [
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.136" version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.136" version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -345,9 +570,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.79" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -360,6 +585,16 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "swaybar-types"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c0b435952b89d872f882cf7ae0756303ef68d310bfa44b9c8012fda88ae143"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "swayipc" name = "swayipc"
version = "3.0.0" version = "3.0.0"
@ -373,9 +608,9 @@ dependencies = [
[[package]] [[package]]
name = "swayipc-types" name = "swayipc-types"
version = "1.0.1" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "620c3054335b817901d36f06fa5ef715f04d59d7b96f48ecc1a7bf408f194af7" checksum = "38ca323200ba9d5e0bde7a8f5a33c62ab785fce01b77c3b2e0ebb8808094e286"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -384,13 +619,13 @@ dependencies = [
[[package]] [[package]]
name = "swayr" name = "swayr"
version = "0.16.0" version = "0.19.0"
dependencies = [ dependencies = [
"clap", "clap",
"directories", "directories",
"env_logger", "env_logger",
"lazy_static",
"log", "log",
"once_cell",
"rand", "rand",
"regex", "regex",
"rt-format", "rt-format",
@ -400,15 +635,51 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "swayrbar"
version = "0.2.2"
dependencies = [
"battery",
"chrono",
"clap",
"directories",
"env_logger",
"log",
"once_cell",
"regex",
"rt-format",
"serde",
"serde_json",
"swaybar-types",
"swayipc",
"sysinfo",
"toml",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.89" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-xid", "unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.23.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9"
dependencies = [
"cfg-if",
"core-foundation-sys 0.8.3",
"libc",
"ntapi",
"once_cell",
"rayon",
"winapi",
] ]
[[package]] [[package]]
@ -428,38 +699,64 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.30" version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.30" version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "unicode-xid" name = "typenum"
version = "0.2.2" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-ident"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
[[package]]
name = "uom"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed"
dependencies = [
"num-traits",
"typenum",
]
[[package]] [[package]]
name = "version_check" name = "version_check"

@ -1,30 +1,9 @@
[package] [workspace]
name = "swayr" members = [
version = "0.16.0" "swayr",
description = "A LRU window-switcher (and more) for the sway window manager" "swayrbar",
homepage = "https://sr.ht/~tsdh/swayr/" ]
repository = "https://git.sr.ht/~tsdh/swayr"
authors = ["Tassilo Horn <tsdh@gnu.org>"]
license = "GPL-3.0+"
edition = "2018"
exclude = ["misc/"]
[dependencies]
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
clap = {version = "3.0.0", features = ["derive"] }
swayipc = "3.0.0"
toml = "0.5.8"
directories = "4.0"
regex = "1.5.4"
lazy_static = "1.4.0"
rand = "0.8.4"
rt-format = "0.3.0"
log = "0.4"
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex
[profile.dev]
lto = "thin"
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "symbols"

@ -1,17 +1,47 @@
# Swayr is a window switcher (and more) for sway # Swayr & Swayrbar
[![builds.sr.ht status](https://builds.sr.ht/~tsdh/swayr.svg)](https://builds.sr.ht/~tsdh/swayr?) [![builds.sr.ht status](https://builds.sr.ht/~tsdh/swayr.svg)](https://builds.sr.ht/~tsdh/swayr?)
[![latest release](https://img.shields.io/crates/v/swayr.svg)](https://crates.io/crates/swayr)
[![License GPL 3 or later](https://img.shields.io/crates/l/swayr.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License GPL 3 or later](https://img.shields.io/crates/l/swayr.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![dependency status](https://deps.rs/repo/sourcehut/~tsdh/swayr/status.svg)](https://deps.rs/repo/sourcehut/~tsdh/swayr) [![dependency status](https://deps.rs/repo/sourcehut/~tsdh/swayr/status.svg)](https://deps.rs/repo/sourcehut/~tsdh/swayr)
[![Hits-of-Code](https://hitsofcode.com/sourcehut/~tsdh/swayr?branch=main)](https://hitsofcode.com/sourcehut/~tsdh/swayr/view?branch=main) [![Hits-of-Code](https://hitsofcode.com/sourcehut/~tsdh/swayr?branch=main)](https://hitsofcode.com/sourcehut/~tsdh/swayr/view?branch=main)
Swayr consists of a demon, and a client. The demon `swayrd` records ## Table of Contents
* [Swayr](#swayr)
* [Commands](#swayr-commands)
* [Screenshots](#swayr-screenshots)
* [Installation](#swayr-installation)
* [Usage](#swayr-usage)
* [Configuration](#swayr-configuration)
* [Version changes](#swayr-version-changes)
* [Swayrbar](#swayrbar)
* [Screenshots](#swayrbar-screenshots)
* [Installation](#swayrbar-installation)
* [Configuration](#swayrbar-configuration)
* [Version changes](#swayrbar-version-changes)
* [Questions and patches](#questions-and-patches)
* [Bugs](#bugs)
* [Build status](#build-status)
* [License](#license)
## <a id="swayr">Swayr, a window-switcher & more for [sway](https://swaywm.org/)</a>
[![latest release](https://img.shields.io/crates/v/swayr.svg)](https://crates.io/crates/swayr)
Swayr consists of a daemon, and a client. The `swayrd` daemon records
window/workspace creations, deletions, and focus changes using sway's JSON IPC window/workspace creations, deletions, and focus changes using sway's JSON IPC
interface. The client `swayr` offers subcommands, see `swayr --help`, and interface. The `swayr` client offers subcommands, see `swayr --help`, and
sends them to the demon which executes them. sends them to the daemon which executes them.
### <a id="swayr-commands">Swayr commands</a>
The `swayr` binary provides many subcommands of different categories.
#### Non-menu switchers
Those are just commands that toggle between windows without spawning the menu
program.
Right now, there are these subcommands:
* `switch-to-urgent-or-lru-window` switches to the next window with urgency * `switch-to-urgent-or-lru-window` switches to the next window with urgency
hint (if any) or to the last recently used window. hint (if any) or to the last recently used window.
* `switch-to-app-or-urgent-or-lru-window` switches to a specific window matched * `switch-to-app-or-urgent-or-lru-window` switches to a specific window matched
@ -25,6 +55,17 @@ Right now, there are these subcommands:
a "browser" mark to your browser window (using a standard sway `for_window` a "browser" mark to your browser window (using a standard sway `for_window`
rule). Then you can provide "browser" as argument to this command to have a rule). Then you can provide "browser" as argument to this command to have a
convenient browser <-> last-recently-used window toggle. convenient browser <-> last-recently-used window toggle.
* `switch-to-matching-or-urgent-or-lru-window` switches to the (first) window
matching the given criterion (see section `CRITERIA` in `sway(5)`) if it
exists and is not already focused. Otherwise, switch to the next urgent
window (if any) or to the last recently used window.
#### Menu switchers
Those spawn a menu program where you can select a window (or workspace, or
output, etc.) and act on that.
* `switch-window` displays all windows in the order urgent first, then * `switch-window` displays all windows in the order urgent first, then
last-recently-used, focused last and focuses the selected. last-recently-used, focused last and focuses the selected.
* `switch-workspace` displays all workspaces in LRU order and switches to the * `switch-workspace` displays all workspaces in LRU order and switches to the
@ -55,6 +96,29 @@ Right now, there are these subcommands:
like with `move-focused-to-workspace`. like with `move-focused-to-workspace`.
* `swap-focused-with` swaps the currently focused window or container with the * `swap-focused-with` swaps the currently focused window or container with the
one selected from the menu program. one selected from the menu program.
##### Menu shortcuts for non-matching input
All menu switching commands (`switch-window`, `switch-workspace`, and
`switch-workspace-or-window`) now handle non-matching input instead of doing
nothing. The input should start with any number of `#` (in order to be able to
force a non-match), a shortcut followed by a colon, and some string as required
by the shortcut. The following shortcuts are supported.
- `w:<workspace>`: Switches to a possibly non-existing workspace.
`<workspace>` must be a digit, a name or `<digit>:<name>`. The
`<digit>:<name>` format is explained in `man 5 sway`. If that format is
given, `swayr` will create the workspace using `workspace number
<digit>:<name>`. If just a digit or name is given, the `number` argument is
not used.
- `s:<cmd>`: Executes the sway command `<cmd>` using `swaymsg`.
- Any other input is assumed to be a workspace name and thus handled as
`w:<input>` would do.
#### <a id="swayr-cycling-commands">Cycling commands</a>
Those commands cycle through (a subset of windows) in last-recently-used order.
* `next-window (all-workspaces|current-workspace)` & `prev-window * `next-window (all-workspaces|current-workspace)` & `prev-window
(all-workspaces|current-workspace)` focus the next/previous window in (all-workspaces|current-workspace)` focus the next/previous window in
depth-first iteration order of the tree. The argument `all-workspaces` or depth-first iteration order of the tree. The argument `all-workspaces` or
@ -74,6 +138,11 @@ Right now, there are these subcommands:
stacked container, it is like `next-tiled-window` / `prev-tiled-window` if stacked container, it is like `next-tiled-window` / `prev-tiled-window` if
the current windows is in a tiled container, and is like `next-window` / the current windows is in a tiled container, and is like `next-window` /
`prev-window` otherwise. `prev-window` otherwise.
#### Layout modification commands
These commands change the layout of the current workspace.
* `tile-workspace exclude-floating|include-floating` tiles all windows on the * `tile-workspace exclude-floating|include-floating` tiles all windows on the
current workspace (excluding or including floating ones). That's done by current workspace (excluding or including floating ones). That's done by
moving all windows away to some special workspace, setting the current moving all windows away to some special workspace, setting the current
@ -97,6 +166,9 @@ Right now, there are these subcommands:
between a tabbed and tiled layout, i.e., it calls `shuffle-tile-workspace` if between a tabbed and tiled layout, i.e., it calls `shuffle-tile-workspace` if
it is currently tabbed, and calls `shuffle-tile-workspace` if it is currently it is currently tabbed, and calls `shuffle-tile-workspace` if it is currently
tiled. tiled.
#### Miscellaneous commands
* `configure-outputs` lets you repeatedly issue output configuration commands * `configure-outputs` lets you repeatedly issue output configuration commands
until you abort the menu program. until you abort the menu program.
* `execute-swaymsg-command` displays most swaymsg which don't require * `execute-swaymsg-command` displays most swaymsg which don't require
@ -106,25 +178,14 @@ Right now, there are these subcommands:
* `execute-swayr-command` displays all commands above and executes the selected * `execute-swayr-command` displays all commands above and executes the selected
one. (This is useful for accessing swayr commands which are not bound to a one. (This is useful for accessing swayr commands which are not bound to a
key.) key.)
* `nop` (unsurprisingly) does nothing, the command can be used to break out of
a sequence of [window cycling commands](#swayr-cycling-commands). The LRU
window order is frozen when the first cycling command is processed and
remains so until a non-cycling command is received. The `nop` command can
conveniently serve to interrupt a sequence without having any other side
effects.
### Menu shortcuts for non-matching input ### <a id="swayr-screenshots">Screenshots</a>
All menu switching commands (`switch-window`, `switch-workspace`, and
`switch-workspace-or-window`) now handle non-matching input instead of doing
nothing. The input should start with any number of `#` (in order to be able to
force a non-match), a shortcut followed by a colon, and some string as required
by the shortcut. The following shortcuts are supported.
- `w:<workspace>`: Switches to a possibly non-existing workspace.
`<workspace>` must be a digit, a name or `<digit>:<name>`. The
`<digit>:<name>` format is explained in `man 5 sway`. If that format is
given, `swayr` will create the workspace using `workspace number
<digit>:<name>`. If just a digit or name is given, the `number` argument is
not used.
- `s:<cmd>`: Executes the sway command `<cmd>` using `swaymsg`.
- Any other input is assumed to be a workspace name and thus handled as
`w:<input>` would do.
## Screenshots
![A screenshot of swayr switch-window](misc/switch-window.png "swayr ![A screenshot of swayr switch-window](misc/switch-window.png "swayr
switch-window") switch-window")
@ -133,13 +194,13 @@ switch-window")
switch-workspace-or-window](misc/switch-workspace-or-window.png "swayr switch-workspace-or-window](misc/switch-workspace-or-window.png "swayr
switch-workspace-or-window") switch-workspace-or-window")
## Installation ### <a id="swayr-installation">Installation</a>
Some distros have packaged swayr so that you can install it using your distro's Some distros have packaged swayr so that you can install it using your distro's
package manager. Alternatively, it's easy to build and install it yourself package manager. Alternatively, it's easy to build and install it yourself
using `cargo`. using `cargo`.
### Distro packages #### Distro packages
The following GNU/Linux and BSD distros package swayr. Thanks a lot to the The following GNU/Linux and BSD distros package swayr. Thanks a lot to the
respective package maintainers! Refer to the [repology respective package maintainers! Refer to the [repology
@ -148,7 +209,7 @@ site](https://repology.org/project/swayr/versions) for details.
[![Packaging status](https://repology.org/badge/vertical-allrepos/swayr.svg)](https://repology.org/project/swayr/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/swayr.svg)](https://repology.org/project/swayr/versions)
[![AUR swayr-git package status](https://repology.org/badge/version-for-repo/aur/swayr.svg?allow_ignored=yes&header=AUR%20swayr-git)](https://repology.org/project/swayr/versions) [![AUR swayr-git package status](https://repology.org/badge/version-for-repo/aur/swayr.svg?allow_ignored=yes&header=AUR%20swayr-git)](https://repology.org/project/swayr/versions)
### Building with cargo #### Building with cargo
You'll need to install the current stable rust toolchain using the one-liner You'll need to install the current stable rust toolchain using the one-liner
shown at the [official rust installation shown at the [official rust installation
@ -171,9 +232,9 @@ cargo install-update --all
cargo install-update -- swayr cargo install-update -- swayr
``` ```
## Usage ### <a id="swayr-usage">Usage</a>
You need to start the swayr demon `swayrd` in your sway config You need to start the swayr daemon (`swayrd`) in your sway config
(`~/.config/sway/config`) like so: (`~/.config/sway/config`) like so:
``` ```
@ -188,8 +249,8 @@ issue with backtrace and logging at the `debug` level and attach that to your
bug report. Valid log levels in the order from logging more to logging less bug report. Valid log levels in the order from logging more to logging less
are: `trace`, `debug`, `info`, `warn`, `error`, `off`. are: `trace`, `debug`, `info`, `warn`, `error`, `off`.
Next to starting the demon, you want to bind swayr commands to some keys like Beyond starting the daemon, you will want to bind swayr commands to some keys
so: like so:
``` ```
bindsym $mod+Space exec env RUST_BACKTRACE=1 \ bindsym $mod+Space exec env RUST_BACKTRACE=1 \
@ -220,7 +281,19 @@ bindsym $mod+Shift+c exec env RUST_BACKTRACE=1 \
Of course, configure the keys to your liking. Again, enabling rust backtraces Of course, configure the keys to your liking. Again, enabling rust backtraces
and logging are optional. and logging are optional.
## Configuration Pending a fix for [Sway issue
#6456](https://github.com/swaywm/sway/issues/6456), it will be possible to
close a sequence of [window cycling commands](#swayr-cycling-commands) using a
`nop` command bound to the release of the `$mod` key. Assuming your `$mod` is
bound to `Super_L` it could look something like this:
```
bindsym --release Super_L exec env RUST_BACKTRACE=1 \
swayr nop >> /tmp/swayr.log 2>&1
```
### <a id="swayr-configuration">Configuration</a>
Swayr can be configured using the `~/.config/swayr/config.toml` or Swayr can be configured using the `~/.config/swayr/config.toml` or
`/etc/xdg/swayr/config.toml` config file. `/etc/xdg/swayr/config.toml` config file.
@ -284,18 +357,21 @@ auto_tile_min_window_width_per_output_width = [
[3440, 1000], [3440, 1000],
[4096, 1200], [4096, 1200],
] ]
[focus]
lockin_delay = 750
``` ```
In the following, all sections are explained. In the following, all sections are explained.
### The menu section #### The menu section
In the `[menu]` section, you can specify the menu program using the In the `[menu]` section, you can specify the menu program using the
`executable` name or full path and the `args` (flags and options) it should get `executable` name or full path and the `args` (flags and options) it should get
passed. If some argument contains the placeholder `{prompt}`, it is replaced passed. If some argument contains the placeholder `{prompt}`, it is replaced
with a prompt such as "Switch to window" depending on context. with a prompt such as "Switch to window" depending on context.
### The format section #### The format section
In the `[format]` section, format strings are specified defining how selection In the `[format]` section, format strings are specified defining how selection
choices are to be layed out. `wofi` supports [pango choices are to be layed out. `wofi` supports [pango
@ -337,9 +413,10 @@ right now.
* `fallback_icon` is a path to some PNG/SVG icon which will be used as * `fallback_icon` is a path to some PNG/SVG icon which will be used as
`{app_icon}` if no application-specific icon can be determined. `{app_icon}` if no application-specific icon can be determined.
All the placeholders except `{app_icon}`, `{indent}`, `{urgency_start}`, and All the <a id="fmt-placeholders">placeholders</a> except `{app_icon}`,
`{urgency_end}` may optionally provide a format string as specified by `{indent}`, `{urgency_start}`, and `{urgency_end}` may optionally provide a
[Rust's std::fmt](https://doc.rust-lang.org/std/fmt/). The syntax is format string as specified by [Rust's
std::fmt](https://doc.rust-lang.org/std/fmt/). The syntax is
`{<placeholder>:<fmt_str><clipped_str>}`. For example, `{app_name:{:>10.10}}` `{<placeholder>:<fmt_str><clipped_str>}`. For example, `{app_name:{:>10.10}}`
would mean that the application name is printed with exactly 10 characters. If would mean that the application name is printed with exactly 10 characters. If
it's shorter, it will be right-aligned (the `>`) and padded with spaces, if it's shorter, it will be right-aligned (the `>`) and padded with spaces, if
@ -370,7 +447,7 @@ processed whereas for double-quoted strings (so-called basic strings)
escape-sequences are processed. `rofi` requires a null character and a escape-sequences are processed. `rofi` requires a null character and a
PARAGRAPH SEPARATOR for image sequences. PARAGRAPH SEPARATOR for image sequences.
### The layout section #### The layout section
In the `[layout]` section, you can enable auto-tiling by setting `auto_tile` to In the `[layout]` section, you can enable auto-tiling by setting `auto_tile` to
`true` (the default is `false`). The option `true` (the default is `false`). The option
@ -403,29 +480,271 @@ over IPC. Therefore, auto-tiling is triggered by new-window events,
close-events, move-events, floating-events, and also focus-events. The latter close-events, move-events, floating-events, and also focus-events. The latter
are a workaround and wouldn't be required if there were resize-events. are a workaround and wouldn't be required if there were resize-events.
## Version Changes
Since version 0.8.0, I've started writing a [NEWS](NEWS.md) file listing the #### The focus section
In the `[focus]` section, you can configure how focus switches affect the LRU
order.
* `lockin_delay` determines the amount of time (in milliseconds) a window has
to keep the focus in order to affect the LRU order. If a given window
is only briefly focused, e.g., by moving the mouse over it on the way to
another window with sway's `focus_follows_mouse` set to `yes` or `always`,
then its position in the LRU order will not be modified.
* `sequence_timeout` may specify an amount of time (in milliseconds) such
that a sequence of next-/prev-window commands is considered finished
when such time elapses after the latest command is executed, even if no
unrelated swayr command is received.
### <a id="swayr-version-changes">Version changes</a>
Since version 0.8.0, I've started writing a [NEWS](swayr/NEWS.md) file listing the
news, and changes to `swayr` commands or configuration options. If something news, and changes to `swayr` commands or configuration options. If something
doesn't seem to work as expected after an update, please consult this file to doesn't seem to work as expected after an update, please consult this file to
check if there has been some (possibly incompatible) change requiring an update check if there has been some (possibly incompatible) change requiring an update
of your config. of your config.
## Questions & Patches ## <a id="swayrbar">Swayrbar</a>
[![latest release](https://img.shields.io/crates/v/swayrbar.svg)](https://crates.io/crates/swayrbar)
`swayrbar` is a status command for sway's `swaybar` implementing the
[`swaybar-protocol(7)`](https://man.archlinux.org/man/swaybar-protocol.7).
This means, you would setup your `swaybar` like so in your
`~/.config/sway/config`:
```conf
bar {
swaybar_command swaybar
# Use swayrbar as status command with some logging output which
# is redirected to /tmp/swayrbar.log. Be sure to only redirect
# stderr because the swaybar protocol requires the status_command
# to emit JSON to stdout which swaybar reads.
status_command env RUST_BACKTRACE=1 RUST_LOG=swayr=debug swayrbar 2> /tmp/swayrbar.log
position top
font pango:Iosevka 11
height 20
colors {
statusline #f8c500
background #33333390
}
}
```
`swayrbar`, like [waybar](https://github.com/Alexays/Waybar/), consists of a
set of modules which you can enable and configure via its config file, either
the one specified via the command line option `--config-file`, the
user-specific (`~/.config/swayrbar/config.toml`), or the system-wide
(`/etc/xdg/swayrbar/config.toml`). Modules emit information which `swaybar`
then displays and mouse clicks on a module's space in `swaybar` are propagated
back and trigger some action (e.g., a shell command).
Right now, there are the following modules:
1. The `window` module can show the title and application name of the current
window in sway.
2. The `sysinfo` module can show things like CPU/memory utilization or system
load.
3. The `battery` module can show the current [state of
charge](https://en.wikipedia.org/wiki/State_of_charge), the state (e.g.,
charging), and the [state of
health](https://en.wikipedia.org/wiki/State_of_health).
4. The `date` module can show, you guess it, the current date and time!
5. The `pactl` module can show the current volume percentage and muted state.
Clicks can increase/decrease the volume or toggle the mute state.
I guess there will be more modules in the future as time permits.
[Patches](#questions-and-patches) are certainly very welcome!
### <a id="swayrbar-screenshots">Screenshots</a>
![A screenshot of swaybar running with swayrbar](misc/swayrbar.png "swaybar
with swayrbar")
### <a id="swayrbar-installation">Installation</a>
Some distros have a swayrbar package so that you can install it using your
distro's package manager, see the [repology
site](https://repology.org/project/swayrbar/versions) for details.
Alternatively, it's easy to build and install it yourself using `cargo`.
[![Packaging status](https://repology.org/badge/vertical-allrepos/swayrbar.svg)](https://repology.org/project/swayrbar/versions)
#### Installation via Cargo
You'll need to install the current stable rust toolchain using the one-liner
shown at the [official rust installation
page](https://www.rust-lang.org/tools/install).
Then you can install swayrbar like so:
```sh
cargo install swayrbar
```
For getting updates easily, I recommend the cargo `install-update` plugin.
```sh
# Install it once.
cargo install install-update
# Then you can update all installed rust binary crates including swayr using:
cargo install-update --all
# If you only want to update swayr, you can do so using:
cargo install-update -- swayrbar
```
### <a id="swayrbar-configuration">Configuration</a>
When `swayrbar` is run for the very first time and doesn't find an existing
configuration file at `~/.config/swayrbar/config.toml` (user-specific) or
`/etc/xdg/swayrbar/config.toml` (system-wide), it'll create a new user-specific
one where all modules are enabled and set up with some reasonable (according to
the author) default values. Adapt it to your needs.
The syntax of the config file is [TOML](https://toml.io/en/). Here's a short
example with all top-level options (one!) and one module.
```toml
refresh_interval = 1000
[[modules]]
name = 'window'
instance = '0'
format = '🪟 {title} — {app_name}'
html_escape = false
[modules.on_click]
Left = ['swayr', 'switch-to-urgent-or-lru-window']
Right = ['kill', '{pid}']
```
The `refresh_interval` defines the number of milliseconds between refreshes of
`swaybar`.
The remainder of the configuration defines a list of modules with their
configuration (which is an [array of
tables](https://toml.io/en/v1.0.0#array-of-tables) in TOML where a module's
`on_click`).
* `name` is the name or type of the module, e.g., `window`, `sysinfo`,
`battery`, `date`,...
* `instance` is an arbitrary string used for distinguishing two modules of the
same `name`. For example, you might want to have two `sysinfo` modules, one
for CPU and one for memory utilization, simply to have a separator between
these different kinds of information. That's easily doable, just give them
different `instance` values.
* `format` is the string to be printed in `swaybar` where certain placeholders
are substituted with module-specific values. Usually, such placeholders are
written like `{title}`, i.e., inside braces. Like in `swayr`, formatting
(padding, aligning, precision, etc.) is available, see
[here](#fmt-placeholders).
* `html_escape` defines if `<`, `>`, and `&` should be escaped as `&lt;`,
`&gt;`, and `&amp;` because `format` may contain [pango
markup](https://docs.gtk.org/Pango/pango_markup.html). Obviously, if you
make use of this feature, you want to set `html_escape = true` for that
module. This option is optional and may be omitted.
* `on_click` is a table defining actions to be performed when you click on a
module's space in `swaybar`. All placeholders available in `format` are
available here, too. The action for each mouse button is specified as an
array `['command', 'arg1', 'arg2',...]`. The available button names to be
assigned to are `Left`, `Middle`, `Right`, `WheelUp`, `WheelDown`,
`WheelLeft`, and `WheelRight`.
The `on_click` table can also be written as inline table
```toml
on_click = { Left = ['swayr', 'switch-to-urgent-or-lru-window'], Right = ['kill', '{pid}'] }
```
but then it has to be on one single line.
#### The `window` module
The `window` module supports the following placeholders:
* `{title}` or `{name}` expand to the currently focused window's title.
* `{app_name}` is the application name.
* `{pid}` is the process id.
By default, it has the following click bindings:
* `Left` executes `swayr switch-to-urgent-or-lru-window`.
* `Right` kills the process of the window.
#### The `sysinfo` module
The `sysinfo` module supports the following placeholders:
* `{cpu_usage}` is the percentage of CPU utilization.
* `{mem_usage}` is the percentage of memory utilization.
* `{load_avg_1}` is the average system load in the last minute.
* `{load_avg_5}` is the average system load in the last five minutes.
* `{load_avg_15}` is the average system load in the last fifteen minutes.
By default, it has the following click bindings:
* `Left` executes `foot htop`.
#### The `battery` module
The `battery` module supports the following placeholders:
* `{state_of_charge}` is the percentage of charge wrt. the battery's current
capacity.
* `{state_of_health}` is the percentage of the battery's remaining capacity
compared to its original capacity.
* `{state}` is the current state, e.g., something like Discharging or Full.
#### The `pactl` module
The `pactl` module requires the pulse-audio command line tool of the same name
to be installed. It supports the following placeholders:
* `{volume}` is the current volume percentage of the default sink.
* `{muted}` is the string `" muted"` if the default sink is currently muted,
otherwise it is the empty string.
By default, it has the following click bindings:
* `Left` calls the `pavucontrol` program (PulseAudio GUI control).
* `Right` toggles the default sink's mute state.
* `WheelUp` and `WheelDown` increase/decrease the volume of the default sink.
#### The `date` module
The `date` module shows the date and time by defining the `format` using
[chrono's strftime
format](https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html#specifiers).
### <a id="swayr-version-changes">Version changes</a>
Version changes are summarized in the [NEWS](swayrbar/NEWS.md) file. If
something doesn't seem to work as expected after an update, please consult this
file to check if there has been some (possibly incompatible) change requiring
an update of your config.
## <a id="questions-and-patches">Questions & Patches</a>
For asking questions, sending feedback, or patches, refer to [my public inbox For asking questions, sending feedback, or patches, refer to [my public inbox
(mailinglist)](https://lists.sr.ht/~tsdh/public-inbox). Please mention the (mailinglist)](https://lists.sr.ht/~tsdh/public-inbox). Please mention the
project you are referring to in the subject. project you are referring to in the subject, e.g., `swayr` or `swayrbar` (or
other projects in different repositories).
## Bugs ## <a id="bugs">Bugs</a>
Bugs and requests can be reported [here](https://todo.sr.ht/~tsdh/swayr). It compiles, therefore there are no bugs. Oh well, if you still found one or
want to request a feature, you can do so
[here](https://todo.sr.ht/~tsdh/swayr).
## Build status ## <a id="build-status">Build status</a>
[![builds.sr.ht status](https://builds.sr.ht/~tsdh/swayr.svg)](https://builds.sr.ht/~tsdh/swayr?) [![builds.sr.ht status](https://builds.sr.ht/~tsdh/swayr.svg)](https://builds.sr.ht/~tsdh/swayr?)
## License ## <a id="license">License</a>
Swayr is licensed under the Swayr & Swayrbar are licensed under the
[GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) (or later). [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) (or later).

@ -1 +1,8 @@
- Switch from lazy_static to once_cell once the latter is in stable rust. Swayr
=====
Swayrbar
========
- Maybe add a launcher bar module
- Make the window module subscribe to sway window events and trigger an early
refresh on focus changes.

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@ -1,292 +0,0 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Functions and data structures of the swayrd demon.
use crate::cmds;
use crate::config;
use crate::layout;
use crate::tree as t;
use crate::util;
use std::collections::HashMap;
use std::io::Read;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::Arc;
use std::sync::RwLock;
use std::thread;
use std::time::Duration;
use swayipc as s;
pub fn run_demon() {
let config = config::load_config();
let extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>> =
Arc::new(RwLock::new(HashMap::new()));
let config_for_ev_handler = config.clone();
let extra_props_for_ev_handler = extra_props.clone();
thread::spawn(move || {
monitor_sway_events(extra_props_for_ev_handler, config_for_ev_handler);
});
serve_client_requests(extra_props, config);
}
fn connect_and_subscribe() -> s::Fallible<s::EventStream> {
s::Connection::new()?.subscribe(&[
s::EventType::Window,
s::EventType::Workspace,
s::EventType::Shutdown,
])
}
pub fn monitor_sway_events(
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
config: config::Config,
) {
let mut focus_counter = 0;
let mut resets = 0;
let max_resets = 10;
'reset: loop {
if resets >= max_resets {
break;
}
resets += 1;
log::debug!("Connecting to sway for subscribing to events...");
match connect_and_subscribe() {
Err(err) => {
log::warn!("Could not connect and subscribe: {}", err);
std::thread::sleep(std::time::Duration::from_secs(3));
}
Ok(iter) => {
for ev_result in iter {
let show_extra_props_state;
resets = 0;
match ev_result {
Ok(ev) => match ev {
s::Event::Window(win_ev) => {
let extra_props_clone = extra_props.clone();
focus_counter += 1;
show_extra_props_state = handle_window_event(
win_ev,
extra_props_clone,
&config,
focus_counter,
);
}
s::Event::Workspace(ws_ev) => {
let extra_props_clone = extra_props.clone();
focus_counter += 1;
show_extra_props_state = handle_workspace_event(
ws_ev,
extra_props_clone,
focus_counter,
);
}
s::Event::Shutdown(sd_ev) => {
log::debug!(
"Sway shuts down with reason '{:?}'.",
sd_ev.change
);
break 'reset;
}
_ => show_extra_props_state = false,
},
Err(e) => {
log::warn!("Error while receiving events: {}", e);
std::thread::sleep(std::time::Duration::from_secs(
3,
));
show_extra_props_state = false;
log::warn!("Resetting!");
}
}
if show_extra_props_state {
log::debug!(
"New extra_props state:\n{:#?}",
*extra_props.read().unwrap()
);
}
}
}
}
}
log::debug!("Swayr demon shutting down.")
}
fn handle_window_event(
ev: Box<s::WindowEvent>,
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
config: &config::Config,
focus_val: u64,
) -> bool {
let s::WindowEvent {
change, container, ..
} = *ev;
match change {
s::WindowChange::Focus => {
layout::maybe_auto_tile(config);
update_last_focus_tick(container.id, extra_props, focus_val);
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::New => {
layout::maybe_auto_tile(config);
update_last_focus_tick(container.id, extra_props, focus_val);
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::Close => {
remove_extra_props(container.id, extra_props);
layout::maybe_auto_tile(config);
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::Move | s::WindowChange::Floating => {
layout::maybe_auto_tile(config);
log::debug!("Handled window event type {:?}", change);
false // We don't affect the extra_props state here.
}
_ => {
log::debug!("Unhandled window event type {:?}", change);
false
}
}
}
fn handle_workspace_event(
ev: Box<s::WorkspaceEvent>,
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
focus_val: u64,
) -> bool {
let s::WorkspaceEvent {
change,
current,
old: _,
..
} = *ev;
match change {
s::WorkspaceChange::Init | s::WorkspaceChange::Focus => {
update_last_focus_tick(
current
.expect("No current in Init or Focus workspace event")
.id,
extra_props,
focus_val,
);
log::debug!("Handled workspace event type {:?}", change);
true
}
s::WorkspaceChange::Empty => {
remove_extra_props(
current.expect("No current in Empty workspace event").id,
extra_props,
);
log::debug!("Handled workspace event type {:?}", change);
true
}
_ => false,
}
}
fn update_last_focus_tick(
id: i64,
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
focus_val: u64,
) {
let mut write_lock = extra_props.write().unwrap();
if let Some(wp) = write_lock.get_mut(&id) {
wp.last_focus_tick = focus_val;
} else {
write_lock.insert(
id,
t::ExtraProps {
last_focus_tick: focus_val,
last_focus_tick_for_next_prev_seq: focus_val,
},
);
}
}
fn remove_extra_props(
id: i64,
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
) {
extra_props.write().unwrap().remove(&id);
}
pub fn serve_client_requests(
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
config: config::Config,
) {
match std::fs::remove_file(util::get_swayr_socket_path()) {
Ok(()) => log::debug!("Deleted stale socket from previous run."),
Err(e) => log::error!("Could not delete socket:\n{:?}", e),
}
let timeout = match config.get_sequence_timeout() {
0 => None,
secs => Some(Duration::from_secs(secs)),
};
match UnixListener::bind(util::get_swayr_socket_path()) {
Ok(listener) => {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
handle_client_request(
stream,
extra_props.clone(),
timeout,
);
}
Err(err) => {
log::error!("Error handling client request: {}", err);
break;
}
}
}
}
Err(err) => {
log::error!("Could not bind socket: {}", err)
}
}
}
fn handle_client_request(
mut stream: UnixStream,
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>,
sequence_timeout: Option<Duration>,
) {
let mut cmd_str = String::new();
if stream.read_to_string(&mut cmd_str).is_ok() {
if let Ok(cmd) = serde_json::from_str::<cmds::SwayrCommand>(&cmd_str) {
cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs {
cmd: &cmd,
extra_props,
sequence_timeout,
});
} else {
log::error!(
"Could not serialize following string to SwayrCommand.\n{}",
cmd_str
);
}
} else {
log::error!("Could not read command from client.");
}
}

@ -1,103 +0,0 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Provides runtime formatting of strings since our format strings are read
//! from the swayr config, not specified as literals, e.g., `{name:{:.30}}` in
//! `format.window_format`.
use rt_format::{
Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier,
};
use std::fmt;
enum FmtArg<'a> {
Str(&'a str),
}
impl<'a> FormatArgument for FmtArg<'a> {
fn supports_format(&self, spec: &Specifier) -> bool {
spec.format == Format::Display
}
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Str(val) => fmt::Display::fmt(&val, f),
}
}
fn fmt_debug(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_octal(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_lower_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_upper_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_binary(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_lower_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_upper_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
}
pub fn format(fmt: &str, arg: &str, clipped_str: &str) -> String {
let args = &[FmtArg::Str(arg)];
if let Ok(pf) = ParsedFormat::parse(fmt, args, &NoNamedArguments) {
let mut s = format!("{}", pf);
if !clipped_str.is_empty() && !s.contains(arg) {
remove_last_n_chars(&mut s, clipped_str.chars().count());
s.push_str(clipped_str);
}
s
} else {
format!("Invalid format string: {}", fmt)
}
}
fn remove_last_n_chars(s: &mut String, n: usize) {
match s.char_indices().nth_back(n) {
Some((pos, ch)) => s.truncate(pos + ch.len_utf8()),
None => s.clear(),
}
}
#[test]
fn test_format() {
assert_eq!(format("{:.10}", "sway", ""), "sway");
assert_eq!(format("{:.10}", "sway", "…"), "sway");
assert_eq!(format("{:.4}", "𝔰𝔴𝔞𝔶", "……"), "𝔰𝔴𝔞𝔶");
assert_eq!(format("{:.3}", "sway", ""), "swa");
assert_eq!(format("{:.3}", "sway", "…"), "sw…");
assert_eq!(format("{:.5}", "𝔰𝔴𝔞𝔶 𝔴𝔦𝔫𝔡𝔬𝔴", "…?"), "𝔰𝔴𝔞…?");
assert_eq!(format("{:.5}", "sway window", "..."), "sw...");
assert_eq!(format("{:.2}", "sway", "..."), "...");
}

@ -1,598 +0,0 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Convenience data structures built from the IPC structs.
use crate::config;
use crate::rtfmt;
use crate::util;
use crate::util::DisplayFormat;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::cmp;
use std::collections::HashMap;
use std::rc::Rc;
use swayipc as s;
/// Immutable Node Iterator
///
/// Iterates nodes in depth-first order, tiled nodes before floating nodes.
pub struct NodeIter<'a> {
stack: Vec<&'a s::Node>,
}
impl<'a> NodeIter<'a> {
fn new(node: &'a s::Node) -> NodeIter {
NodeIter { stack: vec![node] }
}
}
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a s::Node;
fn next(&mut self) -> Option<Self::Item> {
if let Some(node) = self.stack.pop() {
for n in &node.floating_nodes {
self.stack.push(n);
}
for n in &node.nodes {
self.stack.push(n);
}
Some(node)
} else {
None
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Type {
Root,
Output,
Workspace,
Container,
Window,
}
/// Extension methods for [`swayipc::Node`].
pub trait NodeMethods {
fn iter(&self) -> NodeIter;
fn get_type(&self) -> Type;
fn get_app_name(&self) -> &str;
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node>;
fn get_name(&self) -> &str;
fn is_scratchpad(&self) -> bool;
fn is_floating(&self) -> bool;
fn is_current(&self) -> bool;
}
impl NodeMethods for s::Node {
fn iter(&self) -> NodeIter {
NodeIter::new(self)
}
fn get_type(&self) -> Type {
match self.node_type {
s::NodeType::Root => Type::Root,
s::NodeType::Output => Type::Output,
s::NodeType::Workspace => Type::Workspace,
s::NodeType::FloatingCon => Type::Window,
_ => {
if self.node_type == s::NodeType::Con
&& self.name.is_none()
&& self.app_id.is_none()
&& self.pid.is_none()
&& self.shell.is_none()
&& self.window_properties.is_none()
&& self.layout != s::NodeLayout::None
{
Type::Container
} else if (self.node_type == s::NodeType::Con
|| self.node_type == s::NodeType::FloatingCon)
// Apparently there can be windows without app_id, name,
// and window_properties.class, e.g., dolphin-emu-nogui.
&& self.pid.is_some()
// FIXME: While technically correct, old sway versions (up to
// at least sway-1.4) don't expose shell in IPC. So comment in
// again when all major distros have a recent enough sway
// package.
//&& self.shell.is_some()
{
Type::Window
} else {
panic!(
"Don't know type of node with id {} and node_type {:?}\n{:?}",
self.id, self.node_type, self
)
}
}
}
}
fn get_name(&self) -> &str {
if let Some(name) = &self.name {
name.as_ref()
} else {
"<unnamed>"
}
}
fn get_app_name(&self) -> &str {
if let Some(app_id) = &self.app_id {
app_id
} else if let Some(wp_class) = self
.window_properties
.as_ref()
.and_then(|wp| wp.class.as_ref())
{
wp_class
} else {
"<unknown_app>"
}
}
fn is_scratchpad(&self) -> bool {
let name = self.get_name();
name.eq("__i3") || name.eq("__i3_scratch")
}
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node> {
self.iter().filter(|n| n.get_type() == t).collect()
}
fn is_floating(&self) -> bool {
self.node_type == s::NodeType::FloatingCon
}
fn is_current(&self) -> bool {
self.iter().any(|n| n.focused)
}
}
/// Extra properties gathered by swayrd for windows and workspaces.
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
pub struct ExtraProps {
pub last_focus_tick: u64,
pub last_focus_tick_for_next_prev_seq: u64,
}
pub struct Tree<'a> {
root: &'a s::Node,
id_node: HashMap<i64, &'a s::Node>,
id_parent: HashMap<i64, i64>,
extra_props: &'a HashMap<i64, ExtraProps>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum IndentLevel {
Fixed(usize),
WorkspacesZeroWindowsOne,
TreeDepth(usize),
}
pub struct DisplayNode<'a> {
pub node: &'a s::Node,
pub tree: &'a Tree<'a>,
indent_level: IndentLevel,
}
impl<'a> Tree<'a> {
fn get_node_by_id(&self, id: i64) -> &&s::Node {
self.id_node
.get(&id)
.unwrap_or_else(|| panic!("No node with id {}", id))
}
fn get_parent_node(&self, id: i64) -> Option<&&s::Node> {
self.id_parent.get(&id).map(|pid| self.get_node_by_id(*pid))
}
pub fn get_parent_node_of_type(
&self,
id: i64,
t: Type,
) -> Option<&&s::Node> {
let n = self.get_node_by_id(id);
if n.get_type() == t {
Some(n)
} else if let Some(pid) = self.id_parent.get(&id) {
self.get_parent_node_of_type(*pid, t)
} else {
None
}
}
pub fn last_focus_tick(&self, id: i64) -> u64 {
self.extra_props.get(&id).map_or(0, |wp| wp.last_focus_tick)
}
pub fn last_focus_tick_for_next_prev_seq(&self, id: i64) -> u64 {
self.extra_props
.get(&id)
.map_or(0, |wp| wp.last_focus_tick_for_next_prev_seq)
}
fn sorted_nodes_of_type_1(
&self,
node: &'a s::Node,
t: Type,
) -> Vec<&s::Node> {
let mut v: Vec<&s::Node> = node.nodes_of_type(t);
self.sort_by_urgency_and_lru_time_1(&mut v);
v
}
fn sorted_nodes_of_type(&self, t: Type) -> Vec<&s::Node> {
self.sorted_nodes_of_type_1(self.root, t)
}
fn as_display_nodes(
&self,
v: &[&'a s::Node],
indent_level: IndentLevel,
) -> Vec<DisplayNode> {
v.iter()
.map(|node| DisplayNode {
node,
tree: self,
indent_level,
})
.collect()
}
pub fn get_current_workspace(&self) -> &s::Node {
self.root
.iter()
.find(|n| n.get_type() == Type::Workspace && n.is_current())
.expect("No current Workspace")
}
pub fn get_outputs(&self) -> Vec<DisplayNode> {
let outputs: Vec<&s::Node> = self
.root
.iter()
.filter(|n| n.get_type() == Type::Output && !n.is_scratchpad())
.collect();
self.as_display_nodes(&outputs, IndentLevel::Fixed(0))
}
pub fn get_workspaces(&self) -> Vec<DisplayNode> {
let mut v = self.sorted_nodes_of_type(Type::Workspace);
if !v.is_empty() {
v.rotate_left(1);
}
self.as_display_nodes(&v, IndentLevel::Fixed(0))
}
pub fn get_windows(&self) -> Vec<DisplayNode> {
let mut v = self.sorted_nodes_of_type(Type::Window);
// Rotate, but only non-urgent windows. Those should stay at the front
// as they are the most likely switch candidates.
let mut x;
if !v.is_empty() {
x = vec![];
loop {
if !v.is_empty() && v[0].urgent {
x.push(v.remove(0));
} else {
break;
}
}
if !v.is_empty() {
v.rotate_left(1);
x.append(&mut v);
}
} else {
x = v;
}
self.as_display_nodes(&x, IndentLevel::Fixed(0))
}
pub fn get_workspaces_and_windows(&self) -> Vec<DisplayNode> {
let workspaces = self.sorted_nodes_of_type(Type::Workspace);
let mut first = true;
let mut v = vec![];
for ws in workspaces {
v.push(ws);
let mut wins = self.sorted_nodes_of_type_1(ws, Type::Window);
if first && !wins.is_empty() {
wins.rotate_left(1);
first = false;
}
v.append(&mut wins);
}
self.as_display_nodes(&v, IndentLevel::WorkspacesZeroWindowsOne)
}
fn sort_by_urgency_and_lru_time_1(&self, v: &mut Vec<&s::Node>) {
v.sort_by(|a, b| {
if a.urgent && !b.urgent {
cmp::Ordering::Less
} else if !a.urgent && b.urgent {
cmp::Ordering::Greater
} else {
let lru_a = self.last_focus_tick(a.id);
let lru_b = self.last_focus_tick(b.id);
lru_a.cmp(&lru_b).reverse()
}
});
}
fn push_subtree_sorted(
&self,
n: &'a s::Node,
v: Rc<RefCell<Vec<&'a s::Node>>>,
) {
v.borrow_mut().push(n);
let mut children: Vec<&s::Node> = n.nodes.iter().collect();
children.append(&mut n.floating_nodes.iter().collect());
self.sort_by_urgency_and_lru_time_1(&mut children);
for c in children {
self.push_subtree_sorted(c, Rc::clone(&v));
}
}
pub fn get_outputs_workspaces_containers_and_windows(
&self,
) -> Vec<DisplayNode> {
let outputs = self.sorted_nodes_of_type(Type::Output);
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![]));
for o in outputs {
self.push_subtree_sorted(o, Rc::clone(&v));
}
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(1));
x
}
pub fn get_workspaces_containers_and_windows(&self) -> Vec<DisplayNode> {
let workspaces = self.sorted_nodes_of_type(Type::Workspace);
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![]));
for ws in workspaces {
self.push_subtree_sorted(ws, Rc::clone(&v));
}
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(2));
x
}
pub fn is_child_of_tiled_container(&self, id: i64) -> bool {
match self.get_parent_node(id) {
Some(n) => {
n.layout == s::NodeLayout::SplitH
|| n.layout == s::NodeLayout::SplitV
}
None => false,
}
}
pub fn is_child_of_tabbed_or_stacked_container(&self, id: i64) -> bool {
match self.get_parent_node(id) {
Some(n) => {
n.layout == s::NodeLayout::Tabbed
|| n.layout == s::NodeLayout::Stacked
}
None => false,
}
}
}
fn init_id_parent<'a>(
n: &'a s::Node,
parent: Option<&'a s::Node>,
id_node: &mut HashMap<i64, &'a s::Node>,
id_parent: &mut HashMap<i64, i64>,
) {
id_node.insert(n.id, n);
if let Some(p) = parent {
id_parent.insert(n.id, p.id);
}
for c in &n.nodes {
init_id_parent(c, Some(n), id_node, id_parent);
}
for c in &n.floating_nodes {
init_id_parent(c, Some(n), id_node, id_parent);
}
}
pub fn get_tree<'a>(
root: &'a s::Node,
extra_props: &'a HashMap<i64, ExtraProps>,
) -> Tree<'a> {
let mut id_node: HashMap<i64, &s::Node> = HashMap::new();
let mut id_parent: HashMap<i64, i64> = HashMap::new();
init_id_parent(root, None, &mut id_node, &mut id_parent);
Tree {
root,
id_node,
id_parent,
extra_props,
}
}
lazy_static! {
static ref APP_NAME_AND_VERSION_RX: regex::Regex =
regex::Regex::new("(.+)(-[0-9.]+)").unwrap();
static ref PLACEHOLDER_RX: regex::Regex = regex::Regex::new(
r"\{(?P<name>[^}:]+)(?::(?P<fmtstr>\{[^}]*\})(?P<clipstr>[^}]*))?\}"
)
.unwrap();
}
fn maybe_html_escape(do_it: bool, text: String) -> String {
if do_it {
text.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('&', "&amp;")
} else {
text
}
}
fn format_marks(marks: &[String]) -> String {
if marks.is_empty() {
"".to_string()
} else {
format!("[{}]", marks.join(", "))
}
}
impl DisplayFormat for DisplayNode<'_> {
fn format_for_display(&self, cfg: &config::Config) -> String {
let indent = cfg.get_format_indent();
let html_escape = cfg.get_format_html_escape();
let urgency_start = cfg.get_format_urgency_start();
let urgency_end = cfg.get_format_urgency_end();
let icon_dirs = cfg.get_format_icon_dirs();
// fallback_icon has no default value.
let fallback_icon: Option<Box<std::path::Path>> = cfg
.get_format_fallback_icon()
.as_ref()
.map(|i| std::path::Path::new(i).to_owned().into_boxed_path());
let app_name_no_version =
APP_NAME_AND_VERSION_RX.replace(self.node.get_app_name(), "$1");
let fmt = match self.node.get_type() {
Type::Root => String::from("Cannot format Root"),
Type::Output => cfg.get_format_output_format(),
Type::Workspace => cfg.get_format_workspace_format(),
Type::Container => cfg.get_format_container_format(),
Type::Window => cfg.get_format_window_format(),
};
let fmt = fmt
.replace("{indent}", &indent.repeat(self.get_indent_level()))
.replace(
"{urgency_start}",
if self.node.urgent {
urgency_start.as_str()
} else {
""
},
)
.replace(
"{urgency_end}",
if self.node.urgent {
urgency_end.as_str()
} else {
""
},
)
.replace(
"{app_icon}",
util::get_icon(self.node.get_app_name(), &icon_dirs)
.or_else(|| {
util::get_icon(&app_name_no_version, &icon_dirs)
})
.or_else(|| {
util::get_icon(
&app_name_no_version.to_lowercase(),
&icon_dirs,
)
})
.or(fallback_icon)
.map(|i| i.to_string_lossy().into_owned())
.unwrap_or_else(String::new)
.as_str(),
);
PLACEHOLDER_RX
.replace_all(&fmt, |caps: &regex::Captures| {
let value = match &caps["name"] {
"id" => self.node.id.to_string(),
"app_name" => self.node.get_app_name().to_string(),
"layout" => format!("{:?}", self.node.layout),
"name" | "title" => self.node.get_name().to_string(),
"output_name" => self
.tree
.get_parent_node_of_type(self.node.id, Type::Output)
.map_or("<no_output>", |w| w.get_name())
.to_string(),
"workspace_name" => self
.tree
.get_parent_node_of_type(self.node.id, Type::Workspace)
.map_or("<no_workspace>", |w| w.get_name())
.to_string(),
"marks" => format_marks(&self.node.marks),
_ => caps[0].to_string(),
};
let fmt_str = caps.name("fmtstr").map_or("{}", |m| m.as_str());
let clipped_str =
caps.name("clipstr").map_or("", |m| m.as_str());
maybe_html_escape(
html_escape,
rtfmt::format(fmt_str, &value, clipped_str),
)
})
.into()
}
fn get_indent_level(&self) -> usize {
match self.indent_level {
IndentLevel::Fixed(level) => level as usize,
IndentLevel::WorkspacesZeroWindowsOne => {
match self.node.get_type(){
Type::Workspace => 0,
Type::Window => 1,
_ => panic!("Only Workspaces and Windows expected. File a bug report!")
}
}
IndentLevel::TreeDepth(offset) => {
let mut depth: usize = 0;
let mut node = self.node;
while let Some(p) = self.tree.get_parent_node(node.id) {
depth += 1;
node = p;
}
if offset > depth {
0
} else {
depth - offset as usize
}
}
}
}
}
#[test]
fn test_placeholder_rx() {
let caps = PLACEHOLDER_RX.captures("Hello, {place}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr"), None);
assert_eq!(caps.name("clipstr"), None);
let caps = PLACEHOLDER_RX.captures("Hi, {place:{:>10.10}}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:>10.10}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "");
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}…}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "…");
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}...}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "...");
}

@ -0,0 +1,23 @@
[package]
name = "swayr"
version = "0.19.0"
description = "A LRU window-switcher (and more) for the sway window manager"
homepage = "https://sr.ht/~tsdh/swayr/"
repository = "https://git.sr.ht/~tsdh/swayr"
authors = ["Tassilo Horn <tsdh@gnu.org>"]
license = "GPL-3.0+"
edition = "2021"
[dependencies]
clap = {version = "3.0.0", features = ["derive"] }
directories = "4.0"
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex
log = "0.4"
once_cell = "1.10.0"
rand = "0.8.4"
regex = "1.5.5"
rt-format = "0.3.0"
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
swayipc = "3.0.0"
toml = "0.5.8"

@ -1,3 +1,30 @@
swayr v0.19.0
=============
- There's a new command `switch-to-matching-or-urgent-or-lru-window` which
switches to the (first) window matching the given criteria (see section
`CRITERIA` in `sway(5)`) if it exists and is not already focused. Otherwise,
switch to the next urgent window (if any) or to the last recently used
window.
swayr v0.18.0
=============
- The LRU window order will no longer be immediately updated when there is a
focus change. Instead there is now a short (configurable) delay
(`focus.lockin_delay`) before the update. The user-visible change is that
quickly moving over windows with the mouse, or moving through them using
keyboard navigation, will only register the start and destination windows in
the LRU sequence.
- A `nop` command can be used to interrupt a sequence of window-cycling
commands.
swayr v0.17.0
=============
- No user-visible changes but a major restructuring and refactoring in order to
share code between swayr and swayrbar.
swayr v0.16.0 swayr v0.16.0
============= =============

@ -0,0 +1 @@
../README.md

@ -20,5 +20,5 @@ use env_logger::Env;
fn main() { fn main() {
env_logger::Builder::from_env(Env::default().default_filter_or("warn")) env_logger::Builder::from_env(Env::default().default_filter_or("warn"))
.init(); .init();
swayr::demon::run_demon(); swayr::daemon::run_daemon();
} }

@ -16,21 +16,18 @@
//! Functions and data structures of the swayr client. //! Functions and data structures of the swayr client.
use crate::config as cfg; use crate::config as cfg;
use crate::focus::FocusData;
use crate::focus::FocusMessage;
use crate::layout; use crate::layout;
use crate::shared::ipc;
use crate::shared::ipc::NodeMethods;
use crate::tree as t; use crate::tree as t;
use crate::tree::NodeMethods;
use crate::util; use crate::util;
use crate::util::DisplayFormat; use crate::util::DisplayFormat;
use lazy_static::lazy_static; use once_cell::sync::Lazy;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
use std::time::Duration;
use std::time::Instant;
use swayipc as s; use swayipc as s;
pub fn run_sway_command_1(cmd: &str) { pub fn run_sway_command_1(cmd: &str) {
@ -68,6 +65,9 @@ pub enum ConsiderWindows {
#[derive(clap::Parser, Debug, Deserialize, Serialize)] #[derive(clap::Parser, Debug, Deserialize, Serialize)]
pub enum SwayrCommand { pub enum SwayrCommand {
/// No-operation. Interrupts any in-progress prev/next sequence but has
/// no other effect
Nop,
/// Switch to next urgent window (if any) or to last recently used window. /// Switch to next urgent window (if any) or to last recently used window.
SwitchToUrgentOrLRUWindow, SwitchToUrgentOrLRUWindow,
/// Switch to the given app (given by app_id or window class) if that's not /// Switch to the given app (given by app_id or window class) if that's not
@ -86,6 +86,11 @@ pub enum SwayrCommand {
/// "browser" as argument to this command to have a convenient browser <-> /// "browser" as argument to this command to have a convenient browser <->
/// last-recently-used window toggle. /// last-recently-used window toggle.
SwitchToMarkOrUrgentOrLRUWindow { con_mark: String }, SwitchToMarkOrUrgentOrLRUWindow { con_mark: String },
/// Switch to the (first) window matching the given criteria (see section
/// `CRITERIA` in `sway(5)`) if it exists and is not already focused.
/// Otherwise, switch to the next urgent window (if any) or to the last
/// recently used window.
SwitchToMatchingOrUrgentOrLRUWindow { criteria: String },
/// Focus the selected window. /// Focus the selected window.
SwitchWindow, SwitchWindow,
/// Switch to the selected workspace. /// Switch to the selected workspace.
@ -164,8 +169,6 @@ pub enum SwayrCommand {
#[clap(subcommand)] #[clap(subcommand)]
windows: ConsiderWindows, windows: ConsiderWindows,
}, },
/// End current next-prev sequence without doing anything else
EndSequence,
/// Move the currently focused window or container to the selected /// Move the currently focused window or container to the selected
/// workspace. /// workspace.
MoveFocusedToWorkspace, MoveFocusedToWorkspace,
@ -227,8 +230,7 @@ impl SwayrCommand {
pub struct ExecSwayrCmdArgs<'a> { pub struct ExecSwayrCmdArgs<'a> {
pub cmd: &'a SwayrCommand, pub cmd: &'a SwayrCommand,
pub extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, pub focus_data: &'a FocusData,
pub sequence_timeout: Option<Duration>,
} }
impl DisplayFormat for SwayrCommand { impl DisplayFormat for SwayrCommand {
@ -248,99 +250,65 @@ fn always_true(_x: &t::DisplayNode) -> bool {
true true
} }
static IN_NEXT_PREV_WINDOW_SEQ: atomic::AtomicBool =
atomic::AtomicBool::new(false);
lazy_static! {
static ref NEXT_PREV_WINDOW_SEQ_TIMEOUT: Mutex<Option<Instant>> = Mutex::new(None);
}
pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
let props = args.extra_props; let fdata = args.focus_data;
if args.cmd.is_prev_next_window_variant() { if args.cmd.is_prev_next_window_variant() {
let before = fdata.send(FocusMessage::TickUpdateInhibit);
IN_NEXT_PREV_WINDOW_SEQ.swap(true, atomic::Ordering::SeqCst);
if !before
|| NEXT_PREV_WINDOW_SEQ_TIMEOUT
.lock()
.unwrap()
.map_or(false, |i| Instant::now() > i)
{
log::info!("Starting new next/prev sequence");
let mut map = props.write().unwrap();
for val in map.values_mut() {
val.last_focus_tick_for_next_prev_seq = val.last_focus_tick;
}
}
if let Some(timeout) = args.sequence_timeout {
*NEXT_PREV_WINDOW_SEQ_TIMEOUT.lock().unwrap() =
Some(Instant::now() + timeout);
}
} else { } else {
IN_NEXT_PREV_WINDOW_SEQ.store(false, atomic::Ordering::SeqCst); fdata.send(FocusMessage::TickUpdateActivate);
*NEXT_PREV_WINDOW_SEQ_TIMEOUT.lock().unwrap() = None;
} }
match args.cmd { match args.cmd {
SwayrCommand::Nop => {}
SwayrCommand::SwitchToUrgentOrLRUWindow => { SwayrCommand::SwitchToUrgentOrLRUWindow => {
switch_to_urgent_or_lru_window(&*props.read().unwrap()) switch_to_urgent_or_lru_window(fdata)
} }
SwayrCommand::SwitchToAppOrUrgentOrLRUWindow { name } => { SwayrCommand::SwitchToAppOrUrgentOrLRUWindow { name } => {
switch_to_app_or_urgent_or_lru_window( switch_to_app_or_urgent_or_lru_window(name, fdata)
Some(name),
&*props.read().unwrap(),
)
} }
SwayrCommand::SwitchToMarkOrUrgentOrLRUWindow { con_mark } => { SwayrCommand::SwitchToMarkOrUrgentOrLRUWindow { con_mark } => {
switch_to_mark_or_urgent_or_lru_window( switch_to_mark_or_urgent_or_lru_window(con_mark, fdata)
Some(con_mark),
&*props.read().unwrap(),
)
} }
SwayrCommand::SwitchWindow => switch_window(&*props.read().unwrap()), SwayrCommand::SwitchToMatchingOrUrgentOrLRUWindow { criteria } => {
SwayrCommand::SwitchWorkspace => { switch_to_matching_or_urgent_or_lru_window(criteria, fdata)
switch_workspace(&*props.read().unwrap())
} }
SwayrCommand::SwitchOutput => switch_output(&*props.read().unwrap()), SwayrCommand::SwitchWindow => switch_window(fdata),
SwayrCommand::SwitchWorkspace => switch_workspace(fdata),
SwayrCommand::SwitchOutput => switch_output(),
SwayrCommand::SwitchWorkspaceOrWindow => { SwayrCommand::SwitchWorkspaceOrWindow => {
switch_workspace_or_window(&*props.read().unwrap()) switch_workspace_or_window(fdata)
} }
SwayrCommand::SwitchWorkspaceContainerOrWindow => { SwayrCommand::SwitchWorkspaceContainerOrWindow => {
switch_workspace_container_or_window(&*props.read().unwrap()) switch_workspace_container_or_window(fdata)
}
SwayrCommand::SwitchTo => switch_to(&*props.read().unwrap()),
SwayrCommand::QuitWindow { kill } => {
quit_window(&*props.read().unwrap(), *kill)
}
SwayrCommand::QuitWorkspaceOrWindow => {
quit_workspace_or_window(&*props.read().unwrap())
} }
SwayrCommand::SwitchTo => switch_to(fdata),
SwayrCommand::QuitWindow { kill } => quit_window(fdata, *kill),
SwayrCommand::QuitWorkspaceOrWindow => quit_workspace_or_window(fdata),
SwayrCommand::QuitWorkspaceContainerOrWindow => { SwayrCommand::QuitWorkspaceContainerOrWindow => {
quit_workspace_container_or_window(&*props.read().unwrap()) quit_workspace_container_or_window(fdata)
} }
SwayrCommand::MoveFocusedToWorkspace => { SwayrCommand::MoveFocusedToWorkspace => {
move_focused_to_workspace(&*props.read().unwrap()) move_focused_to_workspace(fdata)
}
SwayrCommand::MoveFocusedTo => move_focused_to(&*props.read().unwrap()),
SwayrCommand::SwapFocusedWith => {
swap_focused_with(&*props.read().unwrap())
} }
SwayrCommand::MoveFocusedTo => move_focused_to(fdata),
SwayrCommand::SwapFocusedWith => swap_focused_with(fdata),
SwayrCommand::NextWindow { windows } => focus_window_in_direction( SwayrCommand::NextWindow { windows } => focus_window_in_direction(
Direction::Forward, Direction::Forward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(always_true), Box::new(always_true),
), ),
SwayrCommand::PrevWindow { windows } => focus_window_in_direction( SwayrCommand::PrevWindow { windows } => focus_window_in_direction(
Direction::Backward, Direction::Backward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(always_true), Box::new(always_true),
), ),
SwayrCommand::NextTiledWindow { windows } => focus_window_in_direction( SwayrCommand::NextTiledWindow { windows } => focus_window_in_direction(
Direction::Forward, Direction::Forward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| { Box::new(|dn: &t::DisplayNode| {
!dn.node.is_floating() !dn.node.is_floating()
&& dn.tree.is_child_of_tiled_container(dn.node.id) && dn.tree.is_child_of_tiled_container(dn.node.id)
@ -349,7 +317,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
SwayrCommand::PrevTiledWindow { windows } => focus_window_in_direction( SwayrCommand::PrevTiledWindow { windows } => focus_window_in_direction(
Direction::Backward, Direction::Backward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| { Box::new(|dn: &t::DisplayNode| {
!dn.node.is_floating() !dn.node.is_floating()
&& dn.tree.is_child_of_tiled_container(dn.node.id) && dn.tree.is_child_of_tiled_container(dn.node.id)
@ -359,7 +327,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
focus_window_in_direction( focus_window_in_direction(
Direction::Forward, Direction::Forward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| { Box::new(|dn: &t::DisplayNode| {
!dn.node.is_floating() !dn.node.is_floating()
&& dn && dn
@ -372,7 +340,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
focus_window_in_direction( focus_window_in_direction(
Direction::Backward, Direction::Backward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| { Box::new(|dn: &t::DisplayNode| {
!dn.node.is_floating() !dn.node.is_floating()
&& dn && dn
@ -385,7 +353,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
focus_window_in_direction( focus_window_in_direction(
Direction::Forward, Direction::Forward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| dn.node.is_floating()), Box::new(|dn: &t::DisplayNode| dn.node.is_floating()),
) )
} }
@ -393,7 +361,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
focus_window_in_direction( focus_window_in_direction(
Direction::Backward, Direction::Backward,
windows, windows,
&*props.read().unwrap(), fdata,
Box::new(|dn: &t::DisplayNode| dn.node.is_floating()), Box::new(|dn: &t::DisplayNode| dn.node.is_floating()),
) )
} }
@ -401,19 +369,16 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
focus_window_of_same_layout_in_direction( focus_window_of_same_layout_in_direction(
Direction::Forward, Direction::Forward,
windows, windows,
&*props.read().unwrap(), fdata,
) )
} }
SwayrCommand::PrevWindowOfSameLayout { windows } => { SwayrCommand::PrevWindowOfSameLayout { windows } => {
focus_window_of_same_layout_in_direction( focus_window_of_same_layout_in_direction(
Direction::Backward, Direction::Backward,
windows, windows,
&*props.read().unwrap(), fdata,
) )
} }
SwayrCommand::EndSequence => {
// IN_NEXT_PREV_WINDOW_SEQ set to false above
}
SwayrCommand::TileWorkspace { floating } => { SwayrCommand::TileWorkspace { floating } => {
tile_current_workspace(floating, false) tile_current_workspace(floating, false)
} }
@ -490,8 +455,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) {
{ {
exec_swayr_cmd(ExecSwayrCmdArgs { exec_swayr_cmd(ExecSwayrCmdArgs {
cmd: c, cmd: c,
extra_props: props, focus_data: args.focus_data,
sequence_timeout: args.sequence_timeout,
}); });
} }
} }
@ -506,19 +470,6 @@ fn quit_window_by_id(id: i64) {
run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]); run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]);
} }
pub fn get_tree(include_scratchpad: bool) -> s::Node {
match s::Connection::new() {
Ok(mut con) => {
let mut root = con.get_tree().expect("Got no root node");
if !include_scratchpad {
root.nodes.retain(|o| !o.is_scratchpad());
}
root
}
Err(err) => panic!("{}", err),
}
}
pub fn get_outputs() -> Vec<s::Output> { pub fn get_outputs() -> Vec<s::Output> {
match s::Connection::new() { match s::Connection::new() {
Ok(mut con) => con.get_outputs().expect("Got no outputs"), Ok(mut con) => con.get_outputs().expect("Got no outputs"),
@ -526,38 +477,68 @@ pub fn get_outputs() -> Vec<s::Output> {
} }
} }
pub fn switch_to_urgent_or_lru_window( pub fn switch_to_urgent_or_lru_window(fdata: &FocusData) {
extra_props: &HashMap<i64, t::ExtraProps>, let root = ipc::get_root_node(false);
) { let tree = t::get_tree(&root);
switch_to_app_or_urgent_or_lru_window(None, extra_props) let wins = tree.get_windows(fdata);
focus_win_if_not_focused(None, wins.get(0))
} }
pub fn switch_to_app_or_urgent_or_lru_window( pub fn switch_to_app_or_urgent_or_lru_window(name: &str, fdata: &FocusData) {
name: Option<&str>, let root = ipc::get_root_node(false);
extra_props: &HashMap<i64, t::ExtraProps>, let tree = t::get_tree(&root);
) { let wins = tree.get_windows(fdata);
let root = get_tree(false); let app_win = wins.iter().find(|w| w.node.get_app_name() == name);
let tree = t::get_tree(&root, extra_props);
let wins = tree.get_windows();
let app_win =
name.and_then(|n| wins.iter().find(|w| w.node.get_app_name() == n));
focus_win_if_not_focused(app_win, wins.get(0)) focus_win_if_not_focused(app_win, wins.get(0))
} }
pub fn switch_to_mark_or_urgent_or_lru_window( pub fn switch_to_mark_or_urgent_or_lru_window(
con_mark: Option<&str>, con_mark: &str,
extra_props: &HashMap<i64, t::ExtraProps>, fdata: &FocusData,
) { ) {
let root = get_tree(false); let root = ipc::get_root_node(false);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
let wins = tree.get_windows(); let wins = tree.get_windows(fdata);
let marked_win = con_mark.and_then(|mark| { let con_mark = &con_mark.to_owned();
wins.iter() let marked_win = wins.iter().find(|w| w.node.marks.contains(con_mark));
.find(|w| w.node.marks.contains(&mark.to_owned()))
});
focus_win_if_not_focused(marked_win, wins.get(0)) focus_win_if_not_focused(marked_win, wins.get(0))
} }
fn switch_to_matching_or_urgent_or_lru_window(
criteria: &str,
fdata: &FocusData,
) {
// TODO: It would be great if sway had some command which given a criteria
// query returns the matching windows. Unfortunately, it doesn't have it
// right now. So we call `CRITERION focus` and check if focus has moved.
// If not, we do the "urgent or LRU" thing.
let root = ipc::get_root_node(false);
let tree = t::get_tree(&root);
let wins = tree.get_windows(fdata);
let prev_win_id = wins
.iter()
.find(|w| w.node.focused)
.map(|w| w.node.id)
.unwrap_or(-1);
run_sway_command(&[criteria, "focus"]);
// Wait until the focus event had time to arrive.
std::thread::sleep(std::time::Duration::from_millis(50));
let root = ipc::get_root_node(false);
let tree = t::get_tree(&root);
let wins = tree.get_windows(fdata);
let cur_win_id = wins
.iter()
.find(|w| w.node.focused)
.map(|w| w.node.id)
.unwrap_or(-1);
if prev_win_id == cur_win_id {
focus_win_if_not_focused(None, wins.get(0))
}
}
pub fn focus_win_if_not_focused( pub fn focus_win_if_not_focused(
win: Option<&t::DisplayNode>, win: Option<&t::DisplayNode>,
other: Option<&t::DisplayNode>, other: Option<&t::DisplayNode>,
@ -574,10 +555,8 @@ pub fn focus_win_if_not_focused(
} }
} }
lazy_static! { static DIGIT_AND_NAME: Lazy<Regex> =
static ref DIGIT_AND_NAME: regex::Regex = Lazy::new(|| Regex::new(r"^(\d):(.*)").unwrap());
regex::Regex::new(r"^(\d):(.*)").unwrap();
}
fn create_workspace(ws_name: &str) { fn create_workspace(ws_name: &str) {
if DIGIT_AND_NAME.is_match(ws_name) { if DIGIT_AND_NAME.is_match(ws_name) {
@ -587,12 +566,10 @@ fn create_workspace(ws_name: &str) {
} }
} }
lazy_static! { static SPECIAL_WORKSPACE: Lazy<Regex> =
static ref SPECIAL_WORKSPACE: regex::Regex = Lazy::new(|| Regex::new(r"^#*w:(.*)").unwrap());
regex::Regex::new(r"^#*w:(.*)").unwrap(); static SPECIAL_SWAY: Lazy<regex::Regex> =
static ref SPECIAL_SWAY: regex::Regex = Lazy::new(|| Regex::new(r"^#*s:(.*)").unwrap());
regex::Regex::new(r"^#*s:(.*)").unwrap();
}
fn chop_workspace_shortcut(input: &str) -> &str { fn chop_workspace_shortcut(input: &str) -> &str {
match SPECIAL_WORKSPACE.captures(input) { match SPECIAL_WORKSPACE.captures(input) {
@ -624,17 +601,17 @@ fn handle_non_matching_input(input: &str) {
fn select_and_focus(prompt: &str, choices: &[t::DisplayNode]) { fn select_and_focus(prompt: &str, choices: &[t::DisplayNode]) {
match util::select_from_menu(prompt, choices) { match util::select_from_menu(prompt, choices) {
Ok(tn) => match tn.node.get_type() { Ok(tn) => match tn.node.get_type() {
t::Type::Output => { ipc::Type::Output => {
if !tn.node.is_scratchpad() { if !tn.node.is_scratchpad() {
run_sway_command(&["focus output", tn.node.get_name()]); run_sway_command(&["focus output", tn.node.get_name()]);
} }
} }
t::Type::Workspace => { ipc::Type::Workspace => {
if !tn.node.is_scratchpad() { if !tn.node.is_scratchpad() {
run_sway_command(&["workspace", tn.node.get_name()]); run_sway_command(&["workspace", tn.node.get_name()]);
} }
} }
t::Type::Window | t::Type::Container => { ipc::Type::Window | ipc::Type::Container => {
focus_window_by_id(tn.node.id) focus_window_by_id(tn.node.id)
} }
t => { t => {
@ -647,50 +624,48 @@ fn select_and_focus(prompt: &str, choices: &[t::DisplayNode]) {
} }
} }
pub fn switch_window(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn switch_window(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_focus("Select window", &tree.get_windows()); select_and_focus("Select window", &tree.get_windows(fdata));
} }
pub fn switch_workspace(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn switch_workspace(fdata: &FocusData) {
let root = get_tree(false); let root = ipc::get_root_node(false);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_focus("Select workspace", &tree.get_workspaces()); select_and_focus("Select workspace", &tree.get_workspaces(fdata));
} }
pub fn switch_output(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn switch_output() {
let root = get_tree(false); let root = ipc::get_root_node(false);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_focus("Select output", &tree.get_outputs()); select_and_focus("Select output", &tree.get_outputs());
} }
pub fn switch_workspace_or_window(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn switch_workspace_or_window(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_focus( select_and_focus(
"Select workspace or window", "Select workspace or window",
&tree.get_workspaces_and_windows(), &tree.get_workspaces_and_windows(fdata),
); );
} }
pub fn switch_workspace_container_or_window( pub fn switch_workspace_container_or_window(fdata: &FocusData) {
extra_props: &HashMap<i64, t::ExtraProps>, let root = ipc::get_root_node(true);
) { let tree = t::get_tree(&root);
let root = get_tree(true);
let tree = t::get_tree(&root, extra_props);
select_and_focus( select_and_focus(
"Select workspace, container or window", "Select workspace, container or window",
&tree.get_workspaces_containers_and_windows(), &tree.get_workspaces_containers_and_windows(fdata),
); );
} }
pub fn switch_to(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn switch_to(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_focus( select_and_focus(
"Select output, workspace, container or window", "Select output, workspace, container or window",
&tree.get_outputs_workspaces_containers_and_windows(), &tree.get_outputs_workspaces_containers_and_windows(fdata),
); );
} }
@ -711,14 +686,14 @@ fn kill_process_by_pid(pid: Option<i32>) {
fn select_and_quit(prompt: &str, choices: &[t::DisplayNode], kill: bool) { fn select_and_quit(prompt: &str, choices: &[t::DisplayNode], kill: bool) {
if let Ok(tn) = util::select_from_menu(prompt, choices) { if let Ok(tn) = util::select_from_menu(prompt, choices) {
match tn.node.get_type() { match tn.node.get_type() {
t::Type::Workspace | t::Type::Container => { ipc::Type::Workspace | ipc::Type::Container => {
for win in for win in
tn.node.iter().filter(|n| n.get_type() == t::Type::Window) tn.node.iter().filter(|n| n.get_type() == ipc::Type::Window)
{ {
quit_window_by_id(win.id) quit_window_by_id(win.id)
} }
} }
t::Type::Window => { ipc::Type::Window => {
if kill { if kill {
kill_process_by_pid(tn.node.pid) kill_process_by_pid(tn.node.pid)
} else { } else {
@ -732,30 +707,28 @@ fn select_and_quit(prompt: &str, choices: &[t::DisplayNode], kill: bool) {
} }
} }
pub fn quit_window(extra_props: &HashMap<i64, t::ExtraProps>, kill: bool) { pub fn quit_window(fdata: &FocusData, kill: bool) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_quit("Quit window", &tree.get_windows(), kill); select_and_quit("Quit window", &tree.get_windows(fdata), kill);
} }
pub fn quit_workspace_or_window(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn quit_workspace_or_window(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_quit( select_and_quit(
"Quit workspace or window", "Quit workspace or window",
&tree.get_workspaces_and_windows(), &tree.get_workspaces_and_windows(fdata),
false, false,
); );
} }
pub fn quit_workspace_container_or_window( pub fn quit_workspace_container_or_window(fdata: &FocusData) {
extra_props: &HashMap<i64, t::ExtraProps>, let root = ipc::get_root_node(true);
) { let tree = t::get_tree(&root);
let root = get_tree(true);
let tree = t::get_tree(&root, extra_props);
select_and_quit( select_and_quit(
"Quit workspace, container or window", "Quit workspace, container or window",
&tree.get_workspaces_containers_and_windows(), &tree.get_workspaces_containers_and_windows(fdata),
false, false,
); );
} }
@ -789,7 +762,7 @@ fn move_focused_to_container_or_window(id: i64) {
fn select_and_move_focused_to(prompt: &str, choices: &[t::DisplayNode]) { fn select_and_move_focused_to(prompt: &str, choices: &[t::DisplayNode]) {
match util::select_from_menu(prompt, choices) { match util::select_from_menu(prompt, choices) {
Ok(tn) => match tn.node.get_type() { Ok(tn) => match tn.node.get_type() {
t::Type::Output => { ipc::Type::Output => {
if tn.node.is_scratchpad() { if tn.node.is_scratchpad() {
run_sway_command_1("move container to scratchpad") run_sway_command_1("move container to scratchpad")
} else { } else {
@ -799,14 +772,14 @@ fn select_and_move_focused_to(prompt: &str, choices: &[t::DisplayNode]) {
]) ])
} }
} }
t::Type::Workspace => { ipc::Type::Workspace => {
if tn.node.is_scratchpad() { if tn.node.is_scratchpad() {
run_sway_command_1("move container to scratchpad") run_sway_command_1("move container to scratchpad")
} else { } else {
move_focused_to_workspace_1(tn.node.get_name()) move_focused_to_workspace_1(tn.node.get_name())
} }
} }
t::Type::Container | t::Type::Window => { ipc::Type::Container | ipc::Type::Window => {
move_focused_to_container_or_window(tn.node.id) move_focused_to_container_or_window(tn.node.id)
} }
t => log::error!("Cannot move focused to {:?}", t), t => log::error!("Cannot move focused to {:?}", t),
@ -818,33 +791,33 @@ fn select_and_move_focused_to(prompt: &str, choices: &[t::DisplayNode]) {
} }
} }
pub fn move_focused_to_workspace(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn move_focused_to_workspace(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_move_focused_to( select_and_move_focused_to(
"Move focused container to workspace", "Move focused container to workspace",
&tree.get_workspaces(), &tree.get_workspaces(fdata),
); );
} }
pub fn move_focused_to(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn move_focused_to(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
select_and_move_focused_to( select_and_move_focused_to(
"Move focused container to workspace or container", "Move focused container to workspace or container",
&tree.get_outputs_workspaces_containers_and_windows(), &tree.get_outputs_workspaces_containers_and_windows(fdata),
); );
} }
pub fn swap_focused_with(extra_props: &HashMap<i64, t::ExtraProps>) { pub fn swap_focused_with(fdata: &FocusData) {
let root = get_tree(true); let root = ipc::get_root_node(true);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
match util::select_from_menu( match util::select_from_menu(
"Swap focused with", "Swap focused with",
&tree.get_workspaces_containers_and_windows(), &tree.get_workspaces_containers_and_windows(fdata),
) { ) {
Ok(tn) => match tn.node.get_type() { Ok(tn) => match tn.node.get_type() {
t::Type::Workspace | t::Type::Container | t::Type::Window => { ipc::Type::Workspace | ipc::Type::Container | ipc::Type::Window => {
run_sway_command(&[ run_sway_command(&[
"swap", "swap",
"container", "container",
@ -870,17 +843,17 @@ pub enum Direction {
pub fn focus_window_in_direction( pub fn focus_window_in_direction(
dir: Direction, dir: Direction,
consider_wins: &ConsiderWindows, consider_wins: &ConsiderWindows,
extra_props: &HashMap<i64, t::ExtraProps>, fdata: &FocusData,
pred: Box<dyn Fn(&t::DisplayNode) -> bool>, pred: Box<dyn Fn(&t::DisplayNode) -> bool>,
) { ) {
let root = get_tree(false); let root = ipc::get_root_node(false);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
let mut wins = tree.get_windows(); let mut wins = tree.get_windows(fdata);
if consider_wins == &ConsiderWindows::CurrentWorkspace { if consider_wins == &ConsiderWindows::CurrentWorkspace {
let cur_ws = tree.get_current_workspace(); let cur_ws = tree.get_current_workspace();
wins.retain(|w| { wins.retain(|w| {
tree.get_parent_node_of_type(w.node.id, t::Type::Workspace) tree.get_parent_node_of_type(w.node.id, ipc::Type::Workspace)
.unwrap() .unwrap()
.id .id
== cur_ws.id == cur_ws.id
@ -894,8 +867,8 @@ pub fn focus_window_in_direction(
} }
wins.sort_by(|a, b| { wins.sort_by(|a, b| {
let lru_a = tree.last_focus_tick_for_next_prev_seq(a.node.id); let lru_a = fdata.last_focus_tick(a.node.id);
let lru_b = tree.last_focus_tick_for_next_prev_seq(b.node.id); let lru_b = fdata.last_focus_tick(b.node.id);
lru_a.cmp(&lru_b).reverse() lru_a.cmp(&lru_b).reverse()
}); });
@ -925,18 +898,18 @@ pub fn focus_window_in_direction(
pub fn focus_window_of_same_layout_in_direction( pub fn focus_window_of_same_layout_in_direction(
dir: Direction, dir: Direction,
consider_wins: &ConsiderWindows, consider_wins: &ConsiderWindows,
extra_props: &HashMap<i64, t::ExtraProps>, fdata: &FocusData,
) { ) {
let root = get_tree(false); let root = ipc::get_root_node(false);
let tree = t::get_tree(&root, extra_props); let tree = t::get_tree(&root);
let wins = tree.get_windows(); let wins = tree.get_windows(fdata);
let cur_win = wins.iter().find(|w| w.node.focused); let cur_win = wins.iter().find(|w| w.node.focused);
if let Some(cur_win) = cur_win { if let Some(cur_win) = cur_win {
focus_window_in_direction( focus_window_in_direction(
dir, dir,
consider_wins, consider_wins,
extra_props, fdata,
if cur_win.node.is_floating() { if cur_win.node.is_floating() {
Box::new(|dn| dn.node.is_floating()) Box::new(|dn| dn.node.is_floating())
} else if !cur_win.node.is_floating() } else if !cur_win.node.is_floating()
@ -1039,8 +1012,8 @@ fn tab_current_workspace(floating: &ConsiderFloating) {
} }
fn toggle_tab_tile_current_workspace(floating: &ConsiderFloating) { fn toggle_tab_tile_current_workspace(floating: &ConsiderFloating) {
let tree = get_tree(false); let tree = ipc::get_root_node(false);
let workspaces = tree.nodes_of_type(t::Type::Workspace); let workspaces = tree.nodes_of_type(ipc::Type::Workspace);
let cur_ws = workspaces.iter().find(|w| w.is_current()).unwrap(); let cur_ws = workspaces.iter().find(|w| w.is_current()).unwrap();
if cur_ws.layout == s::NodeLayout::Tabbed { if cur_ws.layout == s::NodeLayout::Tabbed {
tile_current_workspace(floating, true); tile_current_workspace(floating, true);

@ -15,20 +15,17 @@
//! TOML configuration for swayr. //! TOML configuration for swayr.
use directories::ProjectDirs; use crate::shared::cfg;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::DirBuilder; use std::time::Duration;
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize)]
pub struct Config { pub struct Config {
menu: Option<Menu>, menu: Option<Menu>,
format: Option<Format>, format: Option<Format>,
layout: Option<Layout>, layout: Option<Layout>,
sequence: Option<Sequence>, focus: Option<Focus>,
} }
fn tilde_expand_file_names(file_names: Vec<String>) -> Vec<String> { fn tilde_expand_file_names(file_names: Vec<String>) -> Vec<String> {
@ -161,22 +158,35 @@ impl Config {
.expect("No layout.auto_tile_min_window_width_per_output_width defined.") .expect("No layout.auto_tile_min_window_width_per_output_width defined.")
} }
pub fn get_sequence_timeout(&self) -> u64 { pub fn get_focus_lockin_delay(&self) -> Duration {
self.sequence Duration::from_millis(
self.focus
.as_ref() .as_ref()
.and_then(|s| s.timeout) .and_then(|f| f.lockin_delay)
.or_else(|| Sequence::default().timeout) .or_else(|| Focus::default().lockin_delay)
.expect("No sequence.timeout defined") .expect("No focus.lockin_delay defined."),
)
}
pub fn get_focus_sequence_timeout(&self) -> Option<Duration> {
self.focus
.as_ref()
.and_then(|f| f.sequence_timeout)
.or_else(|| Focus::default().sequence_timeout)
.and_then(|d| match d {
0 => None,
_ => Some(Duration::from_millis(d)),
})
} }
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize)]
pub struct Menu { pub struct Menu {
executable: Option<String>, executable: Option<String>,
args: Option<Vec<String>>, args: Option<Vec<String>>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize)]
pub struct Format { pub struct Format {
output_format: Option<String>, output_format: Option<String>,
workspace_format: Option<String>, workspace_format: Option<String>,
@ -190,17 +200,12 @@ pub struct Format {
fallback_icon: Option<String>, fallback_icon: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize)]
pub struct Layout { pub struct Layout {
auto_tile: Option<bool>, auto_tile: Option<bool>,
auto_tile_min_window_width_per_output_width: Option<Vec<[i32; 2]>>, auto_tile_min_window_width_per_output_width: Option<Vec<[i32; 2]>>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Sequence {
timeout: Option<u64>,
}
impl Layout { impl Layout {
pub fn auto_tile_min_window_width_per_output_width_as_map( pub fn auto_tile_min_window_width_per_output_width_as_map(
&self, &self,
@ -217,6 +222,12 @@ impl Layout {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct Focus {
lockin_delay: Option<u64>,
sequence_timeout: Option<u64>,
}
impl Default for Menu { impl Default for Menu {
fn default() -> Self { fn default() -> Self {
Menu { Menu {
@ -308,9 +319,12 @@ impl Default for Layout {
} }
} }
impl Default for Sequence { impl Default for Focus {
fn default() -> Self { fn default() -> Self {
Sequence { timeout: Some(0) } Self {
lockin_delay: Some(750),
sequence_timeout: None,
}
} }
} }
@ -320,77 +334,17 @@ impl Default for Config {
menu: Some(Menu::default()), menu: Some(Menu::default()),
format: Some(Format::default()), format: Some(Format::default()),
layout: Some(Layout::default()), layout: Some(Layout::default()),
sequence: Some(Sequence::default()), focus: Some(Focus::default()),
} }
} }
} }
fn get_config_file_path() -> Box<Path> {
let proj_dirs = ProjectDirs::from("", "", "swayr").expect("");
let user_config_dir = proj_dirs.config_dir();
if !user_config_dir.exists() {
let sys_config_file = Path::new("/etc/xdg/swayr/config.toml");
if sys_config_file.exists() {
return sys_config_file.into();
}
DirBuilder::new()
.recursive(true)
.create(user_config_dir)
.unwrap();
}
user_config_dir.join("config.toml").into_boxed_path()
}
pub fn save_config(cfg: Config) {
let path = get_config_file_path();
let content =
toml::to_string_pretty(&cfg).expect("Cannot serialize config.");
let mut file = OpenOptions::new()
.read(false)
.write(true)
.create(true)
.open(path)
.unwrap();
file.write_all(content.as_str().as_bytes()).unwrap();
}
pub fn load_config() -> Config { pub fn load_config() -> Config {
let path = get_config_file_path(); cfg::load_config::<Config>("swayr")
if !path.exists() {
save_config(Config::default());
// Tell the user that a fresh default config has been created.
std::process::Command::new("swaynag")
.arg("--background")
.arg("00FF44")
.arg("--text")
.arg("0000CC")
.arg("--message")
.arg(
"Welcome to swayr! ".to_owned()
+ "I've created a fresh config for use with wofi for you in "
+ &path.to_string_lossy()
+ ". Adapt it to your needs.",
)
.arg("--type")
.arg("warning")
.arg("--dismiss-button")
.arg("Thanks!")
.spawn()
.ok();
}
let mut file = OpenOptions::new()
.read(true)
.write(false)
.create(false)
.open(path)
.unwrap();
let mut buf: String = String::new();
file.read_to_string(&mut buf).unwrap();
toml::from_str(&buf).expect("Invalid config.")
} }
#[test] #[test]
fn test_load_config() { fn test_load_swayr_config() {
let cfg = load_config(); let cfg = cfg::load_config::<Config>("swayr");
println!("{:?}", cfg); println!("{:?}", cfg);
} }

@ -0,0 +1,412 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Functions and data structures of the swayrd daemon.
use crate::cmds;
use crate::config::{self, Config};
use crate::focus::FocusData;
use crate::focus::FocusEvent;
use crate::focus::FocusMessage;
use crate::layout;
use crate::util;
use std::collections::HashMap;
use std::io::Read;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::RwLock;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use swayipc as s;
pub fn run_daemon() {
let (focus_tx, focus_rx) = mpsc::channel();
let fdata = FocusData {
focus_tick_by_id: Arc::new(RwLock::new(HashMap::new())),
focus_chan: focus_tx,
};
let config = config::load_config();
let lockin_delay = config.get_focus_lockin_delay();
let sequence_timeout = config.get_focus_sequence_timeout();
{
let fdata = fdata.clone();
thread::spawn(move || {
monitor_sway_events(fdata, &config);
});
}
{
let fdata = fdata.clone();
thread::spawn(move || {
focus_lock_in_handler(
focus_rx,
fdata,
lockin_delay,
sequence_timeout,
);
});
}
serve_client_requests(fdata);
}
fn connect_and_subscribe() -> s::Fallible<s::EventStream> {
s::Connection::new()?.subscribe(&[
s::EventType::Window,
s::EventType::Workspace,
s::EventType::Shutdown,
])
}
pub fn monitor_sway_events(fdata: FocusData, config: &Config) {
let mut focus_counter = 0;
let mut resets = 0;
let max_resets = 10;
'reset: loop {
if resets >= max_resets {
break;
}
resets += 1;
log::debug!("Connecting to sway for subscribing to events...");
match connect_and_subscribe() {
Err(err) => {
log::warn!("Could not connect and subscribe: {}", err);
std::thread::sleep(std::time::Duration::from_secs(3));
}
Ok(iter) => {
for ev_result in iter {
let show_extra_props_state;
resets = 0;
match ev_result {
Ok(ev) => match ev {
s::Event::Window(win_ev) => {
focus_counter += 1;
show_extra_props_state = handle_window_event(
win_ev,
&fdata,
config,
focus_counter,
);
}
s::Event::Workspace(ws_ev) => {
focus_counter += 1;
show_extra_props_state = handle_workspace_event(
ws_ev,
&fdata,
focus_counter,
);
}
s::Event::Shutdown(sd_ev) => {
log::debug!(
"Sway shuts down with reason '{:?}'.",
sd_ev.change
);
break 'reset;
}
_ => show_extra_props_state = false,
},
Err(e) => {
log::warn!("Error while receiving events: {}", e);
std::thread::sleep(std::time::Duration::from_secs(
3,
));
show_extra_props_state = false;
log::warn!("Resetting!");
}
}
if show_extra_props_state {
log::debug!(
"New extra_props state:\n{:#?}",
*fdata.focus_tick_by_id.read().unwrap()
);
}
}
}
}
}
log::debug!("Swayr daemon shutting down.")
}
fn handle_window_event(
ev: Box<s::WindowEvent>,
fdata: &FocusData,
config: &config::Config,
focus_val: u64,
) -> bool {
let s::WindowEvent {
change, container, ..
} = *ev;
match change {
s::WindowChange::Focus => {
layout::maybe_auto_tile(config);
fdata.send(FocusMessage::FocusEvent(FocusEvent {
node_id: container.id,
ev_focus_ctr: focus_val,
}));
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::New => {
layout::maybe_auto_tile(config);
fdata.ensure_id(container.id);
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::Close => {
fdata.remove_focus_data(container.id);
layout::maybe_auto_tile(config);
log::debug!("Handled window event type {:?}", change);
true
}
s::WindowChange::Move | s::WindowChange::Floating => {
layout::maybe_auto_tile(config);
log::debug!("Handled window event type {:?}", change);
false // We don't affect the extra_props state here.
}
_ => {
log::debug!("Unhandled window event type {:?}", change);
false
}
}
}
fn handle_workspace_event(
ev: Box<s::WorkspaceEvent>,
fdata: &FocusData,
focus_val: u64,
) -> bool {
let s::WorkspaceEvent {
change,
current,
old: _,
..
} = *ev;
match change {
s::WorkspaceChange::Init | s::WorkspaceChange::Focus => {
let id = current
.expect("No current in Init or Focus workspace event")
.id;
fdata.send(FocusMessage::FocusEvent(FocusEvent {
node_id: id,
ev_focus_ctr: focus_val,
}));
log::debug!("Handled workspace event type {:?}", change);
true
}
s::WorkspaceChange::Empty => {
fdata.remove_focus_data(
current.expect("No current in Empty workspace event").id,
);
log::debug!("Handled workspace event type {:?}", change);
true
}
_ => false,
}
}
pub fn serve_client_requests(fdata: FocusData) {
match std::fs::remove_file(util::get_swayr_socket_path()) {
Ok(()) => log::debug!("Deleted stale socket from previous run."),
Err(e) => log::error!("Could not delete socket:\n{:?}", e),
}
match UnixListener::bind(util::get_swayr_socket_path()) {
Ok(listener) => {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
handle_client_request(stream, &fdata);
}
Err(err) => {
log::error!("Error handling client request: {}", err);
break;
}
}
}
}
Err(err) => {
log::error!("Could not bind socket: {}", err)
}
}
}
fn handle_client_request(mut stream: UnixStream, fdata: &FocusData) {
let mut cmd_str = String::new();
if stream.read_to_string(&mut cmd_str).is_ok() {
if let Ok(cmd) = serde_json::from_str::<cmds::SwayrCommand>(&cmd_str) {
cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs {
cmd: &cmd,
focus_data: fdata,
});
} else {
log::error!(
"Could not serialize following string to SwayrCommand.\n{}",
cmd_str
);
}
} else {
log::error!("Could not read command from client.");
}
}
#[derive(Debug)]
enum InhibitState {
FocusInhibit,
FocusInhibitUntil(Instant),
FocusActive,
}
impl InhibitState {
pub fn is_inhibited(&self) -> bool {
match self {
InhibitState::FocusActive => false,
InhibitState::FocusInhibit => true,
InhibitState::FocusInhibitUntil(t) => &Instant::now() < t,
}
}
pub fn set(&mut self, timeout: Option<Duration>) {
*self = match timeout {
None => match *self {
InhibitState::FocusInhibit => InhibitState::FocusInhibit,
_ => {
log::debug!("Inhibiting tick focus updates");
InhibitState::FocusInhibit
}
},
Some(d) => {
let new_time = Instant::now() + d;
match *self {
// Inhibit only ever gets extended unless clear() is called
InhibitState::FocusInhibit => InhibitState::FocusInhibit,
InhibitState::FocusInhibitUntil(old_time) => {
if old_time > new_time {
InhibitState::FocusInhibitUntil(old_time)
} else {
log::debug!(
"Extending tick focus updates inhibit by {}ms",
(new_time - old_time).as_millis()
);
InhibitState::FocusInhibitUntil(new_time)
}
}
InhibitState::FocusActive => {
log::debug!(
"Inhibiting tick focus updates for {}ms",
d.as_millis()
);
InhibitState::FocusInhibitUntil(new_time)
}
}
}
}
}
pub fn clear(&mut self) {
if self.is_inhibited() {
log::debug!("Activating tick focus updates");
}
*self = InhibitState::FocusActive;
}
}
fn focus_lock_in_handler(
focus_chan: mpsc::Receiver<FocusMessage>,
fdata: FocusData,
lockin_delay: Duration,
sequence_timeout: Option<Duration>,
) {
// Focus event that has not yet been locked-in to the LRU order
let mut pending_fev: Option<FocusEvent> = None;
// Toggle to inhibit LRU focus updates
let mut inhibit = InhibitState::FocusActive;
let update_focus = |fev: Option<FocusEvent>| {
if let Some(fev) = fev {
log::debug!("Locking-in focus on {}", fev.node_id);
fdata.update_last_focus_tick(fev.node_id, fev.ev_focus_ctr)
}
};
// outer loop, waiting for focus events
loop {
let fmsg = match focus_chan.recv() {
Ok(fmsg) => fmsg,
Err(mpsc::RecvError) => return,
};
let mut fev = match fmsg {
FocusMessage::TickUpdateInhibit => {
inhibit.set(sequence_timeout);
continue;
}
FocusMessage::TickUpdateActivate => {
inhibit.clear();
update_focus(pending_fev.take());
continue;
}
FocusMessage::FocusEvent(fev) => {
if inhibit.is_inhibited() {
// update the pending event but take no further action
pending_fev = Some(fev);
continue;
}
fev
}
};
// Inner loop, waiting for the lock-in delay to expire
loop {
let fmsg = match focus_chan.recv_timeout(lockin_delay) {
Ok(fmsg) => fmsg,
Err(mpsc::RecvTimeoutError::Timeout) => {
update_focus(Some(fev));
break; // return to outer loop
}
Err(mpsc::RecvTimeoutError::Disconnected) => return,
};
match fmsg {
FocusMessage::TickUpdateInhibit => {
// inhibit requested before currently focused container
// was locked-in, set it as pending in case no other
// focus changes are made while updates remain inhibited
inhibit.set(sequence_timeout);
pending_fev = Some(fev);
break; // return to outer loop with a preset pending_fev
}
FocusMessage::TickUpdateActivate => {
// updates reactivated while we were waiting to lockin
// Immediately lockin fev
inhibit.clear();
update_focus(Some(fev));
break;
}
FocusMessage::FocusEvent(new_fev) => {
// start a new wait (inner) loop with the most recent
// focus event
fev = new_fev;
}
}
}
}
}

@ -0,0 +1,77 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Structure to hold window focus timestamps used by swayrd
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::RwLock;
/// Data tracking most recent focus events for Sway windows/containers
#[derive(Clone)]
pub struct FocusData {
pub focus_tick_by_id: Arc<RwLock<HashMap<i64, u64>>>,
pub focus_chan: mpsc::Sender<FocusMessage>,
}
impl FocusData {
pub fn last_focus_tick(&self, id: i64) -> u64 {
*self.focus_tick_by_id.read().unwrap().get(&id).unwrap_or(&0)
}
pub fn update_last_focus_tick(&self, id: i64, focus_val: u64) {
let mut write_lock = self.focus_tick_by_id.write().unwrap();
if let Some(tick) = write_lock.get_mut(&id) {
*tick = focus_val;
}
// else the node has since been closed before this focus event got locked in
}
pub fn remove_focus_data(&self, id: i64) {
self.focus_tick_by_id.write().unwrap().remove(&id);
}
/// Ensures that a given node_id is present in the ExtraProps map, this
/// later used to distinguish between the case where a container was
/// closed (it will no longer be in the map) or
pub fn ensure_id(&self, id: i64) {
let mut write_lock = self.focus_tick_by_id.write().unwrap();
if write_lock.get(&id).is_none() {
write_lock.insert(id, 0);
}
}
pub fn send(&self, fmsg: FocusMessage) {
// todo can this be removed?
if let FocusMessage::FocusEvent(ref fev) = fmsg {
self.ensure_id(fev.node_id);
}
self.focus_chan
.send(fmsg)
.expect("Failed to send focus event over channel");
}
}
pub struct FocusEvent {
pub node_id: i64, // node receiving the focus
pub ev_focus_ctr: u64, // Counter for this specific focus event
}
pub enum FocusMessage {
TickUpdateInhibit,
TickUpdateActivate,
FocusEvent(FocusEvent),
}

@ -13,12 +13,11 @@
// You should have received a copy of the GNU General Public License along with // You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>. // this program. If not, see <https://www.gnu.org/licenses/>.
//! Functions and data structures of the swayrd demon. //! Functions and data structures of the swayrd daemon.
use crate::cmds;
use crate::config; use crate::config;
use crate::tree as t; use crate::shared::ipc;
use crate::tree::NodeMethods; use crate::shared::ipc::NodeMethods;
use std::collections::HashMap; use std::collections::HashMap;
use swayipc as s; use swayipc as s;
@ -43,7 +42,7 @@ pub fn auto_tile(res_to_min_width: &HashMap<i32, i32>) {
if let Some(min_window_width) = min_window_width { if let Some(min_window_width) = min_window_width {
for container in output.iter().filter(|n| { for container in output.iter().filter(|n| {
let t = n.get_type(); let t = n.get_type();
t == t::Type::Workspace || t == t::Type::Container t == ipc::Type::Workspace || t == ipc::Type::Container
}) { }) {
if container.is_scratchpad() { if container.is_scratchpad() {
log::debug!(" Skipping scratchpad"); log::debug!(" Skipping scratchpad");
@ -58,7 +57,7 @@ pub fn auto_tile(res_to_min_width: &HashMap<i32, i32>) {
for child_win in container for child_win in container
.nodes .nodes
.iter() .iter()
.filter(|n| n.get_type() == t::Type::Window) .filter(|n| n.get_type() == ipc::Type::Window)
{ {
// Width if we'd split once more. // Width if we'd split once more.
let estimated_width = let estimated_width =
@ -139,16 +138,17 @@ pub fn relayout_current_workspace(
dyn Fn(&mut [&s::Node], &mut s::Connection) -> s::Fallible<()>, dyn Fn(&mut [&s::Node], &mut s::Connection) -> s::Fallible<()>,
>, >,
) -> s::Fallible<()> { ) -> s::Fallible<()> {
let root = cmds::get_tree(false); let root = ipc::get_root_node(false);
let workspaces: Vec<&s::Node> = root let workspaces: Vec<&s::Node> = root
.iter() .iter()
.filter(|n| n.get_type() == t::Type::Workspace) .filter(|n| n.get_type() == ipc::Type::Workspace)
.collect(); .collect();
if let Some(cur_ws) = workspaces.iter().find(|ws| ws.is_current()) { if let Some(cur_ws) = workspaces.iter().find(|ws| ws.is_current()) {
if let Ok(mut con) = s::Connection::new() { if let Ok(mut con) = s::Connection::new() {
let mut moved_wins: Vec<&s::Node> = vec![]; let mut moved_wins: Vec<&s::Node> = vec![];
let mut focused_win = None; let mut focused_win = None;
for win in cur_ws.iter().filter(|n| n.get_type() == t::Type::Window) for win in
cur_ws.iter().filter(|n| n.get_type() == ipc::Type::Window)
{ {
if win.focused { if win.focused {
focused_win = Some(win); focused_win = Some(win);

@ -13,21 +13,17 @@
// You should have received a copy of the GNU General Public License along with // You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>. // this program. If not, see <https://www.gnu.org/licenses/>.
// TODO: Possibly just include README.md when this feature is in the release
// channel.
//
// #![doc(include = "../README.md")]
//! **Swayr** is a LRU window-switcher and more for the sway window manager. //! **Swayr** is a LRU window-switcher and more for the sway window manager.
//! It consists of a demon, and a client. The demon `swayrd` records //! It consists of a daemon, and a client. The `swayrd` daemon records
//! window/workspace creations, deletions, and focus changes using sway's JSON //! window/workspace creations, deletions, and focus changes using sway's JSON
//! IPC interface. The client `swayr` offers subcommands, see `swayr --help`. //! IPC interface. The `swayr` client offers subcommands, see `swayr --help`.
pub mod client; pub mod client;
pub mod cmds; pub mod cmds;
pub mod config; pub mod config;
pub mod demon; pub mod daemon;
pub mod focus;
pub mod layout; pub mod layout;
pub mod rtfmt; pub mod shared;
pub mod tree; pub mod tree;
pub mod util; pub mod util;

@ -0,0 +1,124 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
/// Config file loading stuff.
use directories::ProjectDirs;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fs::{DirBuilder, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
pub fn get_config_file_path(project: &str) -> Box<Path> {
let proj_dirs = ProjectDirs::from("", "", project).expect("");
let user_config_dir = proj_dirs.config_dir();
if !user_config_dir.exists() {
let sys_path = format!("/etc/xdg/{}/config.toml", project);
let sys_config_file = Path::new(sys_path.as_str());
if sys_config_file.exists() {
return sys_config_file.into();
}
DirBuilder::new()
.recursive(true)
.create(user_config_dir)
.unwrap();
}
user_config_dir.join("config.toml").into_boxed_path()
}
pub fn save_config<T>(project: &str, cfg: T)
where
T: Serialize,
{
let path = get_config_file_path(project);
let content =
toml::to_string_pretty::<T>(&cfg).expect("Cannot serialize config.");
let mut file = OpenOptions::new()
.read(false)
.write(true)
.create(true)
.open(path)
.unwrap();
file.write_all(content.as_str().as_bytes()).unwrap();
}
pub fn load_config<T>(project: &str) -> T
where
T: Serialize + DeserializeOwned + Default,
{
let path = get_config_file_path(project);
if !path.exists() {
save_config(project, T::default());
// Tell the user that a fresh default config has been created.
std::process::Command::new("swaynag")
.arg("--background")
.arg("00FF44")
.arg("--text")
.arg("0000CC")
.arg("--message")
.arg(
if project == "swayr" {
"Welcome to swayr! ".to_owned()
+ "I've created a fresh config for use with wofi for you in "
+ &path.to_string_lossy()
+ ". Adapt it to your needs."
} else {
"Welcome to swayrbar! ".to_owned()
+ "I've created a fresh config for for you in "
+ &path.to_string_lossy()
+ ". Adapt it to your needs."
},
)
.arg("--type")
.arg("warning")
.arg("--dismiss-button")
.arg("Thanks!")
.spawn()
.ok();
log::debug!("Created new config in {}.", path.to_string_lossy());
}
load_config_file(&path)
}
pub fn load_config_file<T>(config_file: &Path) -> T
where
T: Serialize + DeserializeOwned + Default,
{
if !config_file.exists() {
panic!(
"Config file {} does not exist.",
config_file.to_string_lossy()
);
} else {
log::debug!("Loading config from {}.", config_file.to_string_lossy());
}
let mut file = OpenOptions::new()
.read(true)
.write(false)
.create(false)
.open(config_file)
.unwrap();
let mut buf: String = String::new();
file.read_to_string(&mut buf).unwrap();
match toml::from_str::<T>(&buf) {
Ok(cfg) => cfg,
Err(err) => {
log::error!("Invalid config: {}", err);
log::error!("Using default configuration.");
T::default()
}
}
}

@ -0,0 +1,254 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
use once_cell::sync::Lazy;
use regex::Regex;
use rt_format::{
Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier,
};
use std::fmt;
pub enum FmtArg {
I64(i64),
I32(i32),
U8(u8),
F64(f64),
F32(f32),
String(String),
}
impl From<i64> for FmtArg {
fn from(x: i64) -> FmtArg {
FmtArg::I64(x)
}
}
impl From<i32> for FmtArg {
fn from(x: i32) -> FmtArg {
FmtArg::I32(x)
}
}
impl From<u8> for FmtArg {
fn from(x: u8) -> FmtArg {
FmtArg::U8(x)
}
}
impl From<f64> for FmtArg {
fn from(x: f64) -> FmtArg {
FmtArg::F64(x)
}
}
impl From<f32> for FmtArg {
fn from(x: f32) -> FmtArg {
FmtArg::F32(x)
}
}
impl From<&str> for FmtArg {
fn from(x: &str) -> FmtArg {
FmtArg::String(x.to_string())
}
}
impl From<String> for FmtArg {
fn from(x: String) -> FmtArg {
FmtArg::String(x)
}
}
impl ToString for FmtArg {
fn to_string(&self) -> String {
match self {
FmtArg::String(x) => x.clone(),
FmtArg::I64(x) => x.to_string(),
FmtArg::I32(x) => x.to_string(),
FmtArg::U8(x) => x.to_string(),
FmtArg::F64(x) => x.to_string(),
FmtArg::F32(x) => x.to_string(),
}
}
}
impl FormatArgument for FmtArg {
fn supports_format(&self, spec: &Specifier) -> bool {
spec.format == Format::Display
}
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::String(val) => fmt::Display::fmt(&val, f),
Self::I64(val) => fmt::Display::fmt(&val, f),
Self::I32(val) => fmt::Display::fmt(&val, f),
Self::U8(val) => fmt::Display::fmt(&val, f),
Self::F64(val) => fmt::Display::fmt(&val, f),
Self::F32(val) => fmt::Display::fmt(&val, f),
}
}
fn fmt_debug(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_octal(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_lower_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_upper_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_binary(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_lower_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
fn fmt_upper_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result {
Err(fmt::Error)
}
}
pub fn rt_format(fmt: &str, arg: FmtArg, clipped_str: &str) -> String {
let arg_string = arg.to_string();
if let Ok(pf) = ParsedFormat::parse(fmt, &[arg], &NoNamedArguments) {
let mut s = format!("{}", pf);
if !clipped_str.is_empty() && !s.contains(arg_string.as_str()) {
remove_last_n_chars(&mut s, clipped_str.chars().count());
s.push_str(clipped_str);
}
s
} else {
format!("Invalid format string: {}", fmt)
}
}
fn remove_last_n_chars(s: &mut String, n: usize) {
match s.char_indices().nth_back(n) {
Some((pos, ch)) => s.truncate(pos + ch.len_utf8()),
None => s.clear(),
}
}
#[test]
fn test_format() {
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), ""), "sway");
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), "…"), "sway");
assert_eq!(rt_format("{:.4}", FmtArg::from("𝔰𝔴𝔞𝔶"), "……"), "𝔰𝔴𝔞𝔶");
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), ""), "swa");
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), "…"), "sw…");
assert_eq!(
rt_format("{:.5}", FmtArg::from("𝔰𝔴𝔞𝔶 𝔴𝔦𝔫𝔡𝔬𝔴"), "…?"),
"𝔰𝔴𝔞…?"
);
assert_eq!(
rt_format("{:.5}", FmtArg::from("sway window"), "..."),
"sw..."
);
assert_eq!(rt_format("{:.2}", FmtArg::from("sway"), "..."), "...");
}
pub static PLACEHOLDER_RX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"\{(?P<name>[^}:]+)(?::(?P<fmtstr>\{[^}]*\})(?P<clipstr>[^}]*))?\}",
)
.unwrap()
});
#[test]
fn test_placeholder_rx() {
let caps = PLACEHOLDER_RX.captures("Hello, {place}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr"), None);
assert_eq!(caps.name("clipstr"), None);
let caps = PLACEHOLDER_RX.captures("Hi, {place:{:>10.10}}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:>10.10}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "");
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}…}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "…");
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}...}!").unwrap();
assert_eq!(caps.name("name").unwrap().as_str(), "place");
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}");
assert_eq!(caps.name("clipstr").unwrap().as_str(), "...");
}
pub fn maybe_html_escape(do_it: bool, text: String) -> String {
if do_it {
text.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('&', "&amp;")
} else {
text
}
}
macro_rules! subst_placeholders {
( $fmt_str:expr, $html_escape:expr,
{ $( $($pat:pat_param)|+ => $exp:expr, )+ }
) => {
$crate::shared::fmt::PLACEHOLDER_RX
.replace_all($fmt_str, |caps: &regex::Captures| {
let value: String = match &caps["name"] {
$(
$( $pat )|+ => {
let val = $crate::shared::fmt::FmtArg::from($exp);
let fmt_str = caps.name("fmtstr")
.map_or("{}", |m| m.as_str());
let clipped_str = caps.name("clipstr")
.map_or("", |m| m.as_str());
$crate::shared::fmt::maybe_html_escape(
$html_escape,
$crate::shared::fmt::rt_format(fmt_str, val, clipped_str),
)
}
)+
_ => caps[0].to_string(),
};
value
}).into()
};
}
pub(crate) use subst_placeholders;
#[test]
fn test_subst_placeholders() {
let foo = "{a}, {b} = {d}";
let html_escape = true;
let x: String = subst_placeholders!(foo, html_escape, {
"a" => "1".to_string(),
"b" | "d" => "2".to_string(),
"c" => "3".to_owned(),
});
assert_eq!("1, 2 = 2", x);
}

@ -0,0 +1,175 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Basic sway IPC.
use std::{cell::RefCell, sync::Mutex};
use once_cell::sync::Lazy;
use swayipc as s;
static SWAY_IPC_CONNECTION: Lazy<Mutex<RefCell<s::Connection>>> =
Lazy::new(|| {
Mutex::new(RefCell::new(
s::Connection::new().expect("Could not open sway IPC connection."),
))
});
pub fn get_root_node(include_scratchpad: bool) -> s::Node {
let mut root = match SWAY_IPC_CONNECTION.lock() {
Ok(cell) => cell.borrow_mut().get_tree().expect("Couldn't get tree"),
Err(err) => panic!("{}", err),
};
if !include_scratchpad {
root.nodes.retain(|o| !o.is_scratchpad());
}
root
}
/// Immutable Node Iterator
///
/// Iterates nodes in depth-first order, tiled nodes before floating nodes.
pub struct NodeIter<'a> {
stack: Vec<&'a s::Node>,
}
impl<'a> NodeIter<'a> {
pub fn new(node: &'a s::Node) -> NodeIter {
NodeIter { stack: vec![node] }
}
}
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a s::Node;
fn next(&mut self) -> Option<Self::Item> {
if let Some(node) = self.stack.pop() {
for n in &node.floating_nodes {
self.stack.push(n);
}
for n in &node.nodes {
self.stack.push(n);
}
Some(node)
} else {
None
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Type {
Root,
Output,
Workspace,
Container,
Window,
}
/// Extension methods for [`swayipc::Node`].
pub trait NodeMethods {
fn iter(&self) -> NodeIter;
fn get_type(&self) -> Type;
fn get_app_name(&self) -> &str;
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node>;
fn get_name(&self) -> &str;
fn is_scratchpad(&self) -> bool;
fn is_floating(&self) -> bool;
fn is_current(&self) -> bool;
}
impl NodeMethods for s::Node {
fn iter(&self) -> NodeIter {
NodeIter::new(self)
}
fn get_type(&self) -> Type {
match self.node_type {
s::NodeType::Root => Type::Root,
s::NodeType::Output => Type::Output,
s::NodeType::Workspace => Type::Workspace,
s::NodeType::FloatingCon => Type::Window,
_ => {
if self.node_type == s::NodeType::Con
&& self.name.is_none()
&& self.app_id.is_none()
&& self.pid.is_none()
&& self.shell.is_none()
&& self.window_properties.is_none()
&& self.layout != s::NodeLayout::None
{
Type::Container
} else if (self.node_type == s::NodeType::Con
|| self.node_type == s::NodeType::FloatingCon)
// Apparently there can be windows without app_id, name,
// and window_properties.class, e.g., dolphin-emu-nogui.
&& self.pid.is_some()
// FIXME: While technically correct, old sway versions (up to
// at least sway-1.4) don't expose shell in IPC. So comment in
// again when all major distros have a recent enough sway
// package.
//&& self.shell.is_some()
{
Type::Window
} else {
panic!(
"Don't know type of node with id {} and node_type {:?}\n{:?}",
self.id, self.node_type, self
)
}
}
}
}
fn get_name(&self) -> &str {
if let Some(name) = &self.name {
name.as_ref()
} else {
"<unnamed>"
}
}
fn get_app_name(&self) -> &str {
if let Some(app_id) = &self.app_id {
app_id
} else if let Some(wp_class) = self
.window_properties
.as_ref()
.and_then(|wp| wp.class.as_ref())
{
wp_class
} else {
"<unknown_app>"
}
}
fn is_scratchpad(&self) -> bool {
let name = self.get_name();
name.eq("__i3") || name.eq("__i3_scratch")
}
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node> {
self.iter().filter(|n| n.get_type() == t).collect()
}
fn is_floating(&self) -> bool {
self.node_type == s::NodeType::FloatingCon
}
fn is_current(&self) -> bool {
self.iter().any(|n| n.focused)
}
}

@ -0,0 +1,18 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
pub mod cfg;
pub mod fmt;
pub mod ipc;

@ -0,0 +1,411 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Convenience data structures built from the IPC structs.
use crate::config;
use crate::focus::FocusData;
use crate::shared::fmt::subst_placeholders;
use crate::shared::ipc;
use crate::shared::ipc::NodeMethods;
use crate::util;
use crate::util::DisplayFormat;
use once_cell::sync::Lazy;
use regex::Regex;
use std::cell::RefCell;
use std::cmp;
use std::collections::HashMap;
use std::rc::Rc;
use swayipc as s;
pub struct Tree<'a> {
root: &'a s::Node,
id_node: HashMap<i64, &'a s::Node>,
id_parent: HashMap<i64, i64>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum IndentLevel {
Fixed(usize),
WorkspacesZeroWindowsOne,
TreeDepth(usize),
}
pub struct DisplayNode<'a> {
pub node: &'a s::Node,
pub tree: &'a Tree<'a>,
indent_level: IndentLevel,
}
impl<'a> Tree<'a> {
fn get_node_by_id(&self, id: i64) -> &&s::Node {
self.id_node
.get(&id)
.unwrap_or_else(|| panic!("No node with id {}", id))
}
fn get_parent_node(&self, id: i64) -> Option<&&s::Node> {
self.id_parent.get(&id).map(|pid| self.get_node_by_id(*pid))
}
pub fn get_parent_node_of_type(
&self,
id: i64,
t: ipc::Type,
) -> Option<&&s::Node> {
let n = self.get_node_by_id(id);
if n.get_type() == t {
Some(n)
} else if let Some(pid) = self.id_parent.get(&id) {
self.get_parent_node_of_type(*pid, t)
} else {
None
}
}
fn sorted_nodes_of_type_1(
&self,
node: &'a s::Node,
t: ipc::Type,
fdata: &FocusData,
) -> Vec<&s::Node> {
let mut v: Vec<&s::Node> = node.nodes_of_type(t);
self.sort_by_urgency_and_lru_time_1(&mut v, fdata);
v
}
fn sorted_nodes_of_type(
&self,
t: ipc::Type,
fdata: &FocusData,
) -> Vec<&s::Node> {
self.sorted_nodes_of_type_1(self.root, t, fdata)
}
fn as_display_nodes(
&self,
v: &[&'a s::Node],
indent_level: IndentLevel,
) -> Vec<DisplayNode> {
v.iter()
.map(|node| DisplayNode {
node,
tree: self,
indent_level,
})
.collect()
}
pub fn get_current_workspace(&self) -> &s::Node {
self.root
.iter()
.find(|n| n.get_type() == ipc::Type::Workspace && n.is_current())
.expect("No current Workspace")
}
pub fn get_outputs(&self) -> Vec<DisplayNode> {
let outputs: Vec<&s::Node> = self
.root
.iter()
.filter(|n| n.get_type() == ipc::Type::Output && !n.is_scratchpad())
.collect();
self.as_display_nodes(&outputs, IndentLevel::Fixed(0))
}
pub fn get_workspaces(&self, fdata: &FocusData) -> Vec<DisplayNode> {
let mut v = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata);
if !v.is_empty() {
v.rotate_left(1);
}
self.as_display_nodes(&v, IndentLevel::Fixed(0))
}
pub fn get_windows(&self, fdata: &FocusData) -> Vec<DisplayNode> {
let mut v = self.sorted_nodes_of_type(ipc::Type::Window, fdata);
// Rotate, but only non-urgent windows. Those should stay at the front
// as they are the most likely switch candidates.
let mut x;
if !v.is_empty() {
x = vec![];
loop {
if !v.is_empty() && v[0].urgent {
x.push(v.remove(0));
} else {
break;
}
}
if !v.is_empty() {
v.rotate_left(1);
x.append(&mut v);
}
} else {
x = v;
}
self.as_display_nodes(&x, IndentLevel::Fixed(0))
}
pub fn get_workspaces_and_windows(
&self,
fdata: &FocusData,
) -> Vec<DisplayNode> {
let workspaces = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata);
let mut first = true;
let mut v = vec![];
for ws in workspaces {
v.push(ws);
let mut wins =
self.sorted_nodes_of_type_1(ws, ipc::Type::Window, fdata);
if first && !wins.is_empty() {
wins.rotate_left(1);
first = false;
}
v.append(&mut wins);
}
self.as_display_nodes(&v, IndentLevel::WorkspacesZeroWindowsOne)
}
fn sort_by_urgency_and_lru_time_1(
&self,
v: &mut [&s::Node],
fdata: &FocusData,
) {
v.sort_by(|a, b| {
if a.urgent && !b.urgent {
cmp::Ordering::Less
} else if !a.urgent && b.urgent {
cmp::Ordering::Greater
} else {
let lru_a = fdata.last_focus_tick(a.id);
let lru_b = fdata.last_focus_tick(b.id);
lru_a.cmp(&lru_b).reverse()
}
});
}
fn push_subtree_sorted(
&self,
n: &'a s::Node,
v: Rc<RefCell<Vec<&'a s::Node>>>,
fdata: &FocusData,
) {
v.borrow_mut().push(n);
let mut children: Vec<&s::Node> = n.nodes.iter().collect();
children.append(&mut n.floating_nodes.iter().collect());
self.sort_by_urgency_and_lru_time_1(&mut children, fdata);
for c in children {
self.push_subtree_sorted(c, Rc::clone(&v), fdata);
}
}
pub fn get_outputs_workspaces_containers_and_windows(
&self,
fdata: &FocusData,
) -> Vec<DisplayNode> {
let outputs = self.sorted_nodes_of_type(ipc::Type::Output, fdata);
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![]));
for o in outputs {
self.push_subtree_sorted(o, Rc::clone(&v), fdata);
}
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(1));
x
}
pub fn get_workspaces_containers_and_windows(
&self,
fdata: &FocusData,
) -> Vec<DisplayNode> {
let workspaces = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata);
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![]));
for ws in workspaces {
self.push_subtree_sorted(ws, Rc::clone(&v), fdata);
}
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(2));
x
}
pub fn is_child_of_tiled_container(&self, id: i64) -> bool {
match self.get_parent_node(id) {
Some(n) => {
n.layout == s::NodeLayout::SplitH
|| n.layout == s::NodeLayout::SplitV
}
None => false,
}
}
pub fn is_child_of_tabbed_or_stacked_container(&self, id: i64) -> bool {
match self.get_parent_node(id) {
Some(n) => {
n.layout == s::NodeLayout::Tabbed
|| n.layout == s::NodeLayout::Stacked
}
None => false,
}
}
}
fn init_id_parent<'a>(
n: &'a s::Node,
parent: Option<&'a s::Node>,
id_node: &mut HashMap<i64, &'a s::Node>,
id_parent: &mut HashMap<i64, i64>,
) {
id_node.insert(n.id, n);
if let Some(p) = parent {
id_parent.insert(n.id, p.id);
}
for c in &n.nodes {
init_id_parent(c, Some(n), id_node, id_parent);
}
for c in &n.floating_nodes {
init_id_parent(c, Some(n), id_node, id_parent);
}
}
pub fn get_tree(root: &s::Node) -> Tree {
let mut id_node: HashMap<i64, &s::Node> = HashMap::new();
let mut id_parent: HashMap<i64, i64> = HashMap::new();
init_id_parent(root, None, &mut id_node, &mut id_parent);
Tree {
root,
id_node,
id_parent,
}
}
static APP_NAME_AND_VERSION_RX: Lazy<Regex> =
Lazy::new(|| Regex::new("(.+)(-[0-9.]+)").unwrap());
fn format_marks(marks: &[String]) -> String {
if marks.is_empty() {
"".to_string()
} else {
format!("[{}]", marks.join(", "))
}
}
impl DisplayFormat for DisplayNode<'_> {
fn format_for_display(&self, cfg: &config::Config) -> String {
let indent = cfg.get_format_indent();
let html_escape = cfg.get_format_html_escape();
let urgency_start = cfg.get_format_urgency_start();
let urgency_end = cfg.get_format_urgency_end();
let icon_dirs = cfg.get_format_icon_dirs();
// fallback_icon has no default value.
let fallback_icon: Option<Box<std::path::Path>> = cfg
.get_format_fallback_icon()
.as_ref()
.map(|i| std::path::Path::new(i).to_owned().into_boxed_path());
let app_name_no_version =
APP_NAME_AND_VERSION_RX.replace(self.node.get_app_name(), "$1");
let fmt = match self.node.get_type() {
ipc::Type::Root => String::from("Cannot format Root"),
ipc::Type::Output => cfg.get_format_output_format(),
ipc::Type::Workspace => cfg.get_format_workspace_format(),
ipc::Type::Container => cfg.get_format_container_format(),
ipc::Type::Window => cfg.get_format_window_format(),
};
let fmt = fmt
.replace(
"{indent}",
indent.repeat(self.get_indent_level()).as_str(),
)
.replace(
"{urgency_start}",
if self.node.urgent {
urgency_start.as_str()
} else {
""
},
)
.replace(
"{urgency_end}",
if self.node.urgent {
urgency_end.as_str()
} else {
""
},
)
.replace(
"{app_icon}",
util::get_icon(self.node.get_app_name(), &icon_dirs)
.or_else(|| {
util::get_icon(&app_name_no_version, &icon_dirs)
})
.or_else(|| {
util::get_icon(
&app_name_no_version.to_lowercase(),
&icon_dirs,
)
})
.or(fallback_icon)
.map(|i| i.to_string_lossy().into_owned())
.unwrap_or_else(String::new)
.as_str(),
);
subst_placeholders!(&fmt, html_escape, {
"id" => self.node.id,
"app_name" => self.node.get_app_name(),
"layout" => format!("{:?}", self.node.layout),
"name" | "title" => self.node.get_name(),
"output_name" => self
.tree
.get_parent_node_of_type(self.node.id, ipc::Type::Output)
.map_or("<no_output>", |w| w.get_name()),
"workspace_name" => self
.tree
.get_parent_node_of_type(self.node.id, ipc::Type::Workspace)
.map_or("<no_workspace>", |w| w.get_name()),
"marks" => format_marks(&self.node.marks),
})
}
fn get_indent_level(&self) -> usize {
match self.indent_level {
IndentLevel::Fixed(level) => level as usize,
IndentLevel::WorkspacesZeroWindowsOne => {
match self.node.get_type(){
ipc::Type::Workspace => 0,
ipc::Type::Window => 1,
_ => panic!("Only Workspaces and Windows expected. File a bug report!")
}
}
IndentLevel::TreeDepth(offset) => {
let mut depth: usize = 0;
let mut node = self.node;
while let Some(p) = self.tree.get_parent_node(node.id) {
depth += 1;
node = p;
}
if offset > depth {
0
} else {
depth - offset as usize
}
}
}
}
}

@ -15,12 +15,15 @@
//! Utility functions including selection between choices using a menu program. //! Utility functions including selection between choices using a menu program.
use once_cell::sync::Lazy;
use regex::Regex;
use crate::config as cfg; use crate::config as cfg;
use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use std::path as p; use std::path as p;
use std::process as proc; use std::process as proc;
use std::sync::Mutex;
pub fn get_swayr_socket_path() -> String { pub fn get_swayr_socket_path() -> String {
// We prefer checking the env variable instead of // We prefer checking the env variable instead of
@ -120,12 +123,10 @@ fn find_icon(icon_name: &str, icon_dirs: &[String]) -> Option<Box<p::Path>> {
None None
} }
lazy_static! { static WM_CLASS_OR_ICON_RX: Lazy<Regex> =
static ref WM_CLASS_OR_ICON_RX: regex::Regex = Lazy::new(|| Regex::new(r"(StartupWMClass|Icon)=(.+)").unwrap());
regex::Regex::new(r"(StartupWMClass|Icon)=(.+)").unwrap(); static REV_DOMAIN_NAME_RX: Lazy<Regex> =
static ref REV_DOMAIN_NAME_RX: regex::Regex = Lazy::new(|| Regex::new(r"^(?:[a-zA-Z0-9-]+\.)+([a-zA-Z0-9-]+)$").unwrap());
regex::Regex::new(r"^(?:[a-zA-Z0-9-]+\.)+([a-zA-Z0-9-]+)$").unwrap();
}
fn get_app_id_to_icon_map( fn get_app_id_to_icon_map(
icon_dirs: &[String], icon_dirs: &[String],
@ -195,10 +196,10 @@ fn get_app_id_to_icon_map(
map map
} }
lazy_static! { // Well, this type definition is pretty useless since it's only used in
static ref APP_ID_TO_ICON_MAP: std::sync::Mutex<Option<HashMap<String, Box<p::Path>>>> = // get_icon anyway but clippy suggested it...
std::sync::Mutex::new(None); type AppIdToIconMap = Lazy<Mutex<Option<HashMap<String, Box<p::Path>>>>>;
} static APP_ID_TO_ICON_MAP: AppIdToIconMap = Lazy::new(|| Mutex::new(None));
pub fn get_icon(app_id: &str, icon_dirs: &[String]) -> Option<Box<p::Path>> { pub fn get_icon(app_id: &str, icon_dirs: &[String]) -> Option<Box<p::Path>> {
let mut opt = APP_ID_TO_ICON_MAP.lock().unwrap(); let mut opt = APP_ID_TO_ICON_MAP.lock().unwrap();

@ -0,0 +1,26 @@
[package]
name = "swayrbar"
version = "0.2.2"
edition = "2021"
homepage = "https://sr.ht/~tsdh/swayr/#swayrbar"
repository = "https://git.sr.ht/~tsdh/swayr"
description = "A swaybar-protocol implementation for sway/swaybar"
authors = ["Tassilo Horn <tsdh@gnu.org>"]
license = "GPL-3.0+"
[dependencies]
clap = {version = "3.0.0", features = ["derive"] }
battery = "0.7.8"
chrono = "0.4"
directories = "4.0"
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex
log = "0.4"
once_cell = "1.10.0"
regex = "1.5.5"
rt-format = "0.3.0"
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
swaybar-types = "3.0.0"
swayipc = "3.0.0"
sysinfo = "0.23"
toml = "0.5.8"

@ -0,0 +1,15 @@
swayrbar 0.2.0
==============
- If a window module is used, subscribe to sway events in order to immediately
refresh it on window/workspace changes.
swayrbar 0.1.1
==============
- Only refresh the module which received the click event.
swayrbar 0.1.0
==============
- Add pactl module.

@ -0,0 +1 @@
../README.md

@ -0,0 +1,317 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! `swayrbar` lib.
use crate::config;
use crate::module;
use crate::module::{BarModuleFn, NameInstanceAndReason, RefreshReason};
use env_logger::Env;
use serde_json;
use std::io;
use std::path::Path;
use std::process as p;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::time::Duration;
use std::{sync::Arc, thread};
use swaybar_types as sbt;
use swayipc as si;
#[derive(clap::Parser)]
#[clap(about, version, author)]
pub struct Opts {
#[clap(
short = 'c',
long,
help = "Path to a config.toml configuration file.
If not specified, the default config ~/.config/swayrbar/config.toml or
/etc/xdg/swayrbar/config.toml is used."
)]
config_file: Option<String>,
}
pub fn start(opts: Opts) {
env_logger::Builder::from_env(Env::default().default_filter_or("warn"))
.init();
let config = match opts.config_file {
None => config::load_config(),
Some(config_file) => {
let path = Path::new(&config_file);
crate::shared::cfg::load_config_file(path)
}
};
let refresh_interval = config.refresh_interval;
let mods: Arc<Vec<Box<dyn BarModuleFn>>> = Arc::new(create_modules(config));
let mods_for_input = mods.clone();
let (sender, receiver) = sync_channel(16);
let sender_for_ticker = sender.clone();
thread::spawn(move || {
tick_periodically(refresh_interval, sender_for_ticker)
});
let sender_for_input = sender.clone();
thread::spawn(move || handle_input(mods_for_input, sender_for_input));
let window_mods: Vec<(String, String)> = mods
.iter()
.filter(|m| m.get_config().name == "window")
.map(|m| (m.get_config().name.clone(), m.get_config().instance.clone()))
.collect();
if !window_mods.is_empty() {
// There's at least one window module, so subscribe to focus events for
// immediate refreshes.
thread::spawn(move || handle_sway_events(window_mods, sender));
}
generate_status(&mods, receiver);
}
fn tick_periodically(
refresh_interval: u64,
sender: SyncSender<Option<NameInstanceAndReason>>,
) {
loop {
send_refresh_event(&sender, None);
thread::sleep(Duration::from_millis(refresh_interval));
}
}
fn create_modules(config: config::Config) -> Vec<Box<dyn BarModuleFn>> {
let mut mods = vec![];
for mc in config.modules {
let m = match mc.name.as_str() {
"window" => module::window::BarModuleWindow::create(mc),
"sysinfo" => module::sysinfo::BarModuleSysInfo::create(mc),
"battery" => module::battery::BarModuleBattery::create(mc),
"date" => module::date::BarModuleDate::create(mc),
"pactl" => module::pactl::BarModulePactl::create(mc),
unknown => {
log::warn!("Unknown module name '{}'. Ignoring...", unknown);
continue;
}
};
mods.push(m);
}
mods
}
fn handle_input(
mods: Arc<Vec<Box<dyn BarModuleFn>>>,
sender: SyncSender<Option<NameInstanceAndReason>>,
) {
let mut sb = String::new();
io::stdin()
.read_line(&mut sb)
.expect("Could not read from stdin");
if "[\n" != sb {
log::error!("Expected [\\n but got {}", sb);
log::error!("Sorry, input events won't work is this session.");
return;
}
loop {
let mut buf = String::new();
if let Err(err) = io::stdin().read_line(&mut buf) {
log::error!("Error while reading from stdin: {}", err);
log::error!("Skipping this input line...");
continue;
}
let click = match serde_json::from_str::<sbt::Click>(
buf.strip_prefix(',').unwrap_or(&buf),
) {
Ok(click) => click,
Err(err) => {
log::error!("Error while parsing str to Click: {}", err);
log::error!("The string was '{}'.", buf);
log::error!("Skipping this input line...");
continue;
}
};
log::debug!("Received click: {:?}", click);
let event = handle_click(click, mods.clone());
if event.is_some() {
send_refresh_event(&sender, event);
}
}
}
fn send_refresh_event(
sender: &SyncSender<Option<NameInstanceAndReason>>,
event: Option<NameInstanceAndReason>,
) {
if event.is_some() {
log::debug!("Sending refresh event {:?}", event);
}
if let Err(err) = sender.send(event) {
log::error!("Error at send: {}", err);
}
}
fn handle_click(
click: sbt::Click,
mods: Arc<Vec<Box<dyn BarModuleFn>>>,
) -> Option<NameInstanceAndReason> {
let name = click.name?;
let instance = click.instance?;
let button_str = format!("{:?}", click.button);
for m in mods.iter() {
if let Some(on_click) = m.get_on_click_map(&name, &instance) {
if let Some(cmd) = on_click.get(&button_str) {
match m.subst_args(cmd) {
Some(cmd) => execute_command(&cmd),
None => execute_command(cmd),
}
let cfg = m.get_config();
// No refresh for click events for window modules because the
// refresh will be triggered by a sway event anyhow.
//
// TODO: That's too much coupling. The bar module shouldn't do
// specific stuff for certain modules.
if cfg.name == module::window::NAME {
return None;
}
return Some((
cfg.name.clone(),
cfg.instance.clone(),
RefreshReason::ClickEvent,
));
}
}
}
None
}
fn execute_command(cmd: &[String]) {
log::debug!("Executing command: {:?}", cmd);
match p::Command::new(&cmd[0]).args(&cmd[1..]).status() {
Ok(exit_status) => {
// TODO: Better use exit_ok() once that has stabilized.
if !exit_status.success() {
log::warn!(
"Command finished with status code {:?}.",
exit_status.code()
)
}
}
Err(err) => {
log::error!("Error running shell command '{}':", cmd.join(" "));
log::error!("{}", err);
}
}
}
fn sway_subscribe() -> si::Fallible<si::EventStream> {
si::Connection::new()?.subscribe(&[
si::EventType::Window,
si::EventType::Shutdown,
si::EventType::Workspace,
])
}
fn handle_sway_events(
window_mods: Vec<(String, String)>,
sender: SyncSender<Option<NameInstanceAndReason>>,
) {
let mut resets = 0;
let max_resets = 10;
'reset: loop {
if resets >= max_resets {
break;
}
resets += 1;
log::debug!("Connecting to sway for subscribing to events...");
match sway_subscribe() {
Err(err) => {
log::warn!("Could not connect and subscribe: {}", err);
std::thread::sleep(std::time::Duration::from_secs(3));
}
Ok(iter) => {
for ev_result in iter {
resets = 0;
match ev_result {
Ok(ev) => match ev {
si::Event::Window(_) | si::Event::Workspace(_) => {
log::trace!(
"Window or Workspace event: {:?}",
ev
);
for m in &window_mods {
let event = Some((
m.0.to_owned(),
m.1.to_owned(),
RefreshReason::SwayEvent,
));
send_refresh_event(&sender, event);
}
}
si::Event::Shutdown(sd_ev) => {
log::debug!(
"Sway shuts down with reason '{:?}'.",
sd_ev.change
);
break 'reset;
}
_ => (),
},
Err(e) => {
log::warn!("Error while receiving events: {}", e);
std::thread::sleep(std::time::Duration::from_secs(
3,
));
log::warn!("Resetting!");
}
}
}
}
}
}
}
fn generate_status_1(
mods: &[Box<dyn BarModuleFn>],
name_and_instance: &Option<NameInstanceAndReason>,
) {
let mut blocks = vec![];
for m in mods {
blocks.push(m.build(name_and_instance));
}
let json = serde_json::to_string_pretty(&blocks)
.unwrap_or_else(|_| "".to_string());
println!("{},", json);
}
fn generate_status(
mods: &[Box<dyn BarModuleFn>],
receiver: Receiver<Option<NameInstanceAndReason>>,
) {
println!("{{\"version\": 1, \"click_events\": true}}");
// status_command should output an infinite array meaning we emit an
// opening [ and never the closing bracket.
println!("[");
for ev in receiver.iter() {
generate_status_1(mods, &ev)
}
}

@ -0,0 +1,24 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The `swayrbar` binary.
use clap::Parser;
use swayrbar::bar::Opts;
fn main() {
let opts: Opts = Opts::parse();
swayrbar::bar::start(opts);
}

@ -0,0 +1,80 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! TOML configuration for swayrbar.
use crate::module::BarModuleFn;
use crate::shared::cfg;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
/// The status is refreshed every `refresh_interval` milliseconds.
pub refresh_interval: u64,
/// The list of modules to display in the given order, each one specified
/// as `"<module_type>/<instance>"`.
pub modules: Vec<ModuleConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ModuleConfig {
pub name: String,
pub instance: String,
pub format: String,
pub html_escape: Option<bool>,
pub on_click: Option<HashMap<String, Vec<String>>>,
}
impl ModuleConfig {
pub fn is_html_escape(&self) -> bool {
self.html_escape.unwrap_or(false)
}
}
impl Default for Config {
fn default() -> Self {
Config {
refresh_interval: 1000,
modules: vec![
crate::module::window::BarModuleWindow::default_config(
"0".to_owned(),
),
crate::module::sysinfo::BarModuleSysInfo::default_config(
"0".to_owned(),
),
crate::module::battery::BarModuleBattery::default_config(
"0".to_owned(),
),
crate::module::pactl::BarModulePactl::default_config(
"0".to_owned(),
),
crate::module::date::BarModuleDate::default_config(
"0".to_owned(),
),
],
}
}
}
pub fn load_config() -> Config {
cfg::load_config::<Config>("swayrbar")
}
#[test]
fn test_load_swayrbar_config() {
let cfg = cfg::load_config::<Config>("swayrbar");
println!("{:?}", cfg);
}

@ -0,0 +1,19 @@
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
pub mod bar;
pub mod config;
pub mod module;
pub mod shared;

@ -0,0 +1,79 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
use std::collections::HashMap;
use crate::config;
use swaybar_types as s;
pub mod battery;
pub mod date;
pub mod pactl;
pub mod sysinfo;
pub mod window;
#[derive(Debug, PartialEq)]
pub enum RefreshReason {
ClickEvent,
SwayEvent,
}
pub type NameInstanceAndReason = (String, String, RefreshReason);
pub trait BarModuleFn: Sync + Send {
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn>
where
Self: Sized;
fn default_config(instance: String) -> config::ModuleConfig
where
Self: Sized;
fn get_config(&self) -> &config::ModuleConfig;
fn get_on_click_map(
&self,
name: &str,
instance: &str,
) -> Option<&HashMap<String, Vec<String>>> {
let cfg = self.get_config();
if name == cfg.name && instance == cfg.instance {
cfg.on_click.as_ref()
} else {
None
}
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block;
fn should_refresh(
&self,
nai: &Option<NameInstanceAndReason>,
periodic: bool,
reasons: &[RefreshReason],
) -> bool {
let cfg = self.get_config();
match nai {
None => periodic,
Some((n, i, r)) => {
n == &cfg.name
&& i == &cfg.instance
&& reasons.iter().any(|x| x == r)
}
}
}
fn subst_args<'a>(&'a self, _cmd: &'a [String]) -> Option<Vec<String>>;
}

@ -0,0 +1,174 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The date `swayrbar` module.
use crate::config;
use crate::module::{BarModuleFn, NameInstanceAndReason};
use crate::shared::fmt::subst_placeholders;
use battery as bat;
use std::collections::HashSet;
use std::sync::Mutex;
use swaybar_types as s;
const NAME: &str = "battery";
struct State {
state_of_charge: f32,
state_of_health: f32,
state: String,
cached_text: String,
}
pub struct BarModuleBattery {
config: config::ModuleConfig,
state: Mutex<State>,
}
fn get_refreshed_batteries(
manager: &bat::Manager,
) -> Result<Vec<bat::Battery>, bat::Error> {
let mut bats = vec![];
for bat in manager.batteries()? {
let mut bat = bat?;
if manager.refresh(&mut bat).is_ok() {
bats.push(bat);
}
}
Ok(bats)
}
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) {
// FIXME: Creating the Manager on every refresh is bad but internally
// it uses an Rc so if I keep it as a field of BarModuleBattery, that
// cannot be Sync.
let manager = battery::Manager::new().unwrap();
match get_refreshed_batteries(&manager) {
Ok(bats) => {
state.state_of_charge =
bats.iter().map(|b| b.state_of_charge().value).sum::<f32>()
/ bats.len() as f32
* 100_f32;
state.state_of_health =
bats.iter().map(|b| b.state_of_health().value).sum::<f32>()
/ bats.len() as f32
* 100_f32;
state.state = {
let states = bats
.iter()
.map(|b| format!("{:?}", b.state()))
.collect::<HashSet<String>>();
if states.len() == 1 {
states.iter().next().unwrap().to_owned()
} else {
let mut comma_sep_string = String::from("[");
let mut first = true;
for state in states {
if first {
comma_sep_string = comma_sep_string + &state;
first = false;
} else {
comma_sep_string = comma_sep_string + ", " + &state;
}
}
comma_sep_string += "]";
comma_sep_string
}
};
state.cached_text = subst_placeholders(fmt_str, html_escape, state);
}
Err(err) => {
log::error!("Could not update battery state: {}", err);
}
}
}
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String {
subst_placeholders!(fmt, html_escape, {
"state_of_charge" => state.state_of_charge,
"state_of_health" => state.state_of_health,
"state" => state.state.as_str(),
})
}
impl BarModuleFn for BarModuleBattery {
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> {
Box::new(BarModuleBattery {
config,
state: Mutex::new(State {
state_of_charge: 0.0,
state_of_health: 0.0,
state: "Unknown".to_owned(),
cached_text: String::new(),
}),
})
}
fn default_config(instance: String) -> config::ModuleConfig {
config::ModuleConfig {
name: NAME.to_owned(),
instance,
format: "🔋 Bat: {state_of_charge:{:5.1}}%, {state}, Health: {state_of_health:{:5.1}}%".to_owned(),
html_escape: Some(false),
on_click: None,
}
}
fn get_config(&self) -> &config::ModuleConfig {
&self.config
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block {
let mut state = self.state.lock().expect("Could not lock state.");
if self.should_refresh(nai, true, &[]) {
refresh_state(
&mut state,
&self.config.format,
self.get_config().is_html_escape(),
);
}
s::Block {
name: Some(NAME.to_owned()),
instance: Some(self.config.instance.clone()),
full_text: state.cached_text.to_owned(),
align: Some(s::Align::Left),
markup: Some(s::Markup::Pango),
short_text: None,
color: None,
background: None,
border: None,
border_top: None,
border_bottom: None,
border_left: None,
border_right: None,
min_width: None,
urgent: None,
separator: Some(true),
separator_block_width: None,
}
}
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> {
let state = self.state.lock().expect("Could not lock state.");
Some(
cmd.iter()
.map(|arg| subst_placeholders(arg, false, &state))
.collect(),
)
}
}

@ -0,0 +1,94 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The date `swayrbar` module.
use std::sync::Mutex;
use crate::module::config;
use crate::module::{BarModuleFn, NameInstanceAndReason};
use swaybar_types as s;
const NAME: &str = "date";
struct State {
cached_text: String,
}
pub struct BarModuleDate {
config: config::ModuleConfig,
state: Mutex<State>,
}
fn chrono_format(s: &str) -> String {
chrono::Local::now().format(s).to_string()
}
impl BarModuleFn for BarModuleDate {
fn create(cfg: config::ModuleConfig) -> Box<dyn BarModuleFn> {
Box::new(BarModuleDate {
config: cfg,
state: Mutex::new(State {
cached_text: String::new(),
}),
})
}
fn default_config(instance: String) -> config::ModuleConfig {
config::ModuleConfig {
name: NAME.to_owned(),
instance,
format: "⏰ %F %X".to_owned(),
html_escape: Some(false),
on_click: None,
}
}
fn get_config(&self) -> &config::ModuleConfig {
&self.config
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block {
let mut state = self.state.lock().expect("Could not lock state.");
if self.should_refresh(nai, true, &[]) {
state.cached_text = chrono_format(&self.config.format);
}
s::Block {
name: Some(NAME.to_owned()),
instance: Some(self.config.instance.clone()),
full_text: state.cached_text.to_owned(),
align: Some(s::Align::Left),
markup: Some(s::Markup::Pango),
short_text: None,
color: None,
background: None,
border: None,
border_top: None,
border_bottom: None,
border_left: None,
border_right: None,
min_width: None,
urgent: None,
separator: Some(true),
separator_block_width: None,
}
}
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> {
Some(cmd.iter().map(|arg| chrono_format(arg)).collect())
}
}

@ -0,0 +1,190 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The pactl `swayrbar` module.
use crate::config;
use crate::module::{BarModuleFn, NameInstanceAndReason};
use crate::shared::fmt::subst_placeholders;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
use std::process::Command;
use std::sync::Mutex;
use swaybar_types as s;
use super::RefreshReason;
const NAME: &str = "pactl";
struct State {
volume: u8,
muted: bool,
cached_text: String,
}
pub static VOLUME_RX: Lazy<Regex> =
Lazy::new(|| Regex::new(r".?* (\d+)%.*").unwrap());
fn run_pactl(args: &[&str]) -> String {
match Command::new("pactl").args(args).output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
Err(err) => {
log::error!("Could not run pactl: {}", err);
String::new()
}
}
}
fn get_volume() -> u8 {
let output = run_pactl(&["get-sink-volume", "@DEFAULT_SINK@"]);
VOLUME_RX
.captures(&output)
.map(|c| c.get(1).unwrap().as_str().parse::<u8>().unwrap())
.unwrap_or(255_u8)
}
fn get_mute_state() -> bool {
run_pactl(&["get-sink-mute", "@DEFAULT_SINK@"]).contains("yes")
}
pub struct BarModulePactl {
config: config::ModuleConfig,
state: Mutex<State>,
}
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) {
state.volume = get_volume();
state.muted = get_mute_state();
state.cached_text = subst_placeholders(fmt_str, html_escape, state);
}
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String {
subst_placeholders!(fmt, html_escape, {
"volume" => {
state.volume
},
"muted" =>{
if state.muted {
" muted"
} else {
""
}
},
})
}
impl BarModuleFn for BarModulePactl {
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn>
where
Self: Sized,
{
Box::new(BarModulePactl {
config,
state: Mutex::new(State {
volume: 255_u8,
muted: false,
cached_text: String::new(),
}),
})
}
fn default_config(instance: String) -> config::ModuleConfig
where
Self: Sized,
{
config::ModuleConfig {
name: NAME.to_owned(),
instance,
format: "🔈 Vol: {volume:{:3}}%{muted}".to_owned(),
html_escape: Some(true),
on_click: Some(HashMap::from([
("Left".to_owned(), vec!["pavucontrol".to_owned()]),
(
"Right".to_owned(),
vec![
"pactl".to_owned(),
"set-sink-mute".to_owned(),
"@DEFAULT_SINK@".to_owned(),
"toggle".to_owned(),
],
),
(
"WheelUp".to_owned(),
vec![
"pactl".to_owned(),
"set-sink-volume".to_owned(),
"@DEFAULT_SINK@".to_owned(),
"+1%".to_owned(),
],
),
(
"WheelDown".to_owned(),
vec![
"pactl".to_owned(),
"set-sink-volume".to_owned(),
"@DEFAULT_SINK@".to_owned(),
"-1%".to_owned(),
],
),
])),
}
}
fn get_config(&self) -> &config::ModuleConfig {
&self.config
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block {
let mut state = self.state.lock().expect("Could not lock state.");
if self.should_refresh(nai, true, &[RefreshReason::ClickEvent]) {
refresh_state(
&mut state,
&self.config.format,
self.config.is_html_escape(),
);
}
s::Block {
name: Some(NAME.to_owned()),
instance: Some(self.config.instance.clone()),
full_text: state.cached_text.to_owned(),
align: Some(s::Align::Left),
markup: Some(s::Markup::Pango),
short_text: None,
color: None,
background: None,
border: None,
border_top: None,
border_bottom: None,
border_left: None,
border_right: None,
min_width: None,
urgent: None,
separator: Some(true),
separator_block_width: None,
}
}
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> {
let state = self.state.lock().expect("Could not lock state.");
Some(
cmd.iter()
.map(|arg| subst_placeholders(arg, false, &state))
.collect(),
)
}
}

@ -0,0 +1,198 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The date `swayrbar` module.
use crate::config;
use crate::module::{BarModuleFn, NameInstanceAndReason};
use crate::shared::fmt::subst_placeholders;
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::Once;
use swaybar_types as s;
use sysinfo as si;
use sysinfo::ProcessorExt;
use sysinfo::SystemExt;
const NAME: &str = "sysinfo";
struct State {
cpu_usage: f32,
mem_usage: f64,
load_avg_1: f64,
load_avg_5: f64,
load_avg_15: f64,
cached_text: String,
}
pub struct BarModuleSysInfo {
config: config::ModuleConfig,
system: Mutex<si::System>,
state: Mutex<State>,
}
struct OnceRefresher {
cpu: Once,
memory: Once,
}
impl OnceRefresher {
fn new() -> OnceRefresher {
OnceRefresher {
cpu: Once::new(),
memory: Once::new(),
}
}
fn refresh_cpu(&self, sys: &mut si::System) {
self.cpu.call_once(|| sys.refresh_cpu());
}
fn refresh_memory(&self, sys: &mut si::System) {
self.memory.call_once(|| sys.refresh_memory());
}
}
fn get_cpu_usage(sys: &mut si::System, upd: &OnceRefresher) -> f32 {
upd.refresh_cpu(sys);
sys.global_processor_info().cpu_usage()
}
fn get_memory_usage(sys: &mut si::System, upd: &OnceRefresher) -> f64 {
upd.refresh_memory(sys);
sys.used_memory() as f64 * 100_f64 / sys.total_memory() as f64
}
#[derive(Debug)]
enum LoadAvg {
One,
Five,
Fifteen,
}
fn get_load_average(
sys: &mut si::System,
avg: LoadAvg,
upd: &OnceRefresher,
) -> f64 {
upd.refresh_cpu(sys);
let load_avg = sys.load_average();
match avg {
LoadAvg::One => load_avg.one,
LoadAvg::Five => load_avg.five,
LoadAvg::Fifteen => load_avg.fifteen,
}
}
fn refresh_state(
sys: &mut si::System,
state: &mut State,
fmt_str: &str,
html_escape: bool,
) {
let updater = OnceRefresher::new();
state.cpu_usage = get_cpu_usage(sys, &updater);
state.mem_usage = get_memory_usage(sys, &updater);
state.load_avg_1 = get_load_average(sys, LoadAvg::One, &updater);
state.load_avg_5 = get_load_average(sys, LoadAvg::Five, &updater);
state.load_avg_15 = get_load_average(sys, LoadAvg::Fifteen, &updater);
state.cached_text = subst_placeholders(fmt_str, html_escape, state);
}
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String {
subst_placeholders!(fmt, html_escape, {
"cpu_usage" => state.cpu_usage,
"mem_usage" => state.mem_usage,
"load_avg_1" => state.load_avg_1,
"load_avg_5" => state.load_avg_5,
"load_avg_15" => state.load_avg_15,
})
}
impl BarModuleFn for BarModuleSysInfo {
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> {
Box::new(BarModuleSysInfo {
config,
system: Mutex::new(si::System::new_all()),
state: Mutex::new(State {
cpu_usage: 0.0,
mem_usage: 0.0,
load_avg_1: 0.0,
load_avg_5: 0.0,
load_avg_15: 0.0,
cached_text: String::new(),
}),
})
}
fn default_config(instance: String) -> config::ModuleConfig {
config::ModuleConfig {
name: NAME.to_owned(),
instance,
format: "💻 CPU: {cpu_usage:{:5.1}}% Mem: {mem_usage:{:5.1}}% Load: {load_avg_1:{:5.2}} / {load_avg_5:{:5.2}} / {load_avg_15:{:5.2}}".to_owned(),
html_escape: Some(false),
on_click: Some(HashMap::from([
("Left".to_owned(),
vec!["foot".to_owned(), "htop".to_owned()])])),
}
}
fn get_config(&self) -> &config::ModuleConfig {
&self.config
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block {
let mut sys = self.system.lock().expect("Could not lock state.");
let mut state = self.state.lock().expect("Could not lock state.");
if self.should_refresh(nai, true, &[]) {
refresh_state(
&mut sys,
&mut state,
&self.config.format,
self.config.is_html_escape(),
);
}
s::Block {
name: Some(NAME.to_owned()),
instance: Some(self.config.instance.clone()),
full_text: state.cached_text.to_owned(),
align: Some(s::Align::Left),
markup: Some(s::Markup::Pango),
short_text: None,
color: None,
background: None,
border: None,
border_top: None,
border_bottom: None,
border_left: None,
border_right: None,
min_width: None,
urgent: None,
separator: Some(true),
separator_block_width: None,
}
}
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> {
let state = self.state.lock().expect("Could not lock state.");
Some(
cmd.iter()
.map(|arg| subst_placeholders(arg, false, &state))
.collect(),
)
}
}

@ -0,0 +1,162 @@
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! The window `swayrbar` module.
use std::collections::HashMap;
use std::sync::Mutex;
use crate::config;
use crate::module::{BarModuleFn, NameInstanceAndReason};
use crate::shared::fmt::subst_placeholders;
use crate::shared::ipc;
use crate::shared::ipc::NodeMethods;
use swaybar_types as s;
use super::RefreshReason;
pub const NAME: &str = "window";
const INITIAL_PID: i32 = -128;
const NO_WINDOW_PID: i32 = -1;
const UNKNOWN_PID: i32 = -2;
struct State {
name: String,
app_name: String,
pid: i32,
cached_text: String,
}
pub struct BarModuleWindow {
config: config::ModuleConfig,
state: Mutex<State>,
}
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) {
let root = ipc::get_root_node(false);
let focused_win = root
.iter()
.find(|n| n.focused && n.get_type() == ipc::Type::Window);
match focused_win {
Some(win) => {
state.name = win.get_name().to_owned();
state.app_name = win.get_app_name().to_owned();
state.pid = win.pid.unwrap_or(UNKNOWN_PID);
state.cached_text = subst_placeholders(fmt_str, html_escape, state);
}
None => {
state.name.clear();
state.app_name.clear();
state.pid = NO_WINDOW_PID;
state.cached_text.clear();
}
};
}
fn subst_placeholders(s: &str, html_escape: bool, state: &State) -> String {
subst_placeholders!(s, html_escape, {
"title" | "name" => state.name.clone(),
"app_name" => state.app_name.clone(),
"pid" => state.pid,
})
}
impl BarModuleFn for BarModuleWindow {
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> {
Box::new(BarModuleWindow {
config,
state: Mutex::new(State {
name: String::new(),
app_name: String::new(),
pid: INITIAL_PID,
cached_text: String::new(),
}),
})
}
fn default_config(instance: String) -> config::ModuleConfig {
config::ModuleConfig {
name: NAME.to_owned(),
instance,
format: "🪟 {title} — {app_name}".to_owned(),
html_escape: Some(false),
on_click: Some(HashMap::from([
(
"Left".to_owned(),
vec![
"swayr".to_owned(),
"switch-to-urgent-or-lru-window".to_owned(),
],
),
(
"Right".to_owned(),
vec!["kill".to_owned(), "{pid}".to_owned()],
),
])),
}
}
fn get_config(&self) -> &config::ModuleConfig {
&self.config
}
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block {
let mut state = self.state.lock().expect("Could not lock state.");
// In contrast to other modules, this one should only refresh its state
// initially at startup and when explicitly named by `nai` (caused by a
// window or workspace event).
if state.pid == INITIAL_PID
|| (self.should_refresh(nai, false, &[RefreshReason::SwayEvent]))
{
refresh_state(
&mut state,
&self.config.format,
self.config.is_html_escape(),
);
}
s::Block {
name: Some(NAME.to_owned()),
instance: Some(self.config.instance.clone()),
full_text: state.cached_text.clone(),
align: Some(s::Align::Left),
markup: Some(s::Markup::Pango),
short_text: None,
color: None,
background: None,
border: None,
border_top: None,
border_bottom: None,
border_left: None,
border_right: None,
min_width: None,
urgent: None,
separator: Some(true),
separator_block_width: None,
}
}
fn subst_args<'b>(&'b self, cmd: &'b [String]) -> Option<Vec<String>> {
let state = self.state.lock().expect("Could not lock state.");
let cmd = cmd
.iter()
.map(|arg| subst_placeholders(arg, false, &*state))
.collect();
Some(cmd)
}
}

@ -0,0 +1 @@
../../swayr/src/shared
Loading…
Cancel
Save