20 Commits

Author SHA1 Message Date
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
Jake Stanger
1d7c3772e4 chore(release): v0.4.0 2022-08-22 23:09:01 +01:00
Jake Stanger
f2ee2dfe7a fix: error when using with swaybar_command 2022-08-22 23:08:41 +01:00
Jake Stanger
ab8f7ecfc8 feat: logging support and proper error handling 2022-08-21 23:36:51 +01:00
Jake Stanger
917838c98c ci(deploy): fix reading wrong branch for changelog 2022-08-15 21:26:13 +01:00
Jake Stanger
5ec46b2a2a ci(build): change job name 2022-08-15 21:26:01 +01:00
36 changed files with 1482 additions and 416 deletions

View File

@@ -1,4 +1,4 @@
name: Rust name: Build
on: on:
push: push:

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
@@ -36,7 +39,7 @@ jobs:
- name: Commit CHANGELOG.md - name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@v4 uses: stefanzweifel/git-auto-commit-action@v4
with: with:
branch: main branch: master
commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]' commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]'
file_pattern: CHANGELOG.md file_pattern: CHANGELOG.md

View File

@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used" /> <option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" /> <option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" /> <option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" /> <option name="requiredFeatures" value="false" />

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

@@ -9,7 +9,10 @@
<option name="withSudo" value="false" /> <option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" /> <option name="buildTarget" value="REMOTE" />
<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="RUST_LOG" value="trace" />
</envs>
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />
<method v="2"> <method v="2">

31
CHANGELOG.md Normal file
View File

@@ -0,0 +1,31 @@
# 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.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

276
Cargo.lock generated
View File

@@ -2,6 +2,21 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.6" version = "0.7.6"
@@ -22,12 +37,27 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.58" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.4.1" version = "0.4.1"
@@ -142,6 +172,21 @@ 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 = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -244,7 +289,7 @@ dependencies = [
"libc", "libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time", "time 0.1.44",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@@ -287,6 +332,33 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "color-eyre"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]] [[package]]
name = "colored" name = "colored"
version = "2.0.0" version = "2.0.0"
@@ -500,6 +572,16 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
dependencies = [
"indenter",
"once_cell",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.8.0" version = "1.8.0"
@@ -737,6 +819,12 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.15.12" version = "0.15.12"
@@ -983,6 +1071,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.1" version = "1.9.1"
@@ -1033,16 +1127,19 @@ dependencies = [
[[package]] [[package]]
name = "ironbar" name = "ironbar"
version = "0.3.0" version = "0.5.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"color-eyre",
"cornfig", "cornfig",
"crossbeam-channel 0.3.9",
"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",
@@ -1050,9 +1147,14 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml 0.9.4", "serde_yaml 0.9.4",
"stray", "stray",
"strip-ansi-escapes",
"sysinfo", "sysinfo",
"tokio", "tokio",
"toml", "toml",
"tracing",
"tracing-appender",
"tracing-error",
"tracing-subscriber",
"walkdir", "walkdir",
] ]
@@ -1141,6 +1243,15 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
@@ -1162,6 +1273,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.6.23" version = "0.6.23"
@@ -1345,6 +1465,24 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.0" version = "1.13.0"
@@ -1367,6 +1505,12 @@ version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "owo-colors"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.15.10" version = "0.15.10"
@@ -1644,6 +1788,15 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.27" version = "0.6.27"
@@ -1659,6 +1812,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.2.3" version = "0.2.3"
@@ -1824,6 +1983,15 @@ 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 = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.7" version = "0.4.7"
@@ -1871,6 +2039,15 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
dependencies = [
"vte",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -1996,6 +2173,17 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "time"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
dependencies = [
"itoa",
"libc",
"num_threads",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.20.1" version = "1.20.1"
@@ -2058,6 +2246,17 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
dependencies = [
"crossbeam-channel 0.5.6",
"time 0.3.13",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.22" version = "0.1.22"
@@ -2076,6 +2275,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b"
dependencies = [
"ansi_term",
"matchers",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@@ -2118,6 +2357,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.1.0" version = "0.1.0"
@@ -2130,6 +2381,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vte"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2 1.0.42",
"quote 1.0.20",
]
[[package]] [[package]]
name = "waker-fn" name = "waker-fn"
version = "1.1.0" version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.3.0" version = "0.5.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable wlroots/sway bar" description = "Customisable wlroots/sway bar"
@@ -11,8 +11,13 @@ description = "Customisable wlroots/sway bar"
gtk = "0.15.5" gtk = "0.15.5"
gtk-layer-shell = "0.4.1" gtk-layer-shell = "0.4.1"
glib = "0.15.12" glib = "0.15.12"
stray = "0.1.1"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] } tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2"
futures-util = "0.3.21" futures-util = "0.3.21"
chrono = "0.4.19" chrono = "0.4.19"
serde = { version = "1.0.141", features = ["derive"] } serde = { version = "1.0.141", features = ["derive"] }
@@ -20,10 +25,14 @@ 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.2.0"
mpd_client = "0.7.5" lazy_static = "1.4.0"
regex = "1.6.0" regex = "1.6.0"
ksway = "0.1.0" stray = "0.1.1"
sysinfo = "0.25.1"
dirs = "4.0.0" dirs = "4.0.0"
walkdir = "2.3.2" walkdir = "2.3.2"
notify = "4.0.17" notify = "4.0.17"
mpd_client = "0.7.5"
ksway = "0.1.0"
sysinfo = "0.25.1"
# required for wrapping ksway
crossbeam-channel = "0.3.9"

View File

@@ -26,6 +26,16 @@ cargo install ironbar
yay -S ironbar-git yay -S ironbar-git
``` ```
### Source
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
cargo build --release
# change path to wherever you want to install
install target/release/ironbar ~/.local/bin/ironbar
```
[aur package](https://aur.archlinux.org/packages/ironbar-git) [aur package](https://aur.archlinux.org/packages/ironbar-git)
## Configuration ## Configuration
@@ -44,37 +54,22 @@ A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/
## Project Status ## Project Status
This project is in very early stages: This project is in alpha, but should be usable.
Everything that is implemented works and should be documented.
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
- Error handling is barely implemented - expect crashes There is currently room for lots more modules, and lots more configuration options for the existing modules.
- There will be bugs! The current configuration schema is not set in stone and breaking changes could come along at any point;
- Lots of modules need more configuration options until the project matures I am more interested in ease of use than backwards compatibility.
- There's room for lots of modules
- The code is messy and quite prototypal in places
- Config options aren't set in stone - expect breaking changes
- Documentation is probably missing in lots of places
That said, it will be *actively developed* as I am using it on my daily driver. A few bugs do exist, and I am sure there are plenty more to be found.
The project will be *actively developed* as I am using it on my daily driver.
Bugs will be fixed, features will be added, code will be refactored. Bugs will be fixed, features will be added, code will be refactored.
## Contribution Guidelines ## Contribution Guidelines
I welcome contributions of any kind with open arms. That said, please do stick to some basics: Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
- For code contributions:
- Fix any `cargo clippy` warnings, using at least the default configuration.
- Make sure your code is formatted using `cargo fmt`.
- Keep any documentation up to date.
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
- For PRs:
- Please open an issue or discussion beforehand.
I'll accept most contributions, but it's best to make sure you're not working on something that won't get accepted :)
- For issues:
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
## Acknowledgements ## Acknowledgements

View File

@@ -1,11 +1,20 @@
use crate::config::{BarPosition, ModuleConfig}; use crate::config::{BarPosition, ModuleConfig};
use crate::modules::{Module, ModuleInfo, ModuleLocation}; use crate::modules::{Module, ModuleInfo, ModuleLocation};
use crate::Config; use crate::Config;
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};
pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, config: Config) { /// Creates a new window for a bar,
/// sets it up and adds its widgets.
pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: &str,
config: Config,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build(); let win = ApplicationWindow::builder().application(app).build();
setup_layer_shell(&win, monitor, &config.position); setup_layer_shell(&win, monitor, &config.position);
@@ -31,17 +40,22 @@ pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, conf
content.set_center_widget(Some(&center)); content.set_center_widget(Some(&center));
content.pack_end(&right, false, false, 0); content.pack_end(&right, false, false, 0);
load_modules(&left, &center, &right, app, config, monitor, monitor_name); load_modules(&left, &center, &right, app, config, monitor, monitor_name)?;
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(())
} }
/// 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,
@@ -50,7 +64,7 @@ fn load_modules(
config: Config, config: Config,
monitor: &Monitor, monitor: &Monitor,
output_name: &str, output_name: &str,
) { ) -> Result<()> {
if let Some(modules) = config.left { if let Some(modules) = config.left {
let info = ModuleInfo { let info = ModuleInfo {
app, app,
@@ -60,7 +74,7 @@ fn load_modules(
output_name, output_name,
}; };
add_modules(left, modules, &info); add_modules(left, modules, &info)?;
} }
if let Some(modules) = config.center { if let Some(modules) = config.center {
@@ -72,7 +86,7 @@ fn load_modules(
output_name, output_name,
}; };
add_modules(center, modules, &info); add_modules(center, modules, &info)?;
} }
if let Some(modules) = config.right { if let Some(modules) = config.right {
@@ -84,16 +98,21 @@ fn load_modules(
output_name, output_name,
}; };
add_modules(right, modules, &info); add_modules(right, modules, &info)?;
}
} }
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) { 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<()> {
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);
}}; }};
} }
@@ -109,8 +128,11 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
ModuleConfig::Focused(module) => add_module!(module, "focused"), ModuleConfig::Focused(module) => add_module!(module, "focused"),
} }
} }
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

@@ -6,7 +6,10 @@ use crate::modules::script::ScriptModule;
use crate::modules::sysinfo::SysInfoModule; use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule; use crate::modules::tray::TrayModule;
use crate::modules::workspaces::WorkspacesModule; use crate::modules::workspaces::WorkspacesModule;
use color_eyre::eyre::{Context, ContextCompat};
use color_eyre::{eyre, Help, Report};
use dirs::config_dir; use dirs::config_dir;
use eyre::Result;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -68,49 +71,74 @@ const fn default_bar_height() -> i32 {
} }
impl Config { impl Config {
pub fn load() -> Option<Self> { /// Attempts to load the config file from file,
if let Ok(config_path) = env::var("IRONBAR_CONFIG") { /// parse it and return a new instance of `Self`.
pub fn load() -> Result<Self> {
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
let path = PathBuf::from(config_path); let path = PathBuf::from(config_path);
Self::load_file( if path.exists() {
&path, Ok(path)
path.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
} else { } else {
let config_dir = config_dir().expect("Failed to locate user config dir"); Err(Report::msg("Specified config file does not exist")
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
}
} else {
Self::try_find_config()
}?;
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> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
let extensions = vec!["json", "toml", "yaml", "yml", "corn"]; let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
extensions.into_iter().find_map(|extension| { let file = extensions.into_iter().find_map(|extension| {
let full_path = config_dir let full_path = config_dir
.join("ironbar") .join("ironbar")
.join(format!("config.{extension}")); .join(format!("config.{extension}"));
Self::load_file(&full_path, extension) if Path::exists(&full_path) {
}) Some(full_path)
} else {
None
}
});
match file {
Some(file) => Ok(file),
None => Err(Report::msg("Could not find config file")),
} }
} }
fn load_file(path: &Path, extension: &str) -> Option<Self> { /// Loads the config file at the specified path
if path.exists() { /// and parses it into `Self` based on its extension.
let file = fs::read(path).expect("Failed to read config file"); fn load_file(path: &Path) -> Result<Self> {
Some(match extension { let file = fs::read(path).wrap_err("Failed to read config file")?;
"json" => serde_json::from_slice(&file).expect("Invalid JSON config"), let extension = path
"toml" => toml::from_slice(&file).expect("Invalid TOML config"), .extension()
"yaml" | "yml" => serde_yaml::from_slice(&file).expect("Invalid YAML config"), .unwrap_or_default()
.to_str()
.unwrap_or_default();
match extension {
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
"corn" => { "corn" => {
// corn doesn't support deserialization yet // corn doesn't support deserialization yet
// so serialize the interpreted result then deserialize that // so serialize the interpreted result then deserialize that
let file = String::from_utf8(file).expect("Config file contains invalid UTF-8"); let file =
let config = cornfig::parse(&file).expect("Invalid corn config").value; String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap() let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
} }
_ => unreachable!(), _ => unreachable!(),
})
} else {
None
} }
} }
} }

View File

@@ -58,9 +58,7 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
let mut map = HashMap::new(); let mut map = HashMap::new();
for line in lines.flatten() { for line in lines.flatten() {
let is_pair = line.contains('='); if let Some((key, value)) = line.split_once('=') {
if is_pair {
let (key, value) = line.split_once('=').unwrap();
map.insert(key.to_string(), value.to_string()); map.insert(key.to_string(), value.to_string());
} }
} }
@@ -88,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())
@@ -100,14 +102,19 @@ fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconL
let is_steam_game = app_id.starts_with("steam_app_"); let is_steam_game = app_id.starts_with("steam_app_");
if is_steam_game { if is_steam_game {
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect(); let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
let home_dir = dirs::data_dir().unwrap();
let path = home_dir.join(format!( return match dirs::data_dir() {
Some(dir) => {
let path = dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{}.png", "icons/hicolor/32x32/apps/steam_icon_{}.png",
steam_id steam_id
)); ));
return Some(IconLocation::File(path)); return Some(IconLocation::File(path));
} }
None => None,
};
}
let icon_name = get_desktop_icon_name(app_id); let icon_name = get_desktop_icon_name(app_id);
if let Some(icon_name) = icon_name { if let Some(icon_name) = icon_name {

57
src/logging.rs Normal file
View File

@@ -0,0 +1,57 @@
use color_eyre::Result;
use dirs::data_dir;
use std::env;
use strip_ansi_escapes::Writer;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_error::ErrorLayer;
use tracing_subscriber::fmt::{Layer, MakeWriter};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
struct MakeFileWriter {
file_writer: NonBlocking,
}
impl MakeFileWriter {
const fn new(file_writer: NonBlocking) -> Self {
Self { file_writer }
}
}
impl<'a> MakeWriter<'a> for MakeFileWriter {
type Writer = Writer<NonBlocking>;
fn make_writer(&'a self) -> Self::Writer {
Writer::new(self.file_writer.clone())
}
}
/// 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> {
let fmt_layer = fmt::layer().with_target(true);
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
let file_filter_layer =
EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn"))?;
let log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");
let appender = tracing_appender::rolling::never(log_path, "error.log");
let (file_writer, guard) = tracing_appender::non_blocking(appender);
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.with(
Layer::default()
.with_writer(MakeFileWriter::new(file_writer))
.with_ansi(false)
.with_filter(file_filter_layer),
)
.init();
Ok(guard)
}

View File

@@ -2,6 +2,7 @@ mod bar;
mod collection; mod collection;
mod config; mod config;
mod icon; mod icon;
mod logging;
mod modules; mod modules;
mod popup; mod popup;
mod style; mod style;
@@ -10,74 +11,137 @@ 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::SwayOutput; use crate::sway::{get_client, SwayOutput};
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir; use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{gdk, Application}; use gtk::Application;
use ksway::client::Client;
use ksway::IpcCommand; use ksway::IpcCommand;
use std::env;
use std::process::exit;
use crate::logging::install_tracing;
use tracing::{debug, error, info};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<()> {
// Disable backtraces by default
if env::var("RUST_LIB_BACKTRACE").is_err() {
env::set_var("RUST_LIB_BACKTRACE", "0");
}
// keep guard in scope
// otherwise file logging drops
let _guard = install_tracing()?;
color_eyre::install()?;
info!("Ironbar version {}", VERSION);
info!("Starting application");
let app = Application::builder() let app = Application::builder()
.application_id("dev.jstanger.waylandbar") .application_id("dev.jstanger.ironbar")
.build(); .build();
let mut sway_client = Client::connect().expect("Failed to connect to Sway IPC");
let outputs = sway_client
.ipc(IpcCommand::GetOutputs)
.expect("Failed to get Sway outputs");
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)
.expect("Failed to deserialize outputs message from Sway IPC");
app.connect_activate(move |app| { app.connect_activate(move |app| {
let config = Config::load().unwrap_or_default(); let display = match Display::default() {
Some(display) => display,
None => {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(1)
}
};
// TODO: Better logging (https://crates.io/crates/tracing) let config = match Config::load() {
// TODO: error handling (https://crates.io/crates/color-eyre) Ok(config) => config,
Err(err) => {
error!("{:?}", err);
Config::default()
}
};
debug!("Loaded config file");
// TODO: Embedded Deno/lua - build custom modules via script??? if let Err(err) = create_bars(app, &display, &config) {
error!("{:?}", err);
exit(2);
}
debug!("Created bars");
let style_path = match config_dir() {
Some(dir) => dir.join("ironbar").join("style.css"),
None => {
let report = Report::msg("Failed to locate user config dir");
error!("{:?}", report);
exit(3);
}
};
if style_path.exists() {
load_css(style_path);
debug!("Loaded CSS watcher file");
}
});
// Ignore CLI args
// Some are provided by swaybar_config but not currently supported
app.run_with_args(&Vec::<&str>::new());
Ok(())
}
/// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
let outputs = {
let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
let outputs = sway.ipc(IpcCommand::GetOutputs);
match outputs {
Ok(outputs) => Ok(outputs),
Err(err) => Err(err),
}
}?;
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
debug!("Received {} outputs from Sway IPC", outputs.len());
let display = gdk::Display::default().expect("Failed to get default GDK display");
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).unwrap(); 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 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;
.get(i as usize)
.expect("GTK monitor output differs from Sway's")
.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| {
let config = config.get(monitor_name); let config = config.get(monitor_name);
match &config { match &config {
Some(MonitorConfig::Single(config)) => { Some(MonitorConfig::Single(config)) => {
create_bar(app, &monitor, monitor_name, config.clone()); create_bar(app, &monitor, monitor_name, config.clone())
} }
Some(MonitorConfig::Multiple(configs)) => { Some(MonitorConfig::Multiple(configs)) => {
for config in configs { for config in configs {
create_bar(app, &monitor, monitor_name, config.clone()); create_bar(app, &monitor, monitor_name, config.clone())?;
} }
Ok(())
} }
_ => {} _ => Ok(()),
} }
}, },
) )?;
} }
let style_path = config_dir() Ok(())
.expect("Failed to locate user config dir")
.join("ironbar")
.join("style.css");
if style_path.exists() {
load_css(style_path);
}
});
app.run();
} }

View File

@@ -3,6 +3,7 @@ mod popup;
use self::popup::Popup; use self::popup::Popup;
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use chrono::Local; use chrono::Local;
use color_eyre::Result;
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
@@ -26,7 +27,7 @@ fn default_format() -> String {
} }
impl Module<Button> for ClockModule { impl Module<Button> for ClockModule {
fn into_widget(self, info: &ModuleInfo) -> Button { fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let button = Button::new(); let button = Button::new();
let popup = Popup::new( let popup = Popup::new(
@@ -51,7 +52,8 @@ impl Module<Button> for ClockModule {
let date = Local::now(); let date = Local::now();
let date_string = format!("{}", date.format(format)); let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap(); tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await; sleep(tokio::time::Duration::from_millis(500)).await;
} }
}); });
@@ -64,6 +66,6 @@ impl Module<Button> for ClockModule {
}); });
} }
button Ok(button)
} }
} }

View File

@@ -24,7 +24,8 @@ impl Popup {
let date = Local::now(); let date = Local::now();
let date_string = format!("{}", date.format(format)); let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap(); tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await; sleep(tokio::time::Duration::from_millis(500)).await;
} }
}); });

View File

@@ -1,23 +1,26 @@
use crate::icon; use crate::icon;
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use crate::sway::node::get_open_windows; use crate::sway::get_client;
use crate::sway::WindowEvent; 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::{Client, IpcEvent};
use serde::Deserialize; use serde::Deserialize;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
#[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>,
} }
@@ -26,7 +29,7 @@ const fn default_icon_size() -> i32 {
} }
impl Module<gtk::Box> for FocusedModule { impl Module<gtk::Box> for FocusedModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box { fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new(); let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme { if let Some(theme) = self.icon_theme {
@@ -41,23 +44,28 @@ impl Module<gtk::Box> for FocusedModule {
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);
let mut sway = Client::connect().unwrap();
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let focused = get_open_windows(&mut sway) let focused = {
let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.get_open_windows()?
.into_iter() .into_iter()
.find(|node| node.focused); .find(|node| node.focused)
};
if let Some(focused) = focused { if let Some(focused) = focused {
tx.send(focused).unwrap(); tx.send(focused)?;
} }
spawn_blocking(move || loop { spawn_blocking(move || {
while let Ok((_, payload)) = srx.try_recv() { let srx = {
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap(); let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.subscribe_window()
};
while let Ok(payload) = srx.recv() {
let update = match payload.change.as_str() { let update = match payload.change.as_str() {
"focus" => true, "focus" => true,
"title" => payload.container.focused, "title" => payload.container.focused,
@@ -65,10 +73,10 @@ impl Module<gtk::Box> for FocusedModule {
}; };
if update { if update {
tx.send(payload.container).unwrap(); tx.send(payload.container)
.expect("Failed to sendf focus update");
} }
} }
sway.poll().unwrap();
}); });
{ {
@@ -89,6 +97,6 @@ impl Module<gtk::Box> for FocusedModule {
}); });
} }
container Ok(container)
} }
} }

View File

@@ -1,21 +1,25 @@
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;
use crate::Report;
use color_eyre::Help;
use gtk::prelude::*; 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;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
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,
} }
@@ -24,14 +28,13 @@ 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)] #[derive(Debug, Clone)]
pub struct State { pub struct State {
pub is_xwayland: bool, pub is_xwayland: bool,
pub open: bool, pub open_state: OpenState,
pub focused: bool,
pub urgent: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -49,16 +52,14 @@ impl LauncherItem {
button.style_context().add_class("item"); button.style_context().add_class("item");
let state = State { let state = State {
open: false, open_state: OpenState::Closed,
focused: false,
urgent: false,
is_xwayland: false, is_xwayland: false,
}; };
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,
}; };
@@ -76,20 +77,19 @@ 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),
}, },
)); ));
let state = State { let state = State {
open: true, open_state: OpenState::from_node(node),
focused: node.focused,
urgent: node.urgent,
is_xwayland: node.is_xwayland(), is_xwayland: node.is_xwayland(),
}; };
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,
}; };
@@ -101,10 +101,17 @@ 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().unwrap(); 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.first().unwrap().name.as_ref() windows
.first()
.expect("Failed to get first window")
.name
.as_ref()
} else { } else {
Some(&self.app_id) Some(&self.app_id)
}; };
@@ -129,21 +136,31 @@ impl LauncherItem {
let (focus_tx, mut focus_rx) = mpsc::channel(32); let (focus_tx, mut focus_rx) = mpsc::channel(32);
button.connect_clicked(move |_| { button.connect_clicked(move |_| {
let state = state.read().unwrap(); let state = state.read().expect("Failed to get read lock on state");
if state.open { if state.open_state.is_open() {
focus_tx.try_send(()).unwrap(); 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
match find_desktop_file(&app_id) { match find_desktop_file(&app_id) {
Some(file) => { Some(file) => {
Command::new("gtk-launch") if let Err(err) = Command::new("gtk-launch")
.arg(file.file_name().unwrap()) .arg(
file.file_name()
.expect("File segment missing from path to desktop file"),
)
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.spawn() .spawn()
.unwrap(); {
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run gtk-launch command.")
.suggestion("Perhaps the desktop file is invalid?")
);
} }
None => (), }
None => error!("Could not find desktop file for {}", app_id),
} }
} }
}); });
@@ -153,15 +170,15 @@ impl LauncherItem {
spawn(async move { spawn(async move {
while focus_rx.recv().await == Some(()) { while focus_rx.recv().await == Some(()) {
let state = state.read().unwrap(); let state = state.read().expect("Failed to get read lock on state");
if state.is_xwayland { if state.is_xwayland {
tx_click tx_click
.try_send(FocusEvent::Class(app_id.clone())) .try_send(FocusEvent::Class(app_id.clone()))
.unwrap(); .expect("Failed to send focus event");
} else { } else {
tx_click tx_click
.try_send(FocusEvent::AppId(app_id.clone())) .try_send(FocusEvent::AppId(app_id.clone()))
.unwrap(); .expect("Failed to send focus event");
} }
} }
}); });
@@ -172,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().unwrap(); 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);
@@ -196,7 +213,7 @@ impl LauncherItem {
let style = button.style_context(); let style = button.style_context();
style.add_class("launcher-item"); style.add_class("launcher-item");
self.update_button_classes(&self.state.read().unwrap()); self.update_button_classes(&self.state.read().expect("Failed to get read lock on state"));
button.show_all(); button.show_all();
} }
@@ -223,22 +240,53 @@ impl LauncherItem {
style.remove_class("favorite"); style.remove_class("favorite");
} }
if state.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.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.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,29 +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}; 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::node::get_open_windows; use crate::sway::{get_client, SwayNode};
use crate::sway::{SwayNode, WindowEvent}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconTheme, Orientation}; use gtk::{IconTheme, Orientation};
use ksway::{Client, 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::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>,
} }
@@ -68,29 +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.state.write().unwrap(); let mut state = item
state.open = true; .state
state.focused = window.focused || state.focused; .write()
state.urgent = window.urgent || state.urgent; .expect("Failed to get write lock on state");
state.is_xwayland = window.is_xwayland(); let new_open_state = OpenState::from_node(&node);
state.open_state = OpenState::merge_states(vec![&state.open_state, &new_open_state]);
state.is_xwayland = node.is_xwayland();
item.update_button_classes(&state); item.update_button_classes(&state);
let mut windows = item.windows.lock().unwrap(); 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);
@@ -103,17 +118,21 @@ 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().unwrap(); let mut windows = windows
.write()
.expect("Failed to get write lock on windows");
windows.remove(&window.id); windows.remove(&window.id);
if windows.is_empty() { if windows.is_empty() {
let mut state = item.state.write().unwrap(); let mut state = item.state.write().expect("Failed to get lock on windows");
state.open = false; state.open_state = OpenState::Closed;
item.update_button_classes(&state); item.update_button_classes(&state);
if item.favorite { if item.favorite {
@@ -134,63 +153,100 @@ 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 debug!("Setting window with ID {} focused", id);
.items
.iter_mut() let prev_focused = self.items.iter_mut().find(|item| {
.find(|item| item.state.read().unwrap().focused); item.state
if let Some(currently_focused) = currently_focused { .read()
let mut state = currently_focused.state.write().unwrap(); .expect("Failed to get read lock on state")
state.focused = false; .open_state
currently_focused.update_button_classes(&state); .is_focused()
});
if let Some(prev_focused) = prev_focused {
let mut state = prev_focused
.state
.write()
.expect("Failed to get write lock on 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);
if let Some(item) = item { if let Some(item) = item {
let mut state = item.state.write().unwrap(); let mut state = item
state.focused = true; .state
.write()
.expect("Failed to get write lock on state");
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().unwrap(); 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) {
window.name = Some(name);
} else { } else {
windows.get_mut(&window.id).unwrap().name = Some(name); // This should never happen
// But makes more sense to wipe title than keep old one in case of error
item.set_title("", &self.button_config);
} }
} }
} }
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.state.write().unwrap(); let mut state = item
state.urgent = window.urgent; .state
.write()
.expect("Failed to get write lock on state");
item.set_window_open_state(node.id, OpenState::urgent(node.urgent), &mut state);
item.update_button_classes(&state); item.update_button_classes(&state);
} }
} }
} }
impl Module<gtk::Box> for LauncherModule { impl Module<gtk::Box> for LauncherModule {
fn into_widget(self, info: &ModuleInfo) -> gtk::Box { fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new(); let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme { if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme)); icon_theme.set_custom_theme(Some(&theme));
} }
let mut sway = Client::connect().unwrap();
let popup = Popup::new( let popup = Popup::new(
"popup-launcher", "popup-launcher",
info.app, info.app,
@@ -216,22 +272,29 @@ impl Module<gtk::Box> for LauncherModule {
button_config, button_config,
); );
let open_windows = get_open_windows(&mut sway); 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]).unwrap();
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 = {
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap(); let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.subscribe_window()
};
tx.send(payload).unwrap(); while let Ok(payload) = srx.recv() {
tx.send(payload)
.expect("Failed to send window event payload");
} }
sway.poll().unwrap();
}); });
{ {
@@ -250,18 +313,21 @@ impl Module<gtk::Box> for LauncherModule {
} }
spawn(async move { spawn(async move {
let mut sway = Client::connect().unwrap(); 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)).unwrap(); sway.run(format!("{} focus", selector))?;
} }
Ok::<(), Report>(())
}); });
container Ok(container)
} }
} }

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

@@ -25,7 +25,8 @@ impl Popup {
let window = self.window.clone(); let window = self.window.clone();
let tx = tx.clone(); let tx = tx.clone();
button.connect_clicked(move |_| { button.connect_clicked(move |_| {
tx.try_send(FocusEvent::ConId(con_id)).unwrap(); tx.try_send(FocusEvent::ConId(con_id))
.expect("Failed to send focus event");
window.hide(); window.hide();
}); });

View File

@@ -14,13 +14,12 @@ pub mod tray;
pub mod workspaces; pub mod workspaces;
use crate::config::BarPosition; use crate::config::BarPosition;
use color_eyre::Result;
/// Shamelessly stolen from here: /// Shamelessly stolen from here:
/// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs> /// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs>
use glib::IsA; use glib::IsA;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::{Application, Widget}; use gtk::{Application, Widget};
use serde::de::DeserializeOwned;
use serde_json::Value;
#[derive(Clone)] #[derive(Clone)]
pub enum ModuleLocation { pub enum ModuleLocation {
@@ -43,12 +42,5 @@ where
{ {
/// Consumes the module config /// Consumes the module config
/// and produces a GTK widget of type `W` /// and produces a GTK widget of type `W`
fn into_widget(self, info: &ModuleInfo) -> W; fn into_widget(self, info: &ModuleInfo) -> Result<W>;
fn from_value(v: &Value) -> Box<Self>
where
Self: DeserializeOwned,
{
serde_json::from_value(v.clone()).unwrap()
}
} }

View File

@@ -1,58 +1,89 @@
use lazy_static::lazy_static;
use mpd_client::commands::responses::Status; use mpd_client::commands::responses::Status;
use mpd_client::raw::MpdProtocolError; use mpd_client::raw::MpdProtocolError;
use mpd_client::{Client, Connection}; use mpd_client::{Client, Connection};
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpStream, UnixStream}; use tokio::net::{TcpStream, UnixStream};
use tokio::sync::Mutex;
use tokio::time::sleep;
fn is_unix_socket(host: &String) -> bool { lazy_static! {
PathBuf::from(host).is_file() static ref CLIENTS: Arc<Mutex<HashMap<String, Arc<Client>>>> =
Arc::new(Mutex::new(HashMap::new()));
} }
pub async fn get_connection(host: &String) -> Result<Connection, MpdProtocolError> { pub async fn get_connection(host: &str) -> Option<Arc<Client>> {
if is_unix_socket(host) { 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,
max_retries: Option<usize>,
) -> Option<Client> {
let mut retries = 0;
let max_retries = max_retries.unwrap_or(usize::MAX);
loop {
if retries == max_retries {
break None;
}
if let Some(conn) = try_get_mpd_conn(host).await {
break Some(conn.0);
}
retries += 1;
sleep(interval).await;
}
}
/// Cycles through each MPD host and
/// returns the first one which connects,
/// or none if there are none
async fn try_get_mpd_conn(host: &str) -> Option<Connection> {
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
} };
connection.ok()
} }
async fn connect_unix(host: &String) -> Result<Connection, MpdProtocolError> { fn is_unix_socket(host: &str) -> bool {
let connection = UnixStream::connect(host) PathBuf::from(host).is_file()
.await }
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await Client::connect(connection).await
} }
async fn connect_tcp(host: &String) -> Result<Connection, MpdProtocolError> { async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host) let connection = TcpStream::connect(host).await?;
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
Client::connect(connection).await Client::connect(connection).await
} }
// /// Gets MPD server status.
// /// Panics on error.
// pub async fn get_status(client: &Client) -> Status {
// client
// .command(commands::Status)
// .await
// .expect("Failed to get MPD server status")
// }
/// Gets the duration of the current song /// Gets the duration of the current song
pub fn get_duration(status: &Status) -> u64 { pub fn get_duration(status: &Status) -> Option<u64> {
status status.duration.map(|duration| duration.as_secs())
.duration
.expect("Failed to get duration from MPD status")
.as_secs()
} }
/// Gets the elapsed time of the current song /// Gets the elapsed time of the current song
pub fn get_elapsed(status: &Status) -> u64 { pub fn get_elapsed(status: &Status) -> Option<u64> {
status status.elapsed.map(|duration| duration.as_secs())
.elapsed
.expect("Failed to get elapsed time from MPD status")
.as_secs()
} }

View File

@@ -5,7 +5,8 @@ use self::popup::Popup;
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed}; 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 dirs::home_dir; use color_eyre::Result;
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};
@@ -14,21 +15,28 @@ use mpd_client::{commands, Tag};
use regex::Regex; use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::time::sleep; use tokio::time::sleep;
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,
} }
@@ -41,16 +49,18 @@ fn default_format() -> String {
String::from("{icon} {title} / {artist}") String::from("{icon} {title} / {artist}")
} }
#[allow(clippy::unnecessary_wraps)]
fn default_icon_play() -> Option<String> { fn default_icon_play() -> Option<String> {
Some(String::from("")) Some(String::from(""))
} }
#[allow(clippy::unnecessary_wraps)]
fn default_icon_pause() -> Option<String> { fn default_icon_pause() -> Option<String> {
Some(String::from("")) Some(String::from(""))
} }
fn default_music_dir() -> PathBuf { fn default_music_dir() -> PathBuf {
home_dir().unwrap().join("Music") audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
} }
/// Attempts to read the first value for a tag /// Attempts to read the first value for a tag
@@ -84,8 +94,8 @@ enum Event {
} }
impl Module<Button> for MpdModule { impl Module<Button> for MpdModule {
fn into_widget(self, info: &ModuleInfo) -> Button { fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let re = Regex::new(r"\{([\w-]+)}").unwrap(); let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str()); let tokens = get_tokens(&re, self.format.as_str());
let button = Button::new(); let button = Button::new();
@@ -107,13 +117,17 @@ impl Module<Button> for MpdModule {
let music_dir = self.music_dir.clone(); let music_dir = self.music_dir.clone();
button.connect_clicked(move |_| { button.connect_clicked(move |_| {
click_tx.send(Event::Open).unwrap(); click_tx
.send(Event::Open)
.expect("Failed to send popup open event");
}); });
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, _) = get_connection(&host).await.unwrap(); // TODO: Handle connecting properly let client = get_connection(&host)
.await
.expect("Unexpected error when trying to connect to MPD server");
loop { loop {
let current_song = client.command(commands::CurrentSong).await; let current_song = client.command(commands::CurrentSong).await;
@@ -125,32 +139,38 @@ impl Module<Button> for MpdModule {
.await; .await;
tx.send(Event::Update(Box::new(Some((song.song, status, string))))) tx.send(Event::Update(Box::new(Some((song.song, status, string)))))
.unwrap(); .expect("Failed to send update event");
} else { } else {
tx.send(Event::Update(Box::new(None))).unwrap(); tx.send(Event::Update(Box::new(None)))
.expect("Failed to send update event");
} }
sleep(tokio::time::Duration::from_secs(1)).await; sleep(Duration::from_secs(1)).await;
} }
}); });
spawn(async move { spawn(async move {
let (client, _) = get_connection(&host2).await.unwrap(); // TODO: Handle connecting properly let client = get_connection(&host2)
.await
.expect("Unexpected error when trying to connect to MPD server");
while let Some(event) = ui_rx.recv().await { while let Some(event) = ui_rx.recv().await {
match event { let res = match event {
PopupEvent::Previous => client.command(commands::Previous).await, PopupEvent::Previous => client.command(commands::Previous).await,
PopupEvent::Toggle => { PopupEvent::Toggle => match client.command(commands::Status).await {
let status = client.command(commands::Status).await.unwrap(); Ok(status) => match status.state {
match status.state {
PlayState::Playing => client.command(commands::SetPause(true)).await, PlayState::Playing => client.command(commands::SetPause(true)).await,
PlayState::Paused => client.command(commands::SetPause(false)).await, PlayState::Paused => client.command(commands::SetPause(false)).await,
PlayState::Stopped => Ok(()), PlayState::Stopped => Ok(()),
} },
} Err(err) => Err(err),
},
PopupEvent::Next => client.command(commands::Next).await, PopupEvent::Next => client.command(commands::Next).await,
};
if let Err(err) = res {
error!("Failed to send command to MPD server: {:?}", err);
} }
.unwrap();
} }
}); });
@@ -178,7 +198,7 @@ impl Module<Button> for MpdModule {
}); });
}; };
button Ok(button)
} }
} }
@@ -220,9 +240,10 @@ impl MpdModule {
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)), "disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)), "genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
"track" => try_get_first_tag(song.tags.get(&Tag::Track)), "track" => try_get_first_tag(song.tags.get(&Tag::Track)),
"duration" => return format_time(get_duration(status)), "duration" => return get_duration(status).map(format_time).unwrap_or_default(),
"elapsed" => return format_time(get_elapsed(status)),
_ => return token.to_string(), "elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
_ => Some(token),
}; };
s.unwrap_or_default().to_string() s.unwrap_or_default().to_string()
} }

View File

@@ -90,17 +90,23 @@ impl MpdPopup {
let tx_prev = tx.clone(); let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| { btn_prev.connect_clicked(move |_| {
tx_prev.try_send(PopupEvent::Previous).unwrap(); tx_prev
.try_send(PopupEvent::Previous)
.expect("Failed to send prev track message");
}); });
let tx_toggle = tx.clone(); let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |_| { btn_play_pause.connect_clicked(move |_| {
tx_toggle.try_send(PopupEvent::Toggle).unwrap(); tx_toggle
.try_send(PopupEvent::Toggle)
.expect("Failed to send play/pause track message");
}); });
let tx_next = tx; let tx_next = tx;
btn_next.connect_clicked(move |_| { btn_next.connect_clicked(move |_| {
tx_next.try_send(PopupEvent::Next).unwrap(); tx_next
.try_send(PopupEvent::Next)
.expect("Failed to send next track message");
}); });
Self { Self {
@@ -121,7 +127,12 @@ impl MpdPopup {
// only update art when album changes // only update art when album changes
if prev_album != curr_album { if prev_album != curr_album {
let cover_path = path.join(song.file_path().parent().unwrap().join("cover.jpg")); let cover_path = path.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) { if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
self.cover.set_from_pixbuf(Some(&pixbuf)); self.cover.set_from_pixbuf(Some(&pixbuf));

View File

@@ -1,14 +1,18 @@
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use std::process::Command; use std::process::Command;
use tokio::spawn; use tokio::spawn;
use tokio::time::sleep; use tokio::time::sleep;
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,
} }
@@ -19,19 +23,15 @@ const fn default_interval() -> u64 {
} }
impl Module<Label> for ScriptModule { impl Module<Label> for ScriptModule {
fn into_widget(self, _info: &ModuleInfo) -> Label { fn into_widget(self, _info: &ModuleInfo) -> Result<Label> {
let label = Label::builder().use_markup(true).build(); let label = Label::builder().use_markup(true).build();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
loop { loop {
let output = Command::new("sh").arg("-c").arg(&self.path).output(); match self.run_script() {
if let Ok(output) = output { Ok(stdout) => tx.send(stdout).expect("Failed to send stdout"),
let stdout = String::from_utf8(output.stdout) Err(err) => error!("{:?}", err),
.map(|output| output.trim().to_string())
.expect("Script output not valid UTF-8");
tx.send(stdout).unwrap();
} }
sleep(tokio::time::Duration::from_millis(self.interval)).await; sleep(tokio::time::Duration::from_millis(self.interval)).await;
@@ -46,6 +46,34 @@ impl Module<Label> for ScriptModule {
}); });
} }
label Ok(label)
}
}
impl ScriptModule {
#[instrument]
fn run_script(&self) -> Result<String> {
let output = Command::new("sh")
.arg("-c")
.arg(&self.path)
.output()
.wrap_err("Failed to get script output")?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.wrap_err("Script stdout not valid UTF-8")?;
Ok(stdout)
} else {
let stderr = String::from_utf8(output.stderr)
.map(|output| output.trim().to_string())
.wrap_err("Script stderr not valid UTF-8")?;
Err(Report::msg(stderr)
.wrap_err("Script returned non-zero error code")
.suggestion("Check the path to your script")
.suggestion("Check the script for errors"))
}
} }
} }

View File

@@ -1,4 +1,5 @@
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Label, Orientation}; use gtk::{Label, Orientation};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
@@ -10,12 +11,13 @@ 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>,
} }
impl Module<gtk::Box> for SysInfoModule { impl Module<gtk::Box> for SysInfoModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box { fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let re = Regex::new(r"\{([\w-]+)}").unwrap(); let re = Regex::new(r"\{([\w-]+)}")?;
let container = gtk::Box::new(Orientation::Horizontal, 10); let container = gtk::Box::new(Orientation::Horizontal, 10);
@@ -46,7 +48,8 @@ impl Module<gtk::Box> for SysInfoModule {
format_info.insert("memory-percent", format!("{:0>2.0}", memory_percent)); format_info.insert("memory-percent", format!("{:0>2.0}", memory_percent));
format_info.insert("cpu-percent", format!("{:0>2.0}", cpu_percent)); format_info.insert("cpu-percent", format!("{:0>2.0}", cpu_percent));
tx.send(format_info).unwrap(); tx.send(format_info)
.expect("Failed to send system info map");
sleep(tokio::time::Duration::from_secs(1)).await; sleep(tokio::time::Duration::from_secs(1)).await;
} }
@@ -69,6 +72,6 @@ impl Module<gtk::Box> for SysInfoModule {
}); });
} }
container Ok(container)
} }
} }

View File

@@ -1,4 +1,5 @@
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use futures_util::StreamExt; use futures_util::StreamExt;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem}; use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
@@ -26,11 +27,12 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
item.icon_theme_path.as_ref().and_then(|path| { item.icon_theme_path.as_ref().and_then(|path| {
let theme = IconTheme::new(); let theme = IconTheme::new();
theme.append_search_path(&path); theme.append_search_path(&path);
let icon_name = item.icon_name.as_ref().unwrap();
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
}) })
})
} }
/// Recursively gets GTK `MenuItem` components /// Recursively gets GTK `MenuItem` components
@@ -38,8 +40,8 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
fn get_menu_items( fn get_menu_items(
menu: &[MenuItemInfo], menu: &[MenuItemInfo],
tx: &mpsc::Sender<NotifierItemCommand>, tx: &mpsc::Sender<NotifierItemCommand>,
id: String, id: &str,
path: String, path: &str,
) -> Vec<MenuItem> { ) -> Vec<MenuItem> {
menu.iter() menu.iter()
.map(|item_info| { .map(|item_info| {
@@ -53,7 +55,7 @@ fn get_menu_items(
if !item_info.submenu.is_empty() { if !item_info.submenu.is_empty() {
let menu = Menu::new(); let menu = Menu::new();
get_menu_items(&item_info.submenu, &tx.clone(), id.clone(), path.clone()) get_menu_items(&item_info.submenu, &tx.clone(), id, path)
.iter() .iter()
.for_each(|item| menu.add(item)); .for_each(|item| menu.add(item));
@@ -63,8 +65,8 @@ fn get_menu_items(
let item = builder.build(); let item = builder.build();
let info = item_info.clone(); let info = item_info.clone();
let id = id.clone(); let id = id.to_string();
let path = path.clone(); let path = path.to_string();
{ {
let tx = tx.clone(); let tx = tx.clone();
@@ -74,7 +76,7 @@ fn get_menu_items(
menu_path: path.clone(), menu_path: path.clone(),
notifier_address: id.clone(), notifier_address: id.clone(),
}) })
.unwrap(); .expect("Failed to send menu item clicked event");
}); });
} }
@@ -88,7 +90,7 @@ fn get_menu_items(
} }
impl Module<MenuBar> for TrayModule { impl Module<MenuBar> for TrayModule {
fn into_widget(self, _info: &ModuleInfo) -> MenuBar { fn into_widget(self, _info: &ModuleInfo) -> Result<MenuBar> {
let container = MenuBar::new(); let container = MenuBar::new();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@@ -107,10 +109,11 @@ impl Module<MenuBar> for TrayModule {
menu, menu,
} => { } => {
tx.send(TrayUpdate::Update(id, Box::new(item), menu)) tx.send(TrayUpdate::Update(id, Box::new(item), menu))
.unwrap(); .expect("Failed to send tray update event");
} }
NotifierItemMessage::Remove { address: id } => { NotifierItemMessage::Remove { address: id } => {
tx.send(TrayUpdate::Remove(id)).unwrap(); tx.send(TrayUpdate::Remove(id))
.expect("Failed to send tray remove event");
} }
} }
} }
@@ -138,13 +141,11 @@ impl Module<MenuBar> for TrayModule {
menu_item menu_item
}); });
if let Some(menu_opts) = menu { if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
let menu_path = item.menu.as_ref().unwrap().to_string();
let submenus = menu_opts.submenus; let submenus = menu_opts.submenus;
if !submenus.is_empty() { if !submenus.is_empty() {
let menu = Menu::new(); let menu = Menu::new();
get_menu_items(&submenus, &ui_tx.clone(), id.clone(), menu_path) get_menu_items(&submenus, &ui_tx.clone(), &id, &menu_path)
.iter() .iter()
.for_each(|item| menu.add(item)); .for_each(|item| menu.add(item));
menu_item.set_submenu(Some(&menu)); menu_item.set_submenu(Some(&menu));
@@ -154,15 +155,16 @@ impl Module<MenuBar> for TrayModule {
widgets.insert(id, menu_item); widgets.insert(id, menu_item);
} }
TrayUpdate::Remove(id) => { TrayUpdate::Remove(id) => {
let widget = widgets.get(&id).unwrap(); if let Some(widget) = widgets.get(&id) {
container.remove(widget); container.remove(widget);
} }
}
}; };
Continue(true) Continue(true)
}); });
}; };
container Ok(container)
} }
} }

View File

@@ -1,19 +1,22 @@
use crate::modules::{Module, ModuleInfo}; use crate::modules::{Module, ModuleInfo};
use crate::sway::{Workspace, WorkspaceEvent}; use crate::sway::{get_client, Workspace};
use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
use ksway::client::Client; use ksway::IpcCommand;
use ksway::{IpcCommand, IpcEvent};
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::{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,
} }
@@ -34,7 +37,10 @@ impl Workspace {
{ {
let tx = tx.clone(); let tx = tx.clone();
let name = self.name.clone(); let name = self.name.clone();
button.connect_clicked(move |_item| tx.try_send(name.clone()).unwrap()); button.connect_clicked(move |_item| {
tx.try_send(name.clone())
.expect("Failed to send workspace click event");
});
} }
button button
@@ -42,18 +48,20 @@ impl Workspace {
} }
impl Module<gtk::Box> for WorkspacesModule { impl Module<gtk::Box> for WorkspacesModule {
fn into_widget(self, info: &ModuleInfo) -> gtk::Box { fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let mut sway = Client::connect().unwrap();
let container = gtk::Box::new(Orientation::Horizontal, 0); let container = gtk::Box::new(Orientation::Horizontal, 0);
let workspaces = { let workspaces = {
let raw = sway.ipc(IpcCommand::GetWorkspaces).unwrap(); trace!("Getting current workspaces");
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw).unwrap(); 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 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)
@@ -67,41 +75,51 @@ 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]).unwrap();
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");
let payload: WorkspaceEvent = serde_json::from_slice(&payload).unwrap(); let srx = {
tx.send(payload).unwrap(); let sway = get_client();
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
sway.subscribe_workspace()
};
while let Ok(payload) = srx.recv() {
tx.send(payload).expect("Failed to send workspace event");
} }
sway.poll().unwrap();
}); });
{ {
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.unwrap(); let old = event.old.and_then(|old| button_map.get(&old.name));
if let Some(old_button) = button_map.get(&old.name) { if let Some(old) = old {
old_button.style_context().remove_class("focused"); old.style_context().remove_class("focused");
} }
let new = event.current.unwrap(); let new = event.current.and_then(|new| button_map.get(&new.name));
if let Some(new_button) = button_map.get(&new.name) { if let Some(new) = new {
new_button.style_context().add_class("focused"); new.style_context().add_class("focused");
} }
trace!("{:?} {:?}", old, new);
} }
"init" => { "init" => {
let workspace = event.current.unwrap(); if let Some(workspace) = event.current {
if self.all_monitors || workspace.output == output_name { if self.all_monitors || workspace.output == output_name {
let item = workspace.as_button(&name_map, &ui_tx); let item = workspace.as_button(&name_map, &ui_tx);
@@ -110,12 +128,29 @@ impl Module<gtk::Box> for WorkspacesModule {
button_map.insert(workspace.name, item); button_map.insert(workspace.name, item);
} }
} }
"empty" => { }
let current = event.current.unwrap(); "move" => {
if let Some(item) = button_map.get(&current.name) { 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); menubar.remove(item);
} }
} }
}
}
"empty" => {
if let Some(workspace) = event.current {
if let Some(item) = button_map.get(&workspace.name) {
menubar.remove(item);
}
}
}
_ => {} _ => {}
} }
@@ -124,12 +159,18 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
spawn(async move { spawn(async move {
let mut sway = Client::connect().unwrap(); 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 {
sway.run(format!("workspace {}", name)).unwrap(); let mut sway = sway
.lock()
.expect("Failed to get write lock on Sway IPC client");
sway.run(format!("workspace {}", name))?;
} }
Ok::<(), Report>(())
}); });
container Ok(container)
} }
} }

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,
@@ -107,9 +110,10 @@ impl Popup {
let screen_width = self.monitor.workarea().width(); let screen_width = self.monitor.workarea().width();
let popup_width = self.window.allocated_width(); let popup_width = self.window.allocated_width();
let top_level = button.toplevel().expect("Failed to get top-level widget");
let (widget_x, _) = button let (widget_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0) .translate_coordinates(&top_level, 0, 0)
.unwrap(); .unwrap_or((0, 0));
let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0; let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0;

View File

@@ -1,3 +1,4 @@
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};
@@ -6,40 +7,61 @@ use std::path::PathBuf;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::time::Duration;
use tokio::spawn; use tokio::spawn;
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();
provider
.load_from_file(&gio::File::for_path(&style_path)) if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
.expect("Couldn't load custom style"); error!("{:?}", Report::new(err)
StyleContext::add_provider_for_screen( .wrap_err("Failed to load CSS")
&gdk::Screen::default().expect("Couldn't get default GDK screen"), .suggestion("Check the CSS file for errors")
&provider, .suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
800,
); );
}
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(&screen, &provider, 800);
let (watcher_tx, watcher_rx) = mpsc::channel::<DebouncedEvent>(); 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 {
let mut watcher = notify::watcher(watcher_tx, Duration::from_millis(500)).unwrap(); match notify::watcher(watcher_tx, Duration::from_millis(500)) {
Ok(mut watcher) => {
watcher watcher
.watch(&style_path, RecursiveMode::NonRecursive) .watch(&style_path, RecursiveMode::NonRecursive)
.unwrap(); .expect("Unexpected error when attempting to watch CSS");
loop { loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() { if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).unwrap(); tx.send(path).expect("Failed to send style changed message");
} }
} }
}
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err("Failed to start CSS watcher")
),
}
}); });
{ {
rx.attach(None, move |path| { rx.attach(None, move |path| {
println!("Reloading CSS"); info!("Reloading CSS");
provider if let Err(err) = provider
.load_from_file(&gio::File::for_path(path)) .load_from_file(&gio::File::for_path(path)) {
.expect("Couldn't load custom style"); error!("{:?}", Report::new(err)
.wrap_err("Failed to load CSS")
.suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
);
}
Continue(true) Continue(true)
}); });

View File

@@ -1,15 +1,22 @@
use color_eyre::{Report, Result};
use crossbeam_channel::Receiver;
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,
@@ -17,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,
@@ -38,12 +45,180 @@ 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: String, pub class: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SwayOutput { pub struct SwayOutput {
pub name: String, pub name: String,
} }
type Broadcaster<T> = Arc<Mutex<UnboundedBroadcast<T>>>;
pub struct SwayClient {
client: ksway::Client,
workspace_bc: Broadcaster<WorkspaceEvent>,
window_bc: Broadcaster<WindowEvent>,
}
impl SwayClient {
fn connect() -> Result<Self> {
let client = match ksway::Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}?;
info!("Sway IPC client connected");
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>> {
debug!("Sending command: {:?}", command);
match self.client.ipc(command) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
debug!("Sending command: {}", cmd);
match self.client.run(cmd) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub fn subscribe_workspace(&mut self) -> Receiver<WorkspaceEvent> {
trace!("Adding new workspace subscriber");
self.workspace_bc
.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()
}
}
/// Gets an error report from a `ksway` error enum variant
pub fn get_client_error(error: Error) -> Report {
match error {
Error::SockPathNotFound => Report::msg("Sway socket path not found"),
Error::SubscriptionError => Report::msg("Sway IPC subscription error"),
Error::AlreadySubscribed => Report::msg("Already subscribed to Sway IPC server"),
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

@@ -1,25 +1,34 @@
use crate::sway::SwayNode; use crate::sway::{SwayClient, SwayNode};
use ksway::{Client, IpcCommand}; use color_eyre::Result;
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(
|| { || {
&self self.window_properties
.window_properties
.as_ref() .as_ref()
.expect("cannot find node name") .expect("Cannot find node window properties")
.class .class
.as_ref()
.expect("Cannot find node name")
}, },
|app_id| app_id, |app_id| app_id,
) )
} }
/// 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);
@@ -34,12 +43,15 @@ fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
} }
} }
pub fn get_open_windows(sway: &mut Client) -> Vec<SwayNode> { impl SwayClient {
let raw = sway.ipc(IpcCommand::GetTree).unwrap(); /// Gets a flat vector of all currently open windows.
let root_node = serde_json::from_slice::<SwayNode>(&raw).unwrap(); pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> {
let root_node = self.ipc(IpcCommand::GetTree)?;
let root_node = serde_json::from_slice(&root_node)?;
let mut window_nodes = vec![]; let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes); check_node(root_node, &mut window_nodes);
window_nodes Ok(window_nodes)
}
} }