19 Commits

Author SHA1 Message Date
Jake Stanger
b801751bda chore(release): v0.5.2 2022-09-07 22:47:57 +01:00
Jake Stanger
ee67b3be28 build: update deps
new corn version allows unicode keys :)
2022-09-07 22:47:47 +01:00
Jake Stanger
6442667961 docs(readme): fix links in install section 2022-09-06 22:50:27 +01:00
JakeStanger
68574d4327 docs: update CHANGELOG.md for v0.5.1 [skip ci] 2022-09-06 21:39:15 +00:00
Jake Stanger
6871126bd8 chore(release): v0.5.1 2022-09-06 22:38:16 +01:00
Jake Stanger
481adfcaa4 chore(intellij): update run configs 2022-09-06 22:37:55 +01:00
Jake Stanger
64650fbf3a Merge pull request #14 from JakeStanger/fix/launcher-state
Fix launcher state issues
2022-09-06 21:56:21 +01:00
Jake Stanger
a35d25520c fix(launcher): item state changes not handled correctly
This completely rewrites the item open state handling code (again) in a more logical way that should prevent incorrect states, and removes some locking issues.
2022-09-06 21:46:02 +01:00
Jake Stanger
78e30b39fe docs: add some rustdoc comments throughout 2022-08-28 16:57:41 +01:00
Jake Stanger
b81927e3a5 fix(launcher): opening new instances when focused/urgent 2022-08-25 22:08:08 +01:00
JakeStanger
5d319e91f2 docs: update CHANGELOG.md for v0.5.0 [skip ci] 2022-08-25 20:55:43 +00:00
Jake Stanger
015dcd3204 chore(release): v0.5.0 2022-08-25 21:54:32 +01:00
Jake Stanger
1e38719996 feat: introduce logging in some areas 2022-08-25 21:53:57 +01:00
Jake Stanger
6dcae66570 fix: avoid creating loads of sway/mpd clients 2022-08-25 21:53:42 +01:00
Jake Stanger
649b0efb19 style: run rustfmt 2022-08-24 21:27:30 +01:00
Jake Stanger
023c2fb118 fix(workspaces): not listening to move event 2022-08-24 21:27:19 +01:00
Jake Stanger
ea57f5e18d Merge remote-tracking branch 'origin/master' 2022-08-24 18:05:38 +01:00
Jake Stanger
53142d1bea ci(release): fix missing build deps [skip ci] 2022-08-22 23:13:45 +01:00
JakeStanger
7e0f2cad1c docs: update CHANGELOG.md for v0.4.0 [skip ci] 2022-08-22 22:09:27 +00:00
29 changed files with 708 additions and 442 deletions

View File

@@ -17,6 +17,9 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
- name: Install build deps
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
- name: Update CHANGELOG - name: Update CHANGELOG
id: changelog id: changelog
uses: Requarks/changelog-action@v1 uses: Requarks/changelog-action@v1

17
.idea/runConfigurations/Format.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="fmt" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -12,6 +12,7 @@
<envs> <envs>
<env name="IRONBAR_CONFIG" value="examples/config.json" /> <env name="IRONBAR_CONFIG" value="examples/config.json" />
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" /> <env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="trace" />
</envs> </envs>
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run (Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <configuration default="false" name="Run (GTK Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package ironbar --bin ironbar" /> <option name="command" value="run --package ironbar --bin ironbar" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" /> <option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" /> <option name="channel" value="DEFAULT" />

View File

@@ -11,6 +11,7 @@
<option name="backtrace" value="SHORT" /> <option name="backtrace" value="SHORT" />
<envs> <envs>
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" /> <env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
<env name="RUST_LOG" value="trace" />
</envs> </envs>
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />

42
CHANGELOG.md Normal file
View File

@@ -0,0 +1,42 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.5.1] - 2022-09-06
### :bug: Bug Fixes
- [`b81927e`](https://github.com/JakeStanger/ironbar/commit/b81927e3a57808188e31419695a36aa4ea3f2830) - **launcher**: opening new instances when focused/urgent *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a35d255`](https://github.com/JakeStanger/ironbar/commit/a35d25520cd3fd235cdc77ec6209d88499ca3639) - **launcher**: item state changes not handled correctly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`481adfc`](https://github.com/JakeStanger/ironbar/commit/481adfcaa41c0d3a1ba7d61edb68db49d959c78f) - **intellij**: update run configs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6871126`](https://github.com/JakeStanger/ironbar/commit/6871126bd8def89ccbf2934180d615e781ec32c7) - **release**: v0.5.1 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.5.0] - 2022-08-25
### :sparkles: New Features
- [`1e38719`](https://github.com/JakeStanger/ironbar/commit/1e387199962b81caeb40ffbd99a956f24abdf4e3) - introduce logging in some areas *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`023c2fb`](https://github.com/JakeStanger/ironbar/commit/023c2fb118f46f3592f1dfe1a6704014c062ab3f) - **workspaces**: not listening to move event *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6dcae66`](https://github.com/JakeStanger/ironbar/commit/6dcae66570cf5434e077ec823cded33771b4239c) - avoid creating loads of sway/mpd clients *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`015dcd3`](https://github.com/JakeStanger/ironbar/commit/015dcd3204dfa6a1ebcef1b4f3b345ed733fee2f) - **release**: v0.5.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.4.0] - 2022-08-22
### :sparkles: New Features
- [`ab8f7ec`](https://github.com/JakeStanger/ironbar/commit/ab8f7ecfc8fa4b96fce78518af75794641950140) - logging support and proper error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :bug: Bug Fixes
- [`f2ee2df`](https://github.com/JakeStanger/ironbar/commit/f2ee2dfe7a0f5575d0c3ec09644ca990b088cd85) - error when using with `swaybar_command` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :wrench: Chores
- [`1d7c377`](https://github.com/JakeStanger/ironbar/commit/1d7c3772e4b97c7198043cb55fe9c71695a211ab) - **release**: v0.4.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1

281
Cargo.lock generated
View File

@@ -43,7 +43,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -163,7 +163,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -290,7 +290,7 @@ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
"time 0.1.44", "time 0.1.44",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -367,7 +367,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [ dependencies = [
"atty", "atty",
"lazy_static", "lazy_static",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -387,17 +387,18 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "cornfig" name = "cornfig"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d9e72be8e5eb3eb96acb65d30ddab369be7a268408d8e25cc3a110ad8bf1bf" checksum = "0b6981753b68f7642c3737b302cd37dee779189fcdad975a69d6a7bb165f134e"
dependencies = [ dependencies = [
"cfg-if 1.0.0",
"clap", "clap",
"colored", "colored",
"pest", "pest",
"pest_derive", "pest_derive",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml 0.8.26", "serde_yaml",
"toml", "toml",
] ]
@@ -536,7 +537,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [ dependencies = [
"libc", "libc",
"redox_users", "redox_users",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -619,41 +620,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "2.0.1" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.21" version = "0.3.21"
@@ -852,7 +827,7 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -1017,15 +992,6 @@ dependencies = [
"syn 1.0.98", "syn 1.0.98",
] ]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@@ -1084,14 +1050,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown",
] ]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.7.1" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"inotify-sys", "inotify-sys",
@@ -1116,35 +1082,27 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "ironbar" name = "ironbar"
version = "0.4.0" version = "0.5.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"color-eyre", "color-eyre",
"cornfig", "cornfig",
"crossbeam-channel 0.3.9", "crossbeam-channel 0.5.6",
"dirs", "dirs",
"futures-util", "futures-util",
"glib", "glib",
"gtk", "gtk",
"gtk-layer-shell", "gtk-layer-shell",
"ksway", "ksway",
"lazy_static",
"mpd_client", "mpd_client",
"notify", "notify",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml 0.9.4", "serde_yaml",
"stray", "stray",
"strip-ansi-escapes", "strip-ansi-escapes",
"sysinfo", "sysinfo",
@@ -1173,13 +1131,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]] [[package]]
name = "kernel32-sys" name = "kqueue"
version = "0.2.2" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e"
dependencies = [ dependencies = [
"winapi 0.2.8", "kqueue-sys",
"winapi-build", "libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
dependencies = [
"bitflags",
"libc",
] ]
[[package]] [[package]]
@@ -1205,24 +1173,12 @@ 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.126" 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 = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.7" version = "0.4.7"
@@ -1281,25 +1237,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "mio"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log",
"miow",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.4" version = "0.8.4"
@@ -1312,39 +1249,13 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio 0.6.23",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]] [[package]]
name = "mpd_client" name = "mpd_client"
version = "0.7.5" 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 = "1137104369b60c7dab080f7a46d5e1dae8ba1edd9003c41964ffa46dec14226c" checksum = "ab5ddb4e7f7f0323823dcadfb17cb8b4d25d7ebcfee20779a814091d5b6dec95"
dependencies = [ dependencies = [
"bytes", "bytes",
"chrono",
"futures-core",
"mpd_protocol", "mpd_protocol",
"tokio", "tokio",
"tracing", "tracing",
@@ -1352,28 +1263,17 @@ dependencies = [
[[package]] [[package]]
name = "mpd_protocol" name = "mpd_protocol"
version = "0.13.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 = "18039a2cd7aa60ebadfe3e759053188def0aef4c036842e645c9ed4490c8ebd3" checksum = "afcc158275b88361fed416b6efe013286b8fe0c8929bbd569504e5992f638693"
dependencies = [ dependencies = [
"ahash",
"bytes", "bytes",
"hashbrown 0.11.2",
"nom", "nom",
"tokio", "tokio",
"tracing", "tracing",
] ]
[[package]]
name = "net2"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
dependencies = [
"cfg-if 0.1.10",
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.23.1" version = "0.23.1"
@@ -1399,20 +1299,20 @@ dependencies = [
[[package]] [[package]]
name = "notify" name = "notify"
version = "4.0.17" version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossbeam-channel 0.5.6",
"filetime", "filetime",
"fsevent",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
"kqueue",
"libc", "libc",
"mio 0.6.23", "mio",
"mio-extras",
"walkdir", "walkdir",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -1421,7 +1321,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -1566,9 +1466,9 @@ dependencies = [
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8" checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4"
dependencies = [ dependencies = [
"thiserror", "thiserror",
"ucd-trie", "ucd-trie",
@@ -1576,9 +1476,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_derive" name = "pest_derive"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91" checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91"
dependencies = [ dependencies = [
"pest", "pest",
"pest_generator", "pest_generator",
@@ -1586,9 +1486,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_generator" name = "pest_generator"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0" checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
@@ -1599,9 +1499,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_meta" name = "pest_meta"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe" checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"pest", "pest",
@@ -1808,7 +1708,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -1933,21 +1833,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.8.26" version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" checksum = "89f31df3f50926cdf2855da5fd8812295c34752cb20438dae42a67f79e021ac3"
dependencies = [
"indexmap",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "serde_yaml"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b7c9017c64a49806c6e8df8ef99b92446d09c92457f85f91835b01a8064ae0"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
@@ -2013,7 +1901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [ dependencies = [
"libc", "libc",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2077,9 +1965,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.25.1" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373e4bc9213f734126e2be3e2e118dbc9b909c37487d8d755822bc90f70ae62a" checksum = "4ae2421f3e16b3afd4aa692d23b83d0ba42ee9b0081d5deeb7d21428d7195fb1"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"core-foundation-sys", "core-foundation-sys",
@@ -2087,7 +1975,7 @@ dependencies = [
"ntapi", "ntapi",
"once_cell", "once_cell",
"rayon", "rayon",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2114,7 +2002,7 @@ dependencies = [
"libc", "libc",
"redox_syscall", "redox_syscall",
"remove_dir_all", "remove_dir_all",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2169,7 +2057,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.10.0+wasi-snapshot-preview1", "wasi 0.10.0+wasi-snapshot-preview1",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2193,13 +2081,13 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
"mio 0.8.4", "mio",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2335,7 +2223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d"
dependencies = [ dependencies = [
"tempfile", "tempfile",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2414,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [ dependencies = [
"same-file", "same-file",
"winapi 0.3.9", "winapi",
"winapi-util", "winapi-util",
] ]
@@ -2430,12 +2318,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -2446,12 +2328,6 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu",
] ]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]] [[package]]
name = "winapi-i686-pc-windows-gnu" name = "winapi-i686-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@@ -2464,7 +2340,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@@ -2516,25 +2392,6 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "2.3.2" version = "2.3.2"
@@ -2569,7 +2426,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"uds_windows", "uds_windows",
"winapi 0.3.9", "winapi",
"zbus_macros", "zbus_macros",
"zbus_names", "zbus_names",
"zvariant", "zvariant",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.4.0" version = "0.5.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable wlroots/sway bar" description = "Customisable wlroots/sway bar"
@@ -24,14 +24,15 @@ serde = { version = "1.0.141", features = ["derive"] }
serde_json = "1.0.82" serde_json = "1.0.82"
serde_yaml = "0.9.4" serde_yaml = "0.9.4"
toml = "0.5.9" toml = "0.5.9"
cornfig = "0.2.0" cornfig = "0.3.0"
lazy_static = "1.4.0"
regex = "1.6.0" regex = "1.6.0"
stray = "0.1.1" stray = "0.1.1"
dirs = "4.0.0" dirs = "4.0.0"
walkdir = "2.3.2" walkdir = "2.3.2"
notify = "4.0.17" notify = "5.0.0"
mpd_client = "0.7.5" mpd_client = "1.0.0"
ksway = "0.1.0" ksway = "0.1.0"
sysinfo = "0.25.1" sysinfo = "0.26.2"
# required for wrapping ksway # required for wrapping ksway
crossbeam-channel = "0.3.9" crossbeam-channel = "0.5.6"

View File

@@ -26,6 +26,8 @@ cargo install ironbar
yay -S ironbar-git yay -S ironbar-git
``` ```
[aur package](https://aur.archlinux.org/packages/ironbar-git)
### Source ### Source
```sh ```sh
@@ -36,7 +38,7 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar install target/release/ironbar ~/.local/bin/ironbar
``` ```
[aur package](https://aur.archlinux.org/packages/ironbar-git) [repo](https://github.com/jakestanger/ironbar)
## Configuration ## Configuration

View File

@@ -5,7 +5,10 @@ use color_eyre::Result;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Orientation}; use gtk::{Application, ApplicationWindow, Orientation};
use tracing::{debug, info};
/// Creates a new window for a bar,
/// sets it up and adds its widgets.
pub fn create_bar( pub fn create_bar(
app: &Application, app: &Application,
monitor: &Monitor, monitor: &Monitor,
@@ -41,15 +44,18 @@ pub fn create_bar(
win.add(&content); win.add(&content);
win.connect_destroy_event(|_, _| { win.connect_destroy_event(|_, _| {
info!("Shutting down");
gtk::main_quit(); gtk::main_quit();
Inhibit(false) Inhibit(false)
}); });
debug!("Showing bar");
win.show_all(); win.show_all();
Ok(()) Ok(())
} }
/// Loads the configured modules onto a bar.
fn load_modules( fn load_modules(
left: &gtk::Box, left: &gtk::Box,
center: &gtk::Box, center: &gtk::Box,
@@ -98,12 +104,15 @@ fn load_modules(
Ok(()) Ok(())
} }
/// Adds modules into a provided GTK box,
/// which should be one of its left, center or right containers.
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> { fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
macro_rules! add_module { macro_rules! add_module {
($module:expr, $name:literal) => {{ ($module:expr, $name:literal) => {{
let widget = $module.into_widget(&info)?; let widget = $module.into_widget(&info)?;
widget.set_widget_name($name); widget.set_widget_name($name);
content.add(&widget); content.add(&widget);
debug!("Added module of type {}", $name);
}}; }};
} }
@@ -123,6 +132,7 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
Ok(()) Ok(())
} }
/// Sets up GTK layer shell for a provided aplication window.
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) { fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
gtk_layer_shell::init_for_window(win); gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor); gtk_layer_shell::set_monitor(win, monitor);

View File

@@ -11,6 +11,7 @@ pub struct Collection<TKey, TData> {
} }
impl<TKey: PartialEq, TData> Collection<TKey, TData> { impl<TKey: PartialEq, TData> Collection<TKey, TData> {
/// Creates a new empty collection.
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
keys: vec![], keys: vec![],
@@ -18,6 +19,7 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
} }
} }
/// Inserts a new key/value pair at the end of the collection.
pub fn insert(&mut self, key: TKey, value: TData) { pub fn insert(&mut self, key: TKey, value: TData) {
self.keys.push(key); self.keys.push(key);
self.values.push(value); self.values.push(value);
@@ -25,6 +27,8 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
assert_eq!(self.keys.len(), self.values.len()); assert_eq!(self.keys.len(), self.values.len());
} }
/// Gets a reference of the value for the specified key
/// if it exists in the collection.
pub fn get(&self, key: &TKey) -> Option<&TData> { pub fn get(&self, key: &TKey) -> Option<&TData> {
let index = self.keys.iter().position(|k| k == key); let index = self.keys.iter().position(|k| k == key);
match index { match index {
@@ -33,6 +37,8 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
} }
} }
/// Gets a mutable reference for the value with the specified key
/// if it exists in the collection.
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> { pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
let index = self.keys.iter().position(|k| k == key); let index = self.keys.iter().position(|k| k == key);
match index { match index {
@@ -41,6 +47,9 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
} }
} }
/// Removes the key/value from the collection
/// if it exists
/// and returns the removed value.
pub fn remove(&mut self, key: &TKey) -> Option<TData> { pub fn remove(&mut self, key: &TKey) -> Option<TData> {
assert_eq!(self.keys.len(), self.values.len()); assert_eq!(self.keys.len(), self.values.len());
@@ -53,26 +62,32 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
} }
} }
/// Gets the length of the collection.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.keys.len() self.keys.len()
} }
/// Gets a reference to the first value in the collection.
pub fn first(&self) -> Option<&TData> { pub fn first(&self) -> Option<&TData> {
self.values.first() self.values.first()
} }
/// Gets the values as a slice.
pub fn as_slice(&self) -> &[TData] { pub fn as_slice(&self) -> &[TData] {
self.values.as_slice() self.values.as_slice()
} }
/// Checks whether the collection is empty.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.keys.is_empty() self.keys.is_empty()
} }
/// Gets an iterator for the collection.
pub fn iter(&self) -> Iter<'_, TData> { pub fn iter(&self) -> Iter<'_, TData> {
self.values.iter() self.values.iter()
} }
/// Gets a mutable iterator for the collection
pub fn iter_mut(&mut self) -> IterMut<'_, TData> { pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
self.values.iter_mut() self.values.iter_mut()
} }

View File

@@ -71,6 +71,8 @@ const fn default_bar_height() -> i32 {
} }
impl Config { impl Config {
/// Attempts to load the config file from file,
/// parse it and return a new instance of `Self`.
pub fn load() -> Result<Self> { pub fn load() -> Result<Self> {
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") { let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
let path = PathBuf::from(config_path); let path = PathBuf::from(config_path);
@@ -87,6 +89,10 @@ impl Config {
Self::load_file(&config_path) Self::load_file(&config_path)
} }
/// Attempts to discover the location of the config file
/// by checking each valid format's extension.
///
/// Returns the path of the first valid match, if any.
fn try_find_config() -> Result<PathBuf> { fn try_find_config() -> Result<PathBuf> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?; let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
@@ -110,6 +116,8 @@ impl Config {
} }
} }
/// Loads the config file at the specified path
/// and parses it into `Self` based on its extension.
fn load_file(path: &Path) -> Result<Self> { fn load_file(path: &Path) -> Result<Self> {
let file = fs::read(path).wrap_err("Failed to read config file")?; let file = fs::read(path).wrap_err("Failed to read config file")?;
let extension = path let extension = path

View File

@@ -86,6 +86,10 @@ enum IconLocation {
File(PathBuf), File(PathBuf),
} }
/// Attempts to get the location of an icon.
///
/// Handles icons that are part of a GTK theme, icons specified as path
/// and icons for steam games.
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> { fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
let has_icon = theme let has_icon = theme
.lookup_icon(app_id, size, IconLookupFlags::empty()) .lookup_icon(app_id, size, IconLookupFlags::empty())

View File

@@ -26,6 +26,10 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
} }
} }
/// Installs tracing into the current application.
///
/// The returned `WorkerGuard` must remain in scope
/// for the lifetime of the application for logging to file to work.
pub fn install_tracing() -> Result<WorkerGuard> { pub fn install_tracing() -> Result<WorkerGuard> {
let fmt_layer = fmt::layer().with_target(true); let fmt_layer = fmt::layer().with_target(true);
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;

View File

@@ -11,14 +11,13 @@ mod sway;
use crate::bar::create_bar; use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig}; use crate::config::{Config, MonitorConfig};
use crate::style::load_css; use crate::style::load_css;
use crate::sway::{get_client_error, SwayOutput}; use crate::sway::{get_client, SwayOutput};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use color_eyre::Report; use color_eyre::Report;
use dirs::config_dir; use dirs::config_dir;
use gtk::gdk::Display; use gtk::gdk::Display;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Application; use gtk::Application;
use ksway::client::Client;
use ksway::IpcCommand; use ksway::IpcCommand;
use std::env; use std::env;
use std::process::exit; use std::process::exit;
@@ -96,25 +95,33 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
/// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> { fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
let mut sway_client = match Client::connect() { let outputs = {
Ok(client) => Ok(client), let sway = get_client();
Err(err) => Err(get_client_error(err)), let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
}?;
let outputs = match sway_client.ipc(IpcCommand::GetOutputs) { let outputs = sway.ipc(IpcCommand::GetOutputs);
Ok(outputs) => Ok(outputs),
Err(err) => Err(get_client_error(err)), match outputs {
Ok(outputs) => Ok(outputs),
Err(err) => Err(err),
}
}?; }?;
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?; let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
debug!("Received {} outputs from Sway IPC", outputs.len());
let num_monitors = display.n_monitors(); let num_monitors = display.n_monitors();
for i in 0..num_monitors { for i in 0..num_monitors {
let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?; let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?;
let monitor_name = &outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?.name; let monitor_name = &outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?.name;
info!("Creating bar on '{}'", monitor_name);
// TODO: Could we use an Arc<Config> here to avoid cloning?
config.monitors.as_ref().map_or_else( config.monitors.as_ref().map_or_else(
|| create_bar(app, &monitor, monitor_name, config.clone()), || create_bar(app, &monitor, monitor_name, config.clone()),
|config| { |config| {

View File

@@ -1,24 +1,26 @@
use crate::icon; use crate::icon;
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, WindowEvent}; use crate::sway::get_client;
use color_eyre::Result; use color_eyre::Result;
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconTheme, Image, Label, Orientation}; use gtk::{IconTheme, Image, Label, Orientation};
use ksway::IpcEvent;
use serde::Deserialize; use serde::Deserialize;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule { pub struct FocusedModule {
/// Whether to show icon on the bar.
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_icon: bool, show_icon: bool,
/// Whether to show app name on the bar.
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_title: bool, show_title: bool,
/// Icon size in pixels.
#[serde(default = "default_icon_size")] #[serde(default = "default_icon_size")]
icon_size: i32, icon_size: i32,
/// GTK icon theme to use.
icon_theme: Option<String>, icon_theme: Option<String>,
} }
@@ -42,42 +44,39 @@ impl Module<gtk::Box> for FocusedModule {
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);
let mut sway = SwayClient::connect()?;
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let focused = sway let focused = {
.get_open_windows()? let sway = get_client();
.into_iter() let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
.find(|node| node.focused); sway.get_open_windows()?
.into_iter()
.find(|node| node.focused)
};
if let Some(focused) = focused { if let Some(focused) = focused {
tx.send(focused)?; tx.send(focused)?;
} }
spawn_blocking(move || loop { spawn_blocking(move || {
while let Ok((_, payload)) = srx.try_recv() { let srx = {
match serde_json::from_slice::<WindowEvent>(&payload) { let sway = get_client();
Ok(payload) => { let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
let update = match payload.change.as_str() { sway.subscribe_window()
"focus" => true, };
"title" => payload.container.focused,
_ => false,
};
if update { while let Ok(payload) = srx.recv() {
tx.send(payload.container) let update = match payload.change.as_str() {
.expect("Failed to sendf focus update"); "focus" => true,
} "title" => payload.container.focused,
} _ => false,
Err(err) => error!("{:?}", err), };
if update {
tx.send(payload.container)
.expect("Failed to sendf focus update");
} }
} }
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
}); });
{ {

View File

@@ -1,5 +1,6 @@
use crate::collection::Collection; use crate::collection::Collection;
use crate::icon::{find_desktop_file, get_icon}; use crate::icon::{find_desktop_file, get_icon};
use crate::modules::launcher::open_state::OpenState;
use crate::modules::launcher::popup::Popup; use crate::modules::launcher::popup::Popup;
use crate::modules::launcher::FocusEvent; use crate::modules::launcher::FocusEvent;
use crate::sway::SwayNode; use crate::sway::SwayNode;
@@ -9,7 +10,7 @@ use gtk::prelude::*;
use gtk::{Button, IconTheme, Image}; use gtk::{Button, IconTheme, Image};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, RwLock};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::error; use tracing::error;
@@ -18,7 +19,7 @@ use tracing::error;
pub struct LauncherItem { pub struct LauncherItem {
pub app_id: String, pub app_id: String,
pub favorite: bool, pub favorite: bool,
pub windows: Rc<Mutex<Collection<i32, LauncherWindow>>>, pub windows: Rc<RwLock<Collection<i32, LauncherWindow>>>,
pub state: Arc<RwLock<State>>, pub state: Arc<RwLock<State>>,
pub button: Button, pub button: Button,
} }
@@ -27,38 +28,7 @@ pub struct LauncherItem {
pub struct LauncherWindow { pub struct LauncherWindow {
pub con_id: i32, pub con_id: i32,
pub name: Option<String>, pub name: Option<String>,
} pub open_state: OpenState,
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum OpenState {
Closed,
Open,
Focused,
Urgent,
}
impl OpenState {
pub const fn from_node(node: &SwayNode) -> Self {
if node.focused {
Self::Urgent
} else if node.urgent {
Self::Focused
} else {
Self::Open
}
}
pub fn highest_of(a: &Self, b: &Self) -> Self {
if a == &Self::Urgent || b == &Self::Urgent {
Self::Urgent
} else if a == &Self::Focused || b == &Self::Focused {
Self::Focused
} else if a == &Self::Open || b == &Self::Open {
Self::Open
} else {
Self::Closed
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -89,7 +59,7 @@ impl LauncherItem {
let item = Self { let item = Self {
app_id, app_id,
favorite, favorite,
windows: Rc::new(Mutex::new(Collection::new())), windows: Rc::new(RwLock::new(Collection::new())),
state: Arc::new(RwLock::new(state)), state: Arc::new(RwLock::new(state)),
button, button,
}; };
@@ -107,6 +77,7 @@ impl LauncherItem {
LauncherWindow { LauncherWindow {
con_id: node.id, con_id: node.id,
name: node.name.clone(), name: node.name.clone(),
open_state: OpenState::from_node(node),
}, },
)); ));
@@ -118,7 +89,7 @@ impl LauncherItem {
let item = Self { let item = Self {
app_id: node.get_id().to_string(), app_id: node.get_id().to_string(),
favorite: false, favorite: false,
windows: Rc::new(Mutex::new(windows)), windows: Rc::new(RwLock::new(windows)),
state: Arc::new(RwLock::new(state)), state: Arc::new(RwLock::new(state)),
button, button,
}; };
@@ -130,7 +101,10 @@ impl LauncherItem {
fn configure_button(&self, config: &ButtonConfig) { fn configure_button(&self, config: &ButtonConfig) {
let button = &self.button; let button = &self.button;
let windows = self.windows.lock().expect("Failed to get lock on windows"); let windows = self
.windows
.read()
.expect("Failed to get read lock on windows");
let name = if windows.len() == 1 { let name = if windows.len() == 1 {
windows windows
@@ -163,7 +137,7 @@ impl LauncherItem {
button.connect_clicked(move |_| { button.connect_clicked(move |_| {
let state = state.read().expect("Failed to get read lock on state"); let state = state.read().expect("Failed to get read lock on state");
if state.open_state == OpenState::Open { if state.open_state.is_open() {
focus_tx.try_send(()).expect("Failed to send focus event"); focus_tx.try_send(()).expect("Failed to send focus event");
} else { } else {
// attempt to find desktop file and launch // attempt to find desktop file and launch
@@ -215,7 +189,7 @@ impl LauncherItem {
let tx_hover = config.tx.clone(); let tx_hover = config.tx.clone();
button.connect_enter_notify_event(move |button, _| { button.connect_enter_notify_event(move |button, _| {
let windows = windows.lock().expect("Failed to get lock on windows"); let windows = windows.read().expect("Failed to get read lock on windows");
if windows.len() > 1 { if windows.len() > 1 {
popup.set_windows(windows.as_slice(), &tx_hover); popup.set_windows(windows.as_slice(), &tx_hover);
popup.show(button); popup.show(button);
@@ -266,22 +240,53 @@ impl LauncherItem {
style.remove_class("favorite"); style.remove_class("favorite");
} }
if state.open_state == OpenState::Open { if state.open_state.is_open() {
style.add_class("open"); style.add_class("open");
} else { } else {
style.remove_class("open"); style.remove_class("open");
} }
if state.open_state == OpenState::Focused { if state.open_state.is_focused() {
style.add_class("focused"); style.add_class("focused");
} else { } else {
style.remove_class("focused"); style.remove_class("focused");
} }
if state.open_state == OpenState::Urgent { if state.open_state.is_urgent() {
style.add_class("urgent"); style.add_class("urgent");
} else { } else {
style.remove_class("urgent"); style.remove_class("urgent");
} }
} }
/// Sets the open state for a specific window on the item
/// and updates the item state based on all its windows.
pub fn set_window_open_state(&self, window_id: i32, new_state: OpenState, state: &mut State) {
let mut windows = self
.windows
.write()
.expect("Failed to get write lock on windows");
let window = windows.iter_mut().find(|w| w.con_id == window_id);
if let Some(window) = window {
window.open_state = new_state;
state.open_state =
OpenState::merge_states(windows.iter().map(|w| &w.open_state).collect());
}
}
/// Sets the open state on the item and all its windows.
/// This overrides the existing open states.
pub fn set_open_state(&self, new_state: OpenState, state: &mut State) {
state.open_state = new_state;
let mut windows = self
.windows
.write()
.expect("Failed to get write lock on windows");
windows
.iter_mut()
.for_each(|window| window.open_state = new_state);
}
} }

View File

@@ -1,30 +1,36 @@
mod item; mod item;
mod open_state;
mod popup; mod popup;
use crate::collection::Collection; use crate::collection::Collection;
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow, OpenState}; use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
use crate::modules::launcher::open_state::OpenState;
use crate::modules::launcher::popup::Popup; use crate::modules::launcher::popup::Popup;
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, SwayNode, WindowEvent}; use crate::sway::{get_client, SwayNode};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconTheme, Orientation}; use gtk::{IconTheme, Orientation};
use ksway::IpcEvent;
use serde::Deserialize; use serde::Deserialize;
use std::rc::Rc; use std::rc::Rc;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::error; use tracing::debug;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct LauncherModule { pub struct LauncherModule {
/// List of app IDs (or classes) to always show regardles of open state,
/// in the order specified.
favorites: Option<Vec<String>>, favorites: Option<Vec<String>>,
/// Whether to show application names on the bar.
#[serde(default = "crate::config::default_false")] #[serde(default = "crate::config::default_false")]
show_names: bool, show_names: bool,
/// Whether to show application icons on the bar.
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_icons: bool, show_icons: bool,
/// Name of the GTK icon theme to use.
icon_theme: Option<String>, icon_theme: Option<String>,
} }
@@ -69,31 +75,37 @@ impl Launcher {
/// Adds a new window to the launcher. /// Adds a new window to the launcher.
/// This gets added to an existing group /// This gets added to an existing group
/// if an instance of the program is already open. /// if an instance of the program is already open.
fn add_window(&mut self, window: SwayNode) { fn add_window(&mut self, node: SwayNode) {
let id = window.get_id().to_string(); let id = node.get_id().to_string();
debug!("Adding window with ID {}", id);
if let Some(item) = self.items.get_mut(&id) { if let Some(item) = self.items.get_mut(&id) {
let mut state = item let mut state = item
.state .state
.write() .write()
.expect("Failed to get write lock on state"); .expect("Failed to get write lock on state");
let new_open_state = OpenState::from_node(&window); let new_open_state = OpenState::from_node(&node);
state.open_state = OpenState::highest_of(&state.open_state, &new_open_state); state.open_state = OpenState::merge_states(vec![&state.open_state, &new_open_state]);
state.is_xwayland = window.is_xwayland(); state.is_xwayland = node.is_xwayland();
item.update_button_classes(&state); item.update_button_classes(&state);
let mut windows = item.windows.lock().expect("Failed to get lock on windows"); let mut windows = item
.windows
.write()
.expect("Failed to get write lock on windows");
windows.insert( windows.insert(
window.id, node.id,
LauncherWindow { LauncherWindow {
con_id: window.id, con_id: node.id,
name: window.name, name: node.name,
open_state: new_open_state,
}, },
); );
} else { } else {
let item = LauncherItem::from_node(&window, &self.button_config); let item = LauncherItem::from_node(&node, &self.button_config);
self.container.add(&item.button); self.container.add(&item.button);
self.items.insert(id, item); self.items.insert(id, item);
@@ -106,11 +118,15 @@ impl Launcher {
fn remove_window(&mut self, window: &SwayNode) { fn remove_window(&mut self, window: &SwayNode) {
let id = window.get_id().to_string(); let id = window.get_id().to_string();
debug!("Removing window with ID {}", id);
let item = self.items.get_mut(&id); let item = self.items.get_mut(&id);
let remove = if let Some(item) = item { let remove = if let Some(item) = item {
let windows = Rc::clone(&item.windows); let windows = Rc::clone(&item.windows);
let mut windows = windows.lock().expect("Failed to get lock on windows"); let mut windows = windows
.write()
.expect("Failed to get write lock on windows");
windows.remove(&window.id); windows.remove(&window.id);
@@ -137,24 +153,33 @@ impl Launcher {
} }
} }
fn set_window_focused(&mut self, window: &SwayNode) { /// Unfocuses the currently focused window
let id = window.get_id().to_string(); /// and focuses the newly focused one.
fn set_window_focused(&mut self, node: &SwayNode) {
let id = node.get_id().to_string();
let currently_focused = self.items.iter_mut().find(|item| { debug!("Setting window with ID {} focused", id);
let prev_focused = self.items.iter_mut().find(|item| {
item.state item.state
.read() .read()
.expect("Failed to get read lock on state") .expect("Failed to get read lock on state")
.open_state .open_state
== OpenState::Focused .is_focused()
}); });
if let Some(currently_focused) = currently_focused { if let Some(prev_focused) = prev_focused {
let mut state = currently_focused let mut state = prev_focused
.state .state
.write() .write()
.expect("Failed to get write lock on state"); .expect("Failed to get write lock on state");
state.open_state = OpenState::Open;
currently_focused.update_button_classes(&state); // if a window from the same item took focus,
// we don't need to unfocus the item.
if prev_focused.app_id != id {
prev_focused.set_open_state(OpenState::open(), &mut state);
prev_focused.update_button_classes(&state);
}
} }
let item = self.items.get_mut(&id); let item = self.items.get_mut(&id);
@@ -163,17 +188,23 @@ impl Launcher {
.state .state
.write() .write()
.expect("Failed to get write lock on state"); .expect("Failed to get write lock on state");
state.open_state = OpenState::Focused; item.set_window_open_state(node.id, OpenState::focused(), &mut state);
item.update_button_classes(&state); item.update_button_classes(&state);
} }
} }
/// Updates the window title for the given node.
fn set_window_title(&mut self, window: SwayNode) { fn set_window_title(&mut self, window: SwayNode) {
let id = window.get_id().to_string(); let id = window.get_id().to_string();
let item = self.items.get_mut(&id); let item = self.items.get_mut(&id);
debug!("Updating title for window with ID {}", id);
if let (Some(item), Some(name)) = (item, window.name) { if let (Some(item), Some(name)) = (item, window.name) {
let mut windows = item.windows.lock().expect("Failed to get lock on windows"); let mut windows = item
.windows
.write()
.expect("Failed to get write lock on windows");
if windows.len() == 1 { if windows.len() == 1 {
item.set_title(&name, &self.button_config); item.set_title(&name, &self.button_config);
} else if let Some(window) = windows.get_mut(&window.id) { } else if let Some(window) = windows.get_mut(&window.id) {
@@ -186,17 +217,23 @@ impl Launcher {
} }
} }
fn set_window_urgent(&mut self, window: &SwayNode) { /// Updates the window urgency based on the given node.
let id = window.get_id().to_string(); fn set_window_urgent(&mut self, node: &SwayNode) {
let id = node.get_id().to_string();
let item = self.items.get_mut(&id); let item = self.items.get_mut(&id);
debug!(
"Setting urgency to {} for window with ID {}",
node.urgent, id
);
if let Some(item) = item { if let Some(item) = item {
let mut state = item let mut state = item
.state .state
.write() .write()
.expect("Failed to get write lock on state"); .expect("Failed to get write lock on state");
state.open_state =
OpenState::highest_of(&state.open_state, &OpenState::from_node(window)); item.set_window_open_state(node.id, OpenState::urgent(node.urgent), &mut state);
item.update_button_classes(&state); item.update_button_classes(&state);
} }
} }
@@ -210,8 +247,6 @@ impl Module<gtk::Box> for LauncherModule {
icon_theme.set_custom_theme(Some(&theme)); icon_theme.set_custom_theme(Some(&theme));
} }
let mut sway = SwayClient::connect()?;
let popup = Popup::new( let popup = Popup::new(
"popup-launcher", "popup-launcher",
info.app, info.app,
@@ -237,28 +272,28 @@ impl Module<gtk::Box> for LauncherModule {
button_config, button_config,
); );
let open_windows = sway.get_open_windows()?; let open_windows = {
let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.get_open_windows()
}?;
for window in open_windows { for window in open_windows {
launcher.add_window(window); launcher.add_window(window);
} }
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop { spawn_blocking(move || {
while let Ok((_, payload)) = srx.try_recv() { let srx = {
match serde_json::from_slice::<WindowEvent>(&payload) { let sway = get_client();
Ok(payload) => { let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
tx.send(payload) sway.subscribe_window()
.expect("Failed to send window event payload"); };
}
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() { while let Ok(payload) = srx.recv() {
error!("{:?}", err); tx.send(payload)
.expect("Failed to send window event payload");
} }
}); });
@@ -278,14 +313,15 @@ impl Module<gtk::Box> for LauncherModule {
} }
spawn(async move { spawn(async move {
let mut sway = SwayClient::connect()?; let sway = get_client();
while let Some(event) = ui_rx.recv().await { while let Some(event) = ui_rx.recv().await {
let selector = match event { let selector = match event {
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id), FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
FocusEvent::Class(class) => format!("[class={}]", class), FocusEvent::Class(class) => format!("[class={}]", class),
FocusEvent::ConId(id) => format!("[con_id={}]", id), FocusEvent::ConId(id) => format!("[con_id={}]", id),
}; };
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.run(format!("{} focus", selector))?; sway.run(format!("{} focus", selector))?;
} }

View File

@@ -0,0 +1,74 @@
use crate::sway::SwayNode;
/// Open state for a launcher item, or item window.
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum OpenState {
Closed,
Open { focused: bool, urgent: bool },
}
impl OpenState {
/// Creates from `SwayNode`
pub const fn from_node(node: &SwayNode) -> Self {
Self::Open {
focused: node.focused,
urgent: node.urgent,
}
}
/// Creates open without focused/urgent
pub const fn open() -> Self {
Self::Open {
focused: false,
urgent: false,
}
}
/// Creates open with focused
pub const fn focused() -> Self {
Self::Open {
focused: true,
urgent: false,
}
}
/// Creates open with urgent
pub const fn urgent(urgent: bool) -> Self {
Self::Open {
focused: false,
urgent,
}
}
/// Checks if open
pub fn is_open(self) -> bool {
self != Self::Closed
}
/// Checks if open with focus
pub const fn is_focused(self) -> bool {
matches!(self, Self::Open { focused: true, .. })
}
/// check if open with urgent
pub const fn is_urgent(self) -> bool {
matches!(self, Self::Open { urgent: true, .. })
}
/// Merges states together to produce a single state.
/// This is effectively an OR operation,
/// so sets state to open and flags to true if any state is open
/// or any instance of the flag is true.
pub fn merge_states(states: Vec<&Self>) -> Self {
states.iter().fold(Self::Closed, |merged, current| {
if merged.is_open() || current.is_open() {
Self::Open {
focused: merged.is_focused() || current.is_focused(),
urgent: merged.is_urgent() || current.is_urgent(),
}
} else {
Self::Closed
}
})
}
}

View File

@@ -1,55 +1,68 @@
use mpd_client::commands::responses::Status; use lazy_static::lazy_static;
use mpd_client::raw::MpdProtocolError; use mpd_client::client::Connection;
use mpd_client::{Client, Connection}; use mpd_client::protocol::MpdProtocolError;
use mpd_client::responses::Status;
use mpd_client::Client;
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::net::{TcpStream, UnixStream}; use tokio::net::{TcpStream, UnixStream};
use tokio::spawn; use tokio::sync::Mutex;
use tokio::time::sleep; use tokio::time::sleep;
pub async fn wait_for_connection( lazy_static! {
hosts: Vec<String>, static ref CLIENTS: Arc<Mutex<HashMap<String, Arc<Client>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub async fn get_connection(host: &str) -> Option<Arc<Client>> {
let mut clients = CLIENTS.lock().await;
match clients.get(host) {
Some(client) => Some(Arc::clone(client)),
None => {
let client = wait_for_connection(host, Duration::from_secs(5), None).await?;
let client = Arc::new(client);
clients.insert(host.to_string(), Arc::clone(&client));
Some(client)
}
}
}
async fn wait_for_connection(
host: &str,
interval: Duration, interval: Duration,
max_retries: Option<usize>, max_retries: Option<usize>,
) -> Option<Client> { ) -> Option<Client> {
let mut retries = 0; let mut retries = 0;
let max_retries = max_retries.unwrap_or(usize::MAX);
spawn(async move { loop {
let max_retries = max_retries.unwrap_or(usize::MAX); if retries == max_retries {
loop { break None;
if retries == max_retries {
break None;
}
if let Some(conn) = try_get_mpd_conn(&hosts).await {
break Some(conn.0);
}
retries += 1;
sleep(interval).await;
} }
})
.await if let Some(conn) = try_get_mpd_conn(host).await {
.expect("Error occurred while handling tasks") break Some(conn.0);
}
retries += 1;
sleep(interval).await;
}
} }
/// Cycles through each MPD host and /// Cycles through each MPD host and
/// returns the first one which connects, /// returns the first one which connects,
/// or none if there are none /// or none if there are none
async fn try_get_mpd_conn(hosts: &[String]) -> Option<Connection> { async fn try_get_mpd_conn(host: &str) -> Option<Connection> {
for host in hosts { let connection = if is_unix_socket(host) {
let connection = if is_unix_socket(host) { connect_unix(host).await
connect_unix(host).await } else {
} else { connect_tcp(host).await
connect_tcp(host).await };
};
if let Ok(connection) = connection { connection.ok()
return Some(connection);
}
}
None
} }
fn is_unix_socket(host: &str) -> bool { fn is_unix_socket(host: &str) -> bool {

View File

@@ -2,7 +2,7 @@ mod client;
mod popup; mod popup;
use self::popup::Popup; use self::popup::Popup;
use crate::modules::mpd::client::{get_duration, get_elapsed, wait_for_connection}; use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
use crate::modules::mpd::popup::{MpdPopup, PopupEvent}; use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use color_eyre::Result; use color_eyre::Result;
@@ -10,8 +10,9 @@ use dirs::{audio_dir, home_dir};
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
use mpd_client::commands::responses::{PlayState, Song, Status}; use mpd_client::commands;
use mpd_client::{commands, Tag}; use mpd_client::responses::{PlayState, Song, Status};
use mpd_client::tag::Tag;
use regex::Regex; use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf; use std::path::PathBuf;
@@ -23,15 +24,20 @@ use tracing::error;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct MpdModule { pub struct MpdModule {
/// TCP or Unix socket address.
#[serde(default = "default_socket")] #[serde(default = "default_socket")]
host: String, host: String,
/// Format of current song info to display on the bar.
#[serde(default = "default_format")] #[serde(default = "default_format")]
format: String, format: String,
/// Icon to display when playing.
#[serde(default = "default_icon_play")] #[serde(default = "default_icon_play")]
icon_play: Option<String>, icon_play: Option<String>,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")] #[serde(default = "default_icon_pause")]
icon_pause: Option<String>, icon_pause: Option<String>,
/// Path to root of music directory.
#[serde(default = "default_music_dir")] #[serde(default = "default_music_dir")]
music_dir: PathBuf, music_dir: PathBuf,
} }
@@ -120,7 +126,7 @@ impl Module<Button> for MpdModule {
let host = self.host.clone(); let host = self.host.clone();
let host2 = self.host.clone(); let host2 = self.host.clone();
spawn(async move { spawn(async move {
let client = wait_for_connection(vec![host], Duration::from_secs(1), None) let client = get_connection(&host)
.await .await
.expect("Unexpected error when trying to connect to MPD server"); .expect("Unexpected error when trying to connect to MPD server");
@@ -145,7 +151,7 @@ impl Module<Button> for MpdModule {
}); });
spawn(async move { spawn(async move {
let client = wait_for_connection(vec![host2], Duration::from_secs(1), None) let client = get_connection(&host2)
.await .await
.expect("Unexpected error when trying to connect to MPD server"); .expect("Unexpected error when trying to connect to MPD server");
@@ -242,4 +248,4 @@ impl MpdModule {
}; };
s.unwrap_or_default().to_string() s.unwrap_or_default().to_string()
} }
} }

View File

@@ -2,7 +2,7 @@ pub use crate::popup::Popup;
use gtk::gdk_pixbuf::Pixbuf; use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Image, Label, Orientation}; use gtk::{Button, Image, Label, Orientation};
use mpd_client::commands::responses::{PlayState, Song, Status}; use mpd_client::responses::{PlayState, Song, Status};
use std::path::Path; use std::path::Path;
use tokio::sync::mpsc; use tokio::sync::mpsc;

View File

@@ -10,7 +10,9 @@ use tracing::{error, instrument};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule { pub struct ScriptModule {
/// Path to script to execute.
path: String, path: String,
/// Time in milliseconds between executions.
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
interval: u64, interval: u64,
} }

View File

@@ -11,6 +11,7 @@ use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct SysInfoModule { pub struct SysInfoModule {
/// List of formatting strings.
format: Vec<String>, format: Vec<String>,
} }

View File

@@ -1,20 +1,22 @@
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use crate::sway::{SwayClient, Workspace, WorkspaceEvent}; use crate::sway::{get_client, Workspace};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
use ksway::{IpcCommand, IpcEvent}; use ksway::IpcCommand;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::error; use tracing::{debug, trace};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule { pub struct WorkspacesModule {
/// Map of actual workspace names to custom names.
name_map: Option<HashMap<String, String>>, name_map: Option<HashMap<String, String>>,
/// Whether to display icons for all monitors.
#[serde(default = "crate::config::default_false")] #[serde(default = "crate::config::default_false")]
all_monitors: bool, all_monitors: bool,
} }
@@ -47,17 +49,19 @@ impl Workspace {
impl Module<gtk::Box> for WorkspacesModule { impl Module<gtk::Box> for WorkspacesModule {
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> { fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let mut sway = SwayClient::connect()?;
let container = gtk::Box::new(Orientation::Horizontal, 0); let container = gtk::Box::new(Orientation::Horizontal, 0);
let workspaces = { let workspaces = {
trace!("Getting current workspaces");
let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
let raw = sway.ipc(IpcCommand::GetWorkspaces)?; let raw = sway.ipc(IpcCommand::GetWorkspaces)?;
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?; let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?;
if self.all_monitors { if self.all_monitors {
workspaces workspaces
} else { } else {
trace!("Filtering workspaces to current monitor only");
workspaces workspaces
.into_iter() .into_iter()
.filter(|workspace| workspace.output == info.output_name) .filter(|workspace| workspace.output == info.output_name)
@@ -71,32 +75,35 @@ impl Module<gtk::Box> for WorkspacesModule {
let (ui_tx, mut ui_rx) = mpsc::channel(32); let (ui_tx, mut ui_rx) = mpsc::channel(32);
trace!("Creating workspace buttons");
for workspace in workspaces { for workspace in workspaces {
let item = workspace.as_button(&name_map, &ui_tx); let item = workspace.as_button(&name_map, &ui_tx);
container.add(&item); container.add(&item);
button_map.insert(workspace.name, item); button_map.insert(workspace.name, item);
} }
let srx = sway.subscribe(vec![IpcEvent::Workspace])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop { spawn_blocking(move || {
while let Ok((_, payload)) = srx.try_recv() { trace!("Starting workspace event listener task");
match serde_json::from_slice::<WorkspaceEvent>(&payload) { let srx = {
Ok(payload) => tx.send(payload).expect("Failed to send workspace event"), let sway = get_client();
Err(err) => error!("{:?}", err), let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
}
}
if let Err(err) = sway.poll() { sway.subscribe_workspace()
error!("{:?}", err); };
while let Ok(payload) = srx.recv() {
tx.send(payload).expect("Failed to send workspace event");
} }
}); });
{ {
trace!("Setting up sway event handler");
let menubar = container.clone(); let menubar = container.clone();
let output_name = info.output_name.to_string(); let output_name = info.output_name.to_string();
rx.attach(None, move |event| { rx.attach(None, move |event| {
debug!("Received workspace event {:?}", event);
match event.change.as_str() { match event.change.as_str() {
"focus" => { "focus" => {
let old = event.old.and_then(|old| button_map.get(&old.name)); let old = event.old.and_then(|old| button_map.get(&old.name));
@@ -108,6 +115,8 @@ impl Module<gtk::Box> for WorkspacesModule {
if let Some(new) = new { if let Some(new) = new {
new.style_context().add_class("focused"); new.style_context().add_class("focused");
} }
trace!("{:?} {:?}", old, new);
} }
"init" => { "init" => {
if let Some(workspace) = event.current { if let Some(workspace) = event.current {
@@ -120,6 +129,21 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
} }
"move" => {
if let Some(workspace) = event.current {
if !self.all_monitors {
if workspace.output == output_name {
let item = workspace.as_button(&name_map, &ui_tx);
item.show();
menubar.add(&item);
button_map.insert(workspace.name, item);
} else if let Some(item) = button_map.get(&workspace.name) {
menubar.remove(item);
}
}
}
}
"empty" => { "empty" => {
if let Some(workspace) = event.current { if let Some(workspace) = event.current {
if let Some(item) = button_map.get(&workspace.name) { if let Some(item) = button_map.get(&workspace.name) {
@@ -135,8 +159,12 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
spawn(async move { spawn(async move {
let mut sway = SwayClient::connect()?; trace!("Setting up UI event handler");
let sway = get_client();
while let Some(name) = ui_rx.recv().await { while let Some(name) = ui_rx.recv().await {
let mut sway = sway
.lock()
.expect("Failed to get write lock on Sway IPC client");
sway.run(format!("workspace {}", name))?; sway.run(format!("workspace {}", name))?;
} }

View File

@@ -11,6 +11,9 @@ pub struct Popup {
} }
impl Popup { impl Popup {
/// Creates a new popup window.
/// This includes setting up gtk-layer-shell
/// and an empty `gtk::Box` container.
pub fn new( pub fn new(
name: &str, name: &str,
app: &Application, app: &Application,

View File

@@ -2,13 +2,16 @@ use color_eyre::{Help, Report};
use glib::Continue; use glib::Continue;
use gtk::prelude::CssProviderExt; use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext}; use gtk::{gdk, gio, CssProvider, StyleContext};
use notify::{DebouncedEvent, RecursiveMode, Watcher}; use notify::{Event, RecursiveMode, Result, Watcher};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
use tokio::spawn; use tokio::spawn;
use tracing::{error, info}; use tracing::{error, info};
/// Attempts to load CSS file at the given path
/// and attach if to the current GTK application.
///
/// Installs a file watcher and reloads CSS when
/// write changes are detected on the file.
pub fn load_css(style_path: PathBuf) { pub fn load_css(style_path: PathBuf) {
let provider = CssProvider::new(); let provider = CssProvider::new();
@@ -23,21 +26,22 @@ pub fn load_css(style_path: PathBuf) {
let screen = gdk::Screen::default().expect("Failed to get default GTK screen"); let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(&screen, &provider, 800); StyleContext::add_provider_for_screen(&screen, &provider, 800);
let (watcher_tx, watcher_rx) = mpsc::channel::<DebouncedEvent>();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
match notify::watcher(watcher_tx, Duration::from_millis(500)) { match notify::recommended_watcher(move |res: Result<Event>| match res {
Ok(event) => {
if let Some(path) = event.paths.first() {
tx.send(path.clone())
.expect("Failed to send style changed message");
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
}) {
Ok(mut watcher) => { Ok(mut watcher) => {
watcher watcher
.watch(&style_path, RecursiveMode::NonRecursive) .watch(&style_path, RecursiveMode::NonRecursive)
.expect("Unexpected error when attempting to watch CSS"); .expect("Unexpected error when attempting to watch CSS");
loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).expect("Failed to send style changed message");
}
}
} }
Err(err) => error!( Err(err) => error!(
"{:?}", "{:?}",

View File

@@ -1,17 +1,22 @@
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use crossbeam_channel::Receiver;
use ksway::{Error, IpcCommand, IpcEvent}; use ksway::{Error, IpcCommand, IpcEvent};
use lazy_static::lazy_static;
use serde::Deserialize; use serde::Deserialize;
use std::sync::{Arc, Mutex};
use tokio::spawn;
use tracing::{debug, info, trace};
pub mod node; pub mod node;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Clone)]
pub struct WorkspaceEvent { pub struct WorkspaceEvent {
pub change: String, pub change: String,
pub old: Option<Workspace>, pub old: Option<Workspace>,
pub current: Option<Workspace>, pub current: Option<Workspace>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Clone)]
pub struct Workspace { pub struct Workspace {
pub name: String, pub name: String,
pub focused: bool, pub focused: bool,
@@ -19,13 +24,13 @@ pub struct Workspace {
pub output: String, pub output: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct WindowEvent { pub struct WindowEvent {
pub change: String, pub change: String,
pub container: SwayNode, pub container: SwayNode,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct SwayNode { pub struct SwayNode {
#[serde(rename = "type")] #[serde(rename = "type")]
pub node_type: String, pub node_type: String,
@@ -40,7 +45,7 @@ pub struct SwayNode {
pub window_properties: Option<WindowProperties>, pub window_properties: Option<WindowProperties>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct WindowProperties { pub struct WindowProperties {
pub class: Option<String>, pub class: Option<String>,
} }
@@ -50,51 +55,115 @@ pub struct SwayOutput {
pub name: String, pub name: String,
} }
type Broadcaster<T> = Arc<Mutex<UnboundedBroadcast<T>>>;
pub struct SwayClient { pub struct SwayClient {
client: ksway::Client, client: ksway::Client,
workspace_bc: Broadcaster<WorkspaceEvent>,
window_bc: Broadcaster<WindowEvent>,
} }
impl SwayClient { impl SwayClient {
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> { fn connect() -> Result<Self> {
match self.client.run(cmd) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
}
impl SwayClient {
pub fn connect() -> Result<Self> {
let client = match ksway::Client::connect() { let client = match ksway::Client::connect() {
Ok(client) => Ok(client), Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)), Err(err) => Err(get_client_error(err)),
}?; }?;
info!("Sway IPC client connected");
Ok(Self { client }) let workspace_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
let window_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
let workspace_bc2 = workspace_bc.clone();
let window_bc2 = window_bc.clone();
spawn(async move {
let mut sub_client = match ksway::Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}
.expect("Failed to connect to Sway IPC server");
info!("Sway IPC subscription client connected");
let event_types = vec![IpcEvent::Window, IpcEvent::Workspace];
let rx = match sub_client.subscribe(event_types) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
.expect("Failed to subscribe to Sway IPC server");
loop {
while let Ok((ev_type, payload)) = rx.try_recv() {
debug!("Received sway event {:?}", ev_type);
match ev_type {
IpcEvent::Workspace => {
let json = serde_json::from_slice::<WorkspaceEvent>(&payload).expect(
"Received invalid workspace event payload from Sway IPC server",
);
workspace_bc
.lock()
.expect("Failed to get lock on workspace event bus")
.send(json)
.expect("Failed to broadcast workspace event");
}
IpcEvent::Window => {
let json = serde_json::from_slice::<WindowEvent>(&payload).expect(
"Received invalid window event payload from Sway IPC server",
);
window_bc
.lock()
.expect("Failed to get lock on window event bus")
.send(json)
.expect("Failed to broadcast window event");
}
_ => {}
}
}
match sub_client.poll() {
Ok(()) => Ok(()),
Err(err) => Err(get_client_error(err)),
}
.expect("Failed to poll Sway IPC client");
}
});
Ok(Self {
client,
workspace_bc: workspace_bc2,
window_bc: window_bc2,
})
} }
pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> { pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> {
debug!("Sending command: {:?}", command);
match self.client.ipc(command) { match self.client.ipc(command) {
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)), Err(err) => Err(get_client_error(err)),
} }
} }
pub fn subscribe( pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
&mut self, debug!("Sending command: {}", cmd);
event_types: Vec<IpcEvent>, match self.client.run(cmd) {
) -> Result<crossbeam_channel::Receiver<(IpcEvent, Vec<u8>)>> {
match self.client.subscribe(event_types) {
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)), Err(err) => Err(get_client_error(err)),
} }
} }
pub fn poll(&mut self) -> Result<()> { pub fn subscribe_workspace(&mut self) -> Receiver<WorkspaceEvent> {
match self.client.poll() { trace!("Adding new workspace subscriber");
Ok(()) => Ok(()), self.workspace_bc
Err(err) => Err(get_client_error(err)), .lock()
} .expect("Failed to get lock on workspace event bus")
.subscribe()
}
pub fn subscribe_window(&mut self) -> Receiver<WindowEvent> {
trace!("Adding new window subscriber");
self.window_bc
.lock()
.expect("Failed to get lock on window event bus")
.subscribe()
} }
} }
@@ -107,3 +176,49 @@ pub fn get_client_error(error: Error) -> Report {
Error::Io(err) => Report::new(err), Error::Io(err) => Report::new(err),
} }
} }
lazy_static! {
static ref CLIENT: Arc<Mutex<SwayClient>> = {
let client = SwayClient::connect();
match client {
Ok(client) => Arc::new(Mutex::new(client)),
Err(err) => panic!("{:?}", err),
}
};
}
pub fn get_client() -> Arc<Mutex<SwayClient>> {
Arc::clone(&CLIENT)
}
/// Crossbeam channel wrapper
/// which sends messages to all receivers.
pub struct UnboundedBroadcast<T> {
channels: Vec<crossbeam_channel::Sender<T>>,
}
impl<T: 'static + Clone + Send + Sync> UnboundedBroadcast<T> {
/// Creates a new broadcaster.
pub const fn new() -> Self {
Self { channels: vec![] }
}
/// Creates a new sender/receiver pair.
/// The sender is stored locally and the receiver is returned.
pub fn subscribe(&mut self) -> Receiver<T> {
let (tx, rx) = crossbeam_channel::unbounded();
self.channels.push(tx);
rx
}
/// Attempts to send a messsge to all receivers.
pub fn send(&self, message: T) -> Result<(), crossbeam_channel::SendError<T>> {
for c in &self.channels {
c.send(message.clone())?;
}
Ok(())
}
}

View File

@@ -3,6 +3,9 @@ use color_eyre::Result;
use ksway::IpcCommand; use ksway::IpcCommand;
impl SwayNode { impl SwayNode {
/// Gets either the `app_id` or `class`
/// depending on whether this is a native Wayland
/// or xwayland application.
pub fn get_id(&self) -> &str { pub fn get_id(&self) -> &str {
self.app_id.as_ref().map_or_else( self.app_id.as_ref().map_or_else(
|| { || {
@@ -17,11 +20,15 @@ impl SwayNode {
) )
} }
/// Checks whether this application
/// is running under xwayland.
pub fn is_xwayland(&self) -> bool { pub fn is_xwayland(&self) -> bool {
self.shell == Some(String::from("xwayland")) self.shell == Some(String::from("xwayland"))
} }
} }
/// Recursively checks the provided node for any child application nodes.
/// Returns a list of any found application nodes.
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) { fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") { if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
window_nodes.push(node); window_nodes.push(node);
@@ -37,6 +44,7 @@ fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
} }
impl SwayClient { impl SwayClient {
/// Gets a flat vector of all currently open windows.
pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> { pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> {
let root_node = self.ipc(IpcCommand::GetTree)?; let root_node = self.ipc(IpcCommand::GetTree)?;
let root_node = serde_json::from_slice(&root_node)?; let root_node = serde_json::from_slice(&root_node)?;