Compare commits
194 Commits
v0.12.1
...
feat/works
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fad8c6a16 | ||
|
|
0c0163cfa1 | ||
|
|
e1b1d6f465 | ||
|
|
1f824edd70 | ||
|
|
088ac971c1 | ||
|
|
59f66df079 | ||
|
|
2d5d1b450c | ||
|
|
7a2edcdffc | ||
|
|
8f4ad02c75 | ||
|
|
c5a1694b10 | ||
|
|
f8cc634cc9 | ||
|
|
ccc6ff2d94 | ||
|
|
71ea185a65 | ||
|
|
c793cd3c1a | ||
|
|
1f4b349423 | ||
|
|
4755e2c827 | ||
|
|
b548e3ec01 | ||
|
|
7033d29c6a | ||
|
|
0335aa4470 | ||
|
|
dce245ef5f | ||
|
|
bd002e278b | ||
|
|
ae1bc2ef84 | ||
|
|
4e67b73a83 | ||
|
|
60bb69feec | ||
|
|
5bc7b7f317 | ||
|
|
f88bec93ff | ||
|
|
0d0447bbd2 | ||
|
|
49a79bb011 | ||
|
|
1dcda6c90e | ||
|
|
4e9c05761e | ||
|
|
af674bb769 | ||
|
|
a31a07e451 | ||
|
|
0df3d4c725 | ||
|
|
50ab2e4742 | ||
|
|
abd1f80548 | ||
|
|
3ddf799739 | ||
|
|
6fbe0d5e71 | ||
|
|
bc553b4918 | ||
|
|
4ee2ce4d67 | ||
|
|
cf3dc17ad3 | ||
|
|
8b8ccf7be7 | ||
|
|
1035ce670f | ||
|
|
43b446d266 | ||
|
|
93eb4c6472 | ||
|
|
40432c8704 | ||
|
|
9ced8597e4 | ||
|
|
10f8bbae3f | ||
|
|
af1f9e39b0 | ||
|
|
25c490b8b4 | ||
|
|
6c38ff29c4 | ||
|
|
fea1f18524 | ||
|
|
b9c41af0f7 | ||
|
|
2a8a62eea6 | ||
|
|
d010fd6398 | ||
|
|
57cca121bf | ||
|
|
7f6ba90bfd | ||
|
|
9431d09de7 | ||
|
|
69bd650810 | ||
|
|
5f9ac64892 | ||
|
|
3f7904fefb | ||
|
|
84e0065c0c | ||
|
|
e5281e9619 | ||
|
|
1b476eb9f9 | ||
|
|
50741941fb | ||
|
|
19c684e49f | ||
|
|
d1be6100d6 | ||
|
|
9f65cf293d | ||
|
|
7e877f6631 | ||
|
|
6203447ec5 | ||
|
|
3621414843 | ||
|
|
e75616c547 | ||
|
|
71002c4250 | ||
|
|
eb58d9caf8 | ||
|
|
6f9b61865d | ||
|
|
fec07c6407 | ||
|
|
8ec0237bc5 | ||
|
|
9e2ac0f43d | ||
|
|
baeb4eae74 | ||
|
|
b6e4ed6608 | ||
|
|
54f0f232f2 | ||
|
|
5255ddffbb | ||
|
|
9fe6d49195 | ||
|
|
b649525a2c | ||
|
|
fb2e30c839 | ||
|
|
82030c7d51 | ||
|
|
4679d8b842 | ||
|
|
901a86caa4 | ||
|
|
1da8c90415 | ||
|
|
9fc3c5302f | ||
|
|
22ab33d194 | ||
|
|
78ba508fb3 | ||
|
|
b6895b9312 | ||
|
|
c59d4267c8 | ||
|
|
2902331af0 | ||
|
|
a47ec69a07 | ||
|
|
c094179fca | ||
|
|
311284590f | ||
|
|
ff04be70cc | ||
|
|
89ec06fc7b | ||
|
|
7f6fef6338 | ||
|
|
36f3db7411 | ||
|
|
2367faab04 | ||
|
|
1c68a97d33 | ||
|
|
00d5606f06 | ||
|
|
ef443e6978 | ||
|
|
4a8c823590 | ||
|
|
1114f67b5d | ||
|
|
0d388d91f9 | ||
|
|
bf682d84ff | ||
|
|
2cd8be72a3 | ||
|
|
eb1347f20b | ||
|
|
ef446f084a | ||
|
|
4bb75282b4 | ||
|
|
5a6b14e5f8 | ||
|
|
806cfc61aa | ||
|
|
782fbf102c | ||
|
|
c823bcc83b | ||
|
|
0654f146a3 | ||
|
|
caecee6875 | ||
|
|
3853c953a6 | ||
|
|
f21d473b5a | ||
|
|
3d949874de | ||
|
|
c09bec2d2b | ||
|
|
6f57ad47ac | ||
|
|
87dd7646fc | ||
|
|
06251e293e | ||
|
|
36abe4073e | ||
|
|
b7ee794bfc | ||
|
|
c582bc3390 | ||
|
|
2a8313a9b4 | ||
|
|
2ccb2633c6 | ||
|
|
2f8443f349 | ||
|
|
fbb41f33a3 | ||
|
|
cc2669a3a6 | ||
|
|
bf470cdadd | ||
|
|
ab67a0a414 | ||
|
|
4305f3e3bc | ||
|
|
eee2182ab9 | ||
|
|
abf490b04e | ||
|
|
4ca17d1337 | ||
|
|
fc820746a4 | ||
|
|
738b9e3da7 | ||
|
|
1a272e00fb | ||
|
|
f8d8c06300 | ||
|
|
c711dd8585 | ||
|
|
68432b066b | ||
|
|
b310ea7636 | ||
|
|
7c8d4668bc | ||
|
|
f1231384c1 | ||
|
|
c2cf41a8b0 | ||
|
|
aa635291c3 | ||
|
|
5b2a1787ef | ||
|
|
95786a1c90 | ||
|
|
d8121de9c5 | ||
|
|
697aac1161 | ||
|
|
b695eeb415 | ||
|
|
f41afce91d | ||
|
|
c99c5de0d6 | ||
|
|
7f4a816379 | ||
|
|
b785b4d4ae | ||
|
|
3502c23817 | ||
|
|
7d3bb02b46 | ||
|
|
4620f29d38 | ||
|
|
a9ac29d885 | ||
|
|
6ae15f44bd | ||
|
|
1759945912 | ||
|
|
12053f111a | ||
|
|
bd90167f4e | ||
|
|
7016f7f79e | ||
|
|
ac04cc27ce | ||
|
|
f78c7f9b98 | ||
|
|
6db7742e06 | ||
|
|
4b88079561 | ||
|
|
9a68dc99bd | ||
|
|
cc181a8b6d | ||
|
|
27f920d012 | ||
|
|
4a9410abac | ||
|
|
607c7285d7 | ||
|
|
c6319b78fd | ||
|
|
ded50cca6f | ||
|
|
f5bdc5a027 | ||
|
|
44313bfc75 | ||
|
|
9e5f72087f | ||
|
|
449795b4e9 | ||
|
|
a67bf38faa | ||
|
|
e539eadd8d | ||
|
|
592213d8af | ||
|
|
d121dc3d1e | ||
|
|
0e8c8a1770 | ||
|
|
93baf8f568 | ||
|
|
1ef32059da | ||
|
|
5be0750792 | ||
|
|
aea8de2552 | ||
|
|
c3e9654cd3 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -22,6 +22,9 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
name: Cache dependencies
|
||||
|
||||
- name: Install build deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -70,4 +73,6 @@ jobs:
|
||||
name: jakestanger
|
||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- run: nix build --print-build-logs
|
||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -4,6 +4,93 @@ 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.13.0] - 2023-08-16
|
||||
### :sparkles: New Features
|
||||
- [`c3e9654`](https://github.com/JakeStanger/ironbar/commit/c3e9654cd3dfcea4276f5b114112b7541dd847fd) - **upower**: icon size option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f5bdc5a`](https://github.com/JakeStanger/ironbar/commit/f5bdc5a0272fefca4c91336699ea63913cdf3c08) - ipc server and cli *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ded50cc`](https://github.com/JakeStanger/ironbar/commit/ded50cca6f01f08a8e44257394fdde634d421e8e) - support for 'ironvar' dynamic variables *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c6319b7`](https://github.com/JakeStanger/ironbar/commit/c6319b78fd3992ad43327e90b6326ab653238f2e) - **ipc**: support for injecting additional stylesheets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27f920d`](https://github.com/JakeStanger/ironbar/commit/27f920d01217bedba279003291ad48c1aaa56bb0) - **launcher**: slightly improve focus logic when clicking item with multiple windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd90167`](https://github.com/JakeStanger/ironbar/commit/bd90167f4ea90cb97984b9f3b5e6f65b375d0c4a) - **clock**: format option for popup header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`12053f1`](https://github.com/JakeStanger/ironbar/commit/12053f111a6f05a59e33396b9042821b98b9bc5c) - **music**: progress/seek bar in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7d3bb02`](https://github.com/JakeStanger/ironbar/commit/7d3bb02b4612f278bcc8a268a48c61b239c63e82) - **ipc**: reload config command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b310ea7`](https://github.com/JakeStanger/ironbar/commit/b310ea76362bcdf10e187d6b00cd2b8ed2870c41) - **clock**: localization support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`738b9e3`](https://github.com/JakeStanger/ironbar/commit/738b9e3da714c9b998030e9f60b9b6f50c62ec76) - **config**: use default fallback with config instructions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2ccb263`](https://github.com/JakeStanger/ironbar/commit/2ccb2633c6c4d7f6eb264a6c49951853b728c9f3) - IPC for get_visible, set_visible, new bar `name` config option *(commit by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja))*
|
||||
- [`b7ee794`](https://github.com/JakeStanger/ironbar/commit/b7ee794bfc86730e7921c8a930cf8d8bb44474ad) - **ipc**: commands for opening/closing popups *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ef443e6`](https://github.com/JakeStanger/ironbar/commit/ef443e6978949479388129760dabc3f8930c0b0f) - **image resolver**: add fallback image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9f65cf2`](https://github.com/JakeStanger/ironbar/commit/9f65cf293d9527a2c536847f0005957421a9be33) - **workspaces**: add `favorites` and `hidden` options *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`19c684e`](https://github.com/JakeStanger/ironbar/commit/19c684e49facb57e3e2acf9cafecf177c2e1bfbf) - **nix**: automatic development environment with direnv *(commit by [@yavko](https://github.com/yavko))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`6db7742`](https://github.com/JakeStanger/ironbar/commit/6db7742e068dc03d67dbf35e0d9db27f900392fe) - crash on startup introduced by recent refactors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f78c7f9`](https://github.com/JakeStanger/ironbar/commit/f78c7f9b981c98676e855ff6a63e33a51927c709) - not resolving flatpak application icons *(commit by [@body20002](https://github.com/body20002))*
|
||||
- [`1759945`](https://github.com/JakeStanger/ironbar/commit/1759945912e376581e5fcd5ed2916f89e2090f2b) - **music**: correctly show/hide popup elements based on player capabilities *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a9ac29d`](https://github.com/JakeStanger/ironbar/commit/a9ac29d8857256d13e14104db235117e3c752972) - clipboard partially behind wrong feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c711dd8`](https://github.com/JakeStanger/ironbar/commit/c711dd858554140bcfb6df515a43b40ee2baee67) - failing to resolve icons with home_manager *(commit by [@christoph00](https://github.com/christoph00))*
|
||||
- [`1a272e0`](https://github.com/JakeStanger/ironbar/commit/1a272e00fbeca4b5e39b527ffed439bc51fd4080) - **label**: not using markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4ca17d1`](https://github.com/JakeStanger/ironbar/commit/4ca17d1337acfbbc21c04058d97f689a1cce60a6) - **launcher**: incorrectly resolving some applications *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`eee2182`](https://github.com/JakeStanger/ironbar/commit/eee2182ab90fdc22cd05da9417cbee17e4c67088) - **ipc**: command/response casing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c582bc3`](https://github.com/JakeStanger/ironbar/commit/c582bc33905702a9ebe323e6dfa9413485f48fb7) - **cli**: `set-visible` command causing panic *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`87dd764`](https://github.com/JakeStanger/ironbar/commit/87dd7646fc9223ac7e67842934f3bd104b4eea80) - **launcher**: not clearing focused state when closing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6f57ad4`](https://github.com/JakeStanger/ironbar/commit/6f57ad47ac30348c0ae2b7dba35d5ffdbf96f72d) - **launcher**: not setting focus state when opening favourite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2367faa`](https://github.com/JakeStanger/ironbar/commit/2367faab0440327620052de845c6a0d3032f2f05) - **image**: using fallback in places it shouldn't *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7f6fef6`](https://github.com/JakeStanger/ironbar/commit/7f6fef6338d7a8c909f3224b32426dfc2aacc295) - **image**: matching desktop file names too eagerly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`89ec06f`](https://github.com/JakeStanger/ironbar/commit/89ec06fc7b252052f96e45f5d0f6d6504878a13a) - **music**: hide album art widget when no image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2902331`](https://github.com/JakeStanger/ironbar/commit/2902331af00f2e52fdea06964fbd89d72fe2ebbb) - **dynamic string**: incorrectly handling strings containing multipoint utf-8 chars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`901a86c`](https://github.com/JakeStanger/ironbar/commit/901a86caa491648268f0618e17a25b978552db0c) - **custom**: crash when clicking non-popup button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`54f0f23`](https://github.com/JakeStanger/ironbar/commit/54f0f232f208602699e5021942c3d0f3947ca6de) - **launcher**: popup not closing when hover leaves widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`d121dc3`](https://github.com/JakeStanger/ironbar/commit/d121dc3d1e9468a67deb528c35fc3897c3840f77) - fix unused var warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cc181a8`](https://github.com/JakeStanger/ironbar/commit/cc181a8b6d0ac1cccd4ed2f9f420c138ed5383d2) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7016f7f`](https://github.com/JakeStanger/ironbar/commit/7016f7f79e7e29a3318b535ba224aa78bd91688a) - use new smart pointer macros throughout codebase *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`06251e2`](https://github.com/JakeStanger/ironbar/commit/06251e293e8f56e1897fed80335f114fdea57183) - fix new pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3d94987`](https://github.com/JakeStanger/ironbar/commit/3d949874de90b0e5c06cb62726629133d0ea76e3) - **ipc**: add link to luajit library *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.12.1] - 2023-06-18
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.
|
||||
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`31a57ae`](https://github.com/JakeStanger/ironbar/commit/31a57ae637fa5918f163c8b191916867395912f3) - scripts don't work while running ironbar under a systemd service *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f82f897`](https://github.com/JakeStanger/ironbar/commit/f82f897982e87906e2c9156d4115013bc8e99763) - **upower**: popup always empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9012fee`](https://github.com/JakeStanger/ironbar/commit/9012feee4f9b60b2c22a956de732847892331222) - **image**: still blurry on hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0e65f93`](https://github.com/JakeStanger/ironbar/commit/0e65f93a230cb5ab010b43962fd2e829945c291b) - excess popup windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`87ca399`](https://github.com/JakeStanger/ironbar/commit/87ca399220e5d48eefe2f295d1dba1b9452c4472) - poor error handling for missing images *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`22b630a`](https://github.com/JakeStanger/ironbar/commit/22b630a10b9836531a8b03eb904e6f9fcf839fe6) - broken nerd font icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`48d6af0`](https://github.com/JakeStanger/ironbar/commit/48d6af0281f460d3ed3745a2ffb2b61848430ecb) - **music**: showing when no mpris player found *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b9740cb`](https://github.com/JakeStanger/ironbar/commit/b9740cba8f2fa9dfa18a57345027283610f6487e) - upower icon too large *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a6b6866`](https://github.com/JakeStanger/ironbar/commit/a6b686624b750863aa1c26ca4f1688dfa8c81a61) - **upower**: icon outside button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a5ecb36`](https://github.com/JakeStanger/ironbar/commit/a5ecb363fdb2eb3ab543ad56c55c186414500469) - popups occasionally getting jumbled with multiple bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ac34c05`](https://github.com/JakeStanger/ironbar/commit/ac34c05d2ecb07fd871ed03ef6ee545dc2e6743d) - **focused**: empty icon rendered when `show_icon = false` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de3aa5d`](https://github.com/JakeStanger/ironbar/commit/de3aa5d7b10e0bf6d5ff3a39b009ff53a3316a5e) - **focused**: previous icon does not clear if new icon fails to load *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de98cf3`](https://github.com/JakeStanger/ironbar/commit/de98cf3daee816a0ff72d1f6ba6bc0e15ec53fca) - **tray**: (maybe?) sometimes bus name is taken *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ff315ff`](https://github.com/JakeStanger/ironbar/commit/ff315ff5dbd545d8b72b6aa10087c940cb8a5eee) - **music**: fix incorrect type for `host`/`music_dir` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd144e8`](https://github.com/JakeStanger/ironbar/commit/bd144e87a8f6668c877d42697ebbedbe5a374c3d) - **readme**: make prettier *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`242b70e`](https://github.com/JakeStanger/ironbar/commit/242b70ed3988b85455b0dbbcb3243b31f89d2ee1) - **contributing**: enforce conventional commits *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`96d36c4`](https://github.com/JakeStanger/ironbar/commit/96d36c43d43ba2f9e9d9441ae01c0743cc56f627) - add missing icon/image selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.12.0] - 2023-05-06
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
@@ -337,3 +424,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
||||
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
|
||||
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
|
||||
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
|
||||
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0
|
||||
980
Cargo.lock
generated
980
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
@@ -1,12 +1,17 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.12.1"
|
||||
version = "0.14.0-pre"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||
repository = "https://github.com/jakestanger/ironbar"
|
||||
categories = ["gui"]
|
||||
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"cli",
|
||||
"ipc",
|
||||
"http",
|
||||
"config+all",
|
||||
"clipboard",
|
||||
@@ -17,10 +22,19 @@ default = [
|
||||
"upower",
|
||||
"workspaces+all"
|
||||
]
|
||||
http = ["dep:reqwest"]
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
|
||||
cli = ["dep:clap", "ipc"]
|
||||
ipc = ["dep:serde_json"]
|
||||
|
||||
http = ["dep:reqwest"]
|
||||
|
||||
"config+all" = [
|
||||
"config+json",
|
||||
"config+yaml",
|
||||
"config+toml",
|
||||
"config+corn",
|
||||
"config+ron",
|
||||
]
|
||||
"config+json" = ["universal-config/json"]
|
||||
"config+yaml" = ["universal-config/yaml"]
|
||||
"config+toml" = ["universal-config/toml"]
|
||||
@@ -38,7 +52,9 @@ music = ["regex"]
|
||||
|
||||
sys_info = ["sysinfo", "regex"]
|
||||
|
||||
tray = ["stray"]
|
||||
tray = ["system-tray"]
|
||||
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
workspaces = ["futures-util"]
|
||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||
@@ -50,59 +66,75 @@ workspaces = ["futures-util"]
|
||||
gtk = "0.17.0"
|
||||
gtk-layer-shell = "0.6.0"
|
||||
glib = "0.17.10"
|
||||
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
"process",
|
||||
"sync",
|
||||
"io-util",
|
||||
"net",
|
||||
] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-appender = "0.2.2"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
color-eyre = "0.6.2"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
indexmap = "1.9.1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
indexmap = "2.0.0"
|
||||
dirs = "5.0.1"
|
||||
walkdir = "2.3.2"
|
||||
notify = { version = "6.0.0", default-features = false }
|
||||
walkdir = "2.4.0"
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
wayland-client = "0.30.2"
|
||||
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
|
||||
wayland-protocols = { version = "0.30.1", features = ["unstable", "client"] }
|
||||
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] }
|
||||
universal-config = { version = "0.4.0", default_features = false }
|
||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
|
||||
"calloop",
|
||||
] }
|
||||
universal-config = { version = "0.4.3", default_features = false }
|
||||
ctrlc = "3.4.1"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
# cli
|
||||
clap = { version = "4.4.4", optional = true, features = ["derive"] }
|
||||
|
||||
# ipc
|
||||
serde_json = { version = "1.0.107", optional = true }
|
||||
|
||||
# http
|
||||
reqwest = { version = "0.11.18", optional = true }
|
||||
reqwest = { version = "0.11.20", optional = true }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.26.2", optional = true, features = ["event"] }
|
||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.26", optional = true }
|
||||
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
|
||||
|
||||
# music
|
||||
mpd_client = { version = "1.0.0", optional = true }
|
||||
mpris = { version = "2.0.0", optional = true }
|
||||
mpd_client = { version = "1.2.0", optional = true }
|
||||
mpris = { version = "2.0.1", optional = true }
|
||||
|
||||
# sys_info
|
||||
sysinfo = { version = "0.29.2", optional = true }
|
||||
sysinfo = { version = "0.29.10", optional = true }
|
||||
|
||||
# tray
|
||||
stray = { version = "0.1.3", optional = true }
|
||||
system-tray = { version = "0.1.4", optional = true }
|
||||
|
||||
# upower
|
||||
upower_dbus = { version = "0.3.2", optional = true }
|
||||
futures-lite = { version = "1.12.0", optional = true }
|
||||
zbus = { version = "3.13.1", optional = true }
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "=0.3.1", optional = true }
|
||||
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
|
||||
futures-util = { version = "0.3.21", optional = true }
|
||||
|
||||
# shared
|
||||
regex = { version = "1.8.4", default-features = false, features = ["std"], optional = true } # music, sys_info
|
||||
|
||||
[patch.crates-io]
|
||||
stray = { git = "https://github.com/jakestanger/stray", branch = "fix/connection-errors" }
|
||||
regex = { version = "1.9.5", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true } # music, sys_info
|
||||
10
README.md
10
README.md
@@ -75,7 +75,15 @@ cargo install ironbar
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
### Nix Flake
|
||||
### Nix
|
||||
|
||||
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
|
||||
|
||||
```sh
|
||||
nix-shell -p ironbar
|
||||
```
|
||||
|
||||
#### Flake
|
||||
|
||||
A flake is included with the repo which can be used with Home Manager.
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ cargo build --release --no-default-features \
|
||||
|---------------------|-----------------------------------------------------------------------------------|
|
||||
| **Core** | |
|
||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||
| ipc | Enables the IPC server. |
|
||||
| cli | Enables the CLI. Will also enable `ipc`. |
|
||||
| config+all | Enables support for all configuration languages. |
|
||||
| config+json | Enables configuration support for JSON. |
|
||||
| config+yaml | Enables configuration support for YAML. |
|
||||
|
||||
@@ -268,7 +268,8 @@ Check [here](config) for an example config file for a fully configured bar in ea
|
||||
The following table lists each of the top-level bar config options:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
||||
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
|
||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
@@ -278,6 +279,7 @@ The following table lists each of the top-level bar config options:
|
||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
@@ -306,9 +308,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||
| `transition_duration` | `Integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||
|
||||
#### Appearance
|
||||
|
||||
|
||||
224
docs/Controlling Ironbar.md
Normal file
224
docs/Controlling Ironbar.md
Normal file
@@ -0,0 +1,224 @@
|
||||
Ironbar includes a simple IPC server which can be used to control it programmatically at runtime.
|
||||
|
||||
It also includes a command line interface, which can be used for interacting with the IPC server.
|
||||
|
||||
# CLI
|
||||
|
||||
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
|
||||
You can also view help per-command, for example using `ironbar set --help`.
|
||||
|
||||
Responses are handled by writing their type to stdout, followed by any value starting on the next line.
|
||||
Error responses are written to stderr in the same format.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
$ ironbar set subject world
|
||||
ok
|
||||
|
||||
$ ironbar get subject
|
||||
ok
|
||||
world
|
||||
```
|
||||
|
||||
# IPC
|
||||
|
||||
The server listens on a Unix socket.
|
||||
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
|
||||
|
||||
Commands and responses are sent as JSON objects, denoted by their `type` key.
|
||||
|
||||
The message buffer is currently limited to `1024` bytes.
|
||||
Particularly large messages will be truncated or cause an error.
|
||||
|
||||
The full spec can be found below.
|
||||
|
||||
## Libraries
|
||||
|
||||
- [Luajit](https://github.com/A-Cloud-Ninja/ironbar-ipc-luajit) - Maintained by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja)
|
||||
|
||||
## Commands
|
||||
|
||||
### `ping`
|
||||
|
||||
Sends a ping request to the IPC.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ping"
|
||||
}
|
||||
```
|
||||
|
||||
### `inspect`
|
||||
|
||||
Opens the GTK inspector window.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "inspect"
|
||||
}
|
||||
```
|
||||
|
||||
### `reload`
|
||||
|
||||
Restarts the bars, reloading the config in the process.
|
||||
|
||||
The IPC server and main GTK application are untouched.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "reload"
|
||||
}
|
||||
```
|
||||
|
||||
### `get`
|
||||
|
||||
Gets an [ironvar](ironvars) value.
|
||||
|
||||
Responds with `ok_value` if the value exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "get",
|
||||
"key": "foo"
|
||||
}
|
||||
```
|
||||
|
||||
### `set`
|
||||
|
||||
Sets an [ironvar](ironvars) value.
|
||||
|
||||
Responds with `ok`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set",
|
||||
"key": "foo",
|
||||
"value": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
### `load_css`
|
||||
|
||||
Loads an additional CSS stylesheet, with hot-reloading enabled.
|
||||
|
||||
Responds with `ok` if the stylesheet exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "load_css",
|
||||
"path": "/path/to/style.css"
|
||||
}
|
||||
```
|
||||
|
||||
### `set_visible`
|
||||
|
||||
Sets a bar's visibility.
|
||||
|
||||
Responds with `ok` if the bar exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set_visible",
|
||||
"bar_name": "bar-123",
|
||||
"visible": true
|
||||
}
|
||||
```
|
||||
|
||||
### `get_visible`
|
||||
|
||||
Gets a bar's visibility.
|
||||
|
||||
Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "get_visible",
|
||||
"bar_name": "bar-123"
|
||||
}
|
||||
```
|
||||
|
||||
### `toggle_popup`
|
||||
|
||||
Toggles the open/closed state for a module's popup.
|
||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "toggle_popup",
|
||||
"bar_name": "bar-123",
|
||||
"name": "clock"
|
||||
}
|
||||
```
|
||||
|
||||
### `open_popup`
|
||||
|
||||
Sets a module's popup open, regardless of its current state.
|
||||
Since each bar only has a single popup, any open popup on the bar is closed.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "open_popup",
|
||||
"bar_name": "bar-123",
|
||||
"name": "clock"
|
||||
}
|
||||
```
|
||||
|
||||
### `close_popup`
|
||||
|
||||
Sets the popup on a bar closed, regardless of which module it is open for.
|
||||
|
||||
Responds with `ok` if the popup exists, otherwise `error`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "close_popup",
|
||||
"bar_name": "bar-123"
|
||||
}
|
||||
```
|
||||
|
||||
## Responses
|
||||
|
||||
### `ok`
|
||||
|
||||
The operation completed successfully, with no response data.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### `ok_value`
|
||||
|
||||
The operation completed successfully, with response data.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ok_value",
|
||||
"value": "lorem ipsum"
|
||||
}
|
||||
```
|
||||
|
||||
### `error`
|
||||
|
||||
The operation failed.
|
||||
|
||||
Message is optional.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"message": "lorem ipsum"
|
||||
}
|
||||
```
|
||||
39
docs/Dynamic values.md
Normal file
39
docs/Dynamic values.md
Normal file
@@ -0,0 +1,39 @@
|
||||
In some configuration locations, Ironbar supports dynamic values,
|
||||
meaning you can inject content into the bar from an external source.
|
||||
|
||||
Currently two dynamic content sources are supported - scripts and ironvars.
|
||||
|
||||
## Dynamic String
|
||||
|
||||
Dynamic strings can contain any mixture of static string elements, scripts and variables.
|
||||
|
||||
Scripts should be placed inside `{{double braces}}`. Both polling and watching scripts are supported.
|
||||
|
||||
Variables use the standard `#name` syntax. Variables cannot be placed inside scripts.
|
||||
|
||||
To use a literal hash, use `##`. This is only necessary outside of scripts.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
label = "{{cat greeting.txt}}, #subject"
|
||||
```
|
||||
|
||||
## Dynamic Boolean
|
||||
|
||||
Dynamic booleans can use a single source of either a script or variable to control a true/false value.
|
||||
|
||||
For scripts, you can just write these directly with no notation.
|
||||
Only polling scripts are supported.
|
||||
The script exit code is used, where `0` is `true` and any other code is `false.
|
||||
|
||||
For variables, use the standard `#name` notation.
|
||||
An empty string, `0` and `false` are treated as false.
|
||||
Any other value is true.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
show_if = "exit 0" # script
|
||||
show_if = "#show_module" # variable
|
||||
```
|
||||
9
docs/Ironvars.md
Normal file
9
docs/Ironvars.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Ironvars are runtime variables that can be referenced in several places in your config,
|
||||
then set using the IPC server (such as via the CLI) using the `set` command.
|
||||
|
||||
Any UTF-8 string *without whitespace* is a valid key.
|
||||
Any UTF-8 string is a valid value.
|
||||
|
||||
Reference values using `#my_variable`. These update as soon as the value changes.
|
||||
|
||||
You can set defaults using the `ironvar_defaults` key in your top-level config.
|
||||
@@ -11,13 +11,15 @@ The below table describes the selectors provided by the bar itself.
|
||||
Information on styling individual modules can be found on their pages in the sidebar.
|
||||
|
||||
| Selector | Description |
|
||||
|----------------|--------------------------------------------|
|
||||
|---------------------|--------------------------------------------|
|
||||
| `.background` | Top-level window. |
|
||||
| `#bar` | Bar root box. |
|
||||
| `#bar #start` | Bar left or top modules container box. |
|
||||
| `#bar #center` | Bar center modules container box. |
|
||||
| `#bar #end` | Bar right or bottom modules container box. |
|
||||
| `.container` | All of the above. |
|
||||
| `.widget-container` | The `EventBox` wrapping any widget. |
|
||||
| `.widget` | Any widget. |
|
||||
| `.popup` | Any popup box. |
|
||||
|
||||
Every widget can be selected using a `kebab-case` class name matching its name.
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
- [Compiling from source](compiling)
|
||||
- [Configuration guide](configuration-guide)
|
||||
- [Scripts](scripts)
|
||||
- [Images](images)
|
||||
- [Styling guide](styling-guide)
|
||||
|
||||
# Dynamic content
|
||||
|
||||
- [Controlling Ironbar](controlling-ironbar)
|
||||
- [Dynamic values](dynamic-values)
|
||||
- [Scripts](scripts)
|
||||
- [Ironvars](ironvars)
|
||||
|
||||
# Examples
|
||||
|
||||
- [Config](config)
|
||||
|
||||
@@ -10,17 +10,15 @@ Supports plain text and images.
|
||||
> Type: `clipboard`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `icon` | `string/image` | `` | Icon to show on the widget button. |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
See [here](images) for information on images.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@ Clicking on the widget opens a popup with the time and a calendar.
|
||||
> Type: `clock`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
|
||||
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. |
|
||||
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. |
|
||||
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
|
||||
|
||||
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
||||
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||
|
||||
If you only intend to run a single script, prefer the [script](script) module,
|
||||
or [label](label) if you only need a single text label.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
@@ -19,8 +22,8 @@ Every widget has the following options available; `type` is mandatory.
|
||||
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
||||
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
||||
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
|
||||
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
|
||||
| `name` | `string` | `null` | Widget name. |
|
||||
| `class` | `string` | `null` | Widget class name. |
|
||||
|
||||
@@ -31,19 +34,19 @@ A container to place nested widgets inside.
|
||||
> Type: `box`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------|
|
||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. |
|
||||
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
|
||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||
|
||||
#### Label
|
||||
|
||||
A text label. Pango markup and embedded scripts are supported.
|
||||
A text label. Pango markup is supported.
|
||||
|
||||
> Type `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|----------|--------------|---------------------------------------------------------------------|
|
||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
|
||||
#### Button
|
||||
|
||||
@@ -52,8 +55,8 @@ A clickable button, which can run a command when clicked.
|
||||
> Type `button`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------------|--------------------|--------------|---------------------------------------------------------------------|
|
||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||
|
||||
#### Image
|
||||
@@ -63,8 +66,8 @@ An image or icon from disk or http.
|
||||
> Type `image`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------|-----------|---------|---------------------------------------------------------------------------------------------|
|
||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
|
||||
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
||||
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
|
||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||
|
||||
#### Slider
|
||||
@@ -77,10 +80,8 @@ Note that `on_change` will provide the **floating point** value as an argument.
|
||||
If your input program requires an integer, you will need to round it.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
||||
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
|
||||
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
||||
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
||||
| `min` | `float` | `0` | Minimum slider value. |
|
||||
@@ -116,10 +117,8 @@ A progress bar.
|
||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
||||
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
|
||||
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
||||
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||
|
||||
@@ -8,12 +8,12 @@ Displays the title and/or icon of the currently focused window.
|
||||
> Type: `focused`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
||||
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
||||
| `icon_size` | `integer` | `32` | Size of icon in pixels |
|
||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
||||
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
||||
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
|
||||
Displays custom text, with markup support.
|
||||
|
||||
If you only intend to run a single script, prefer the [script](script) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|----------|---------|-----------------------------------------|
|
||||
| `label` | `string` | `null` | Text, optionally with embedded scripts. |
|
||||
|---------|-------------------------------------------------|---------|------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
@@ -12,21 +12,21 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
||||
> Type: `music`
|
||||
|
||||
| | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
| `icons.play` | `string/image` | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
||||
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
||||
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
|
||||
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
|
||||
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
|
||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||
@@ -128,8 +128,6 @@ and will be replaced with values from the currently playing track:
|
||||
| `{track}` | Track number |
|
||||
| `{disc}` | Disc number |
|
||||
| `{genre}` | Genre |
|
||||
| `{duration}` | Duration in `mm:ss` |
|
||||
| `{elapsed}` | Time elapsed in `mm:ss` |
|
||||
|
||||
## Styling
|
||||
|
||||
@@ -166,7 +164,10 @@ and will be replaced with values from the currently playing track:
|
||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
||||
| `.popup-music .volume` | Volume container inside popup box |
|
||||
| `.popup-music .volume .slider` | Volume slider popup box |
|
||||
| `.popup-music .volume .icon` | Volume icon label inside popup box |
|
||||
| `.popup-music .volume .slider` | Slider inside volume container |
|
||||
| `.popup-music .volume .icon` | Icon inside volume container |
|
||||
| `.popup-music .progress` | Progress (seek) bar container |
|
||||
| `.popup-music .progress .slider` | Slider inside progress container |
|
||||
| `.popup-music .progress .label` | Duration label inside progress container |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -1,6 +1,9 @@
|
||||
Executes a script and shows the result of `stdout` on a label.
|
||||
Pango markup is supported.
|
||||
|
||||
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `script`
|
||||
|
||||
@@ -10,8 +10,9 @@ Displays system power information such as the battery percentage, and estimated
|
||||
> Type: `upower`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------|----------|-----------------|---------------------------------------------------|
|
||||
|-------------|-----------|-----------------|---------------------------------------------------|
|
||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
@@ -9,11 +9,13 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
> Type: `workspaces`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
|
||||
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -28,6 +30,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
"2": "",
|
||||
"3": ""
|
||||
},
|
||||
"favorites": ["1", "2", "3"],
|
||||
"all_monitors": false
|
||||
}
|
||||
]
|
||||
@@ -43,6 +46,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
[[end]]
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
favorites = ["1", "2", "3"]
|
||||
|
||||
[[end.name_map]]
|
||||
1 = ""
|
||||
@@ -63,6 +67,10 @@ end:
|
||||
1: ""
|
||||
2: ""
|
||||
3: ""
|
||||
favorites:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
all_monitors: false
|
||||
```
|
||||
|
||||
@@ -79,6 +87,7 @@ end:
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
favorites = [ "1" "2" "3" ]
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
@@ -94,6 +103,8 @@ end:
|
||||
| `.workspaces` | Workspaces widget box |
|
||||
| `.workspaces .item` | Workspace button |
|
||||
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
||||
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
|
||||
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
|
||||
| `.workspaces .item .icon` | Workspace button icon (any type) |
|
||||
| `.workspaces .item .text-icon` | Workspace button icon (textual only) |
|
||||
| `.workspaces .item .image` | Workspace button icon (image only) |
|
||||
|
||||
@@ -3,7 +3,7 @@ let {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Games = "icon:steam"
|
||||
@@ -67,7 +67,7 @@ let {
|
||||
|
||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||
|
||||
$label = { type = "label" label = "random num: {{500:echo $RANDOM}}" }
|
||||
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
|
||||
|
||||
// -- begin custom --
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "ﭮ",
|
||||
"1": "",
|
||||
"2": "icon:firefox",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
@@ -128,7 +128,7 @@
|
||||
"type": "launcher"
|
||||
},
|
||||
{
|
||||
"label": "random num: {{500:echo $RANDOM}}",
|
||||
"label": "random num: {{500:echo FIXME}}",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
anchor_to_edges = true
|
||||
icon_theme = 'Paper'
|
||||
position = 'bottom'
|
||||
icon_theme = "Paper"
|
||||
position = "bottom"
|
||||
|
||||
[[end]]
|
||||
music_dir = '/home/jake/Music'
|
||||
player_type = 'mpd'
|
||||
type = 'music'
|
||||
music_dir = "/home/jake/Music"
|
||||
player_type = "mpd"
|
||||
type = "music"
|
||||
|
||||
[end.truncate]
|
||||
max_length = 100
|
||||
mode = 'end'
|
||||
mode = "end"
|
||||
|
||||
[[end]]
|
||||
host = 'chloe:6600'
|
||||
player_type = 'mpd'
|
||||
truncate = 'end'
|
||||
type = 'music'
|
||||
host = "chloe:6600"
|
||||
player_type = "mpd"
|
||||
truncate = "end"
|
||||
type = "music"
|
||||
|
||||
[[end]]
|
||||
cmd = '/home/jake/bin/phone-battery'
|
||||
type = 'script'
|
||||
cmd = "/home/jake/bin/phone-battery"
|
||||
type = "script"
|
||||
|
||||
[end.show_if]
|
||||
cmd = '/home/jake/bin/phone-connected'
|
||||
cmd = "/home/jake/bin/phone-connected"
|
||||
interval = 500
|
||||
|
||||
[[end]]
|
||||
type = 'sys_info'
|
||||
format = [
|
||||
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}",
|
||||
]
|
||||
type = "sys_info"
|
||||
|
||||
[end.interval]
|
||||
cpu = 1
|
||||
@@ -46,77 +46,77 @@ temps = 5
|
||||
|
||||
[[end]]
|
||||
max_items = 3
|
||||
type = 'clipboard'
|
||||
type = "clipboard"
|
||||
|
||||
[end.truncate]
|
||||
length = 50
|
||||
mode = 'end'
|
||||
mode = "end"
|
||||
|
||||
[[end]]
|
||||
class = 'power-menu'
|
||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
type = 'custom'
|
||||
class = "power-menu"
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
type = "custom"
|
||||
|
||||
[[end.bar]]
|
||||
label = ''
|
||||
name = 'power-btn'
|
||||
on_click = 'popup:toggle'
|
||||
type = 'button'
|
||||
label = ""
|
||||
name = "power-btn"
|
||||
on_click = "popup:toggle"
|
||||
type = "button"
|
||||
|
||||
[[end.popup]]
|
||||
orientation = 'vertical'
|
||||
type = 'box'
|
||||
orientation = "vertical"
|
||||
type = "box"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = 'Power menu'
|
||||
name = 'header'
|
||||
type = 'label'
|
||||
label = "Power menu"
|
||||
name = "header"
|
||||
type = "label"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
type = 'box'
|
||||
type = "box"
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!shutdown now'
|
||||
type = 'button'
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!shutdown now"
|
||||
type = "button"
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!reboot'
|
||||
type = 'button'
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!reboot"
|
||||
type = "button"
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
name = 'uptime'
|
||||
type = 'label'
|
||||
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
name = "uptime"
|
||||
type = "label"
|
||||
|
||||
[[end]]
|
||||
type = 'clock'
|
||||
type = "clock"
|
||||
|
||||
[[start]]
|
||||
all_monitors = false
|
||||
type = 'workspaces'
|
||||
type = "workspaces"
|
||||
|
||||
[start.name_map]
|
||||
1 = 'ﭮ'
|
||||
2 = 'icon:firefox'
|
||||
3 = ''
|
||||
Code = ''
|
||||
Games = 'icon:steam'
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Code = ""
|
||||
Games = "icon:steam"
|
||||
|
||||
[[start]]
|
||||
favorites = [
|
||||
"firefox",
|
||||
"discord",
|
||||
"steam",
|
||||
]
|
||||
show_icons = true
|
||||
show_names = false
|
||||
type = 'launcher'
|
||||
favorites = [
|
||||
'firefox',
|
||||
'discord',
|
||||
'steam',
|
||||
]
|
||||
type = "launcher"
|
||||
|
||||
[[start]]
|
||||
label = 'random num: {{500:echo $RANDOM}}'
|
||||
type = 'label'
|
||||
label = "random num: {{500:echo FIXME}}"
|
||||
type = "label"
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ position: bottom
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1': ﭮ
|
||||
'1':
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
@@ -82,6 +82,6 @@ start:
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo $RANDOM}}'
|
||||
- label: 'random num: {{500:echo FIXME}}'
|
||||
type: label
|
||||
|
||||
|
||||
@@ -120,7 +120,11 @@ button:hover {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.popup-music .title .icon *, .popup-music .title .label {
|
||||
.popup-music .icon-box {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.popup-music .title .icon, .popup-music .title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
@@ -128,15 +132,17 @@ button:hover {
|
||||
color: @color_border;
|
||||
}
|
||||
|
||||
.popup-music .volume scale slider {
|
||||
.popup-music .volume .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* volume icon */
|
||||
.popup-music .volume > box:last-child label {
|
||||
margin-left: 6px;
|
||||
.popup-music .volume .icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.popup-music .progress .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* -- script -- */
|
||||
|
||||
|
||||
149
flake.lock
generated
149
flake.lock
generated
@@ -1,9 +1,66 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1693439040,
|
||||
"narHash": "sha256-t2nOxBcP0Q/XJt6Ild4v0hJ49OSl9F3nE1cdIT4xsDg=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "174604795d316b75777e28185c3a4918bc69b399",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
@@ -18,13 +75,45 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692351612,
|
||||
"narHash": "sha256-KTGonidcdaLadRnv9KFgwSMh1ZbXoR/OBmPjeNMhFwU=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "78789c30d64dea2396c9da516bbcc8db3a475207",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1686960236,
|
||||
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
|
||||
"lastModified": 1693355128,
|
||||
"narHash": "sha256-+ZoAny3ZxLcfMaUoLVgL9Ywb/57wP+EtsdNGuXUJrwg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a63a64b593dcf2fe05f7c5d666eb395950f36bc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1693377291,
|
||||
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
|
||||
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -36,23 +125,50 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
"crane": "crane",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-utils": [
|
||||
"crane",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"crane",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1691374719,
|
||||
"narHash": "sha256-HCodqnx1Mi2vN4f3hjRPc7+lSQy18vRn8xWW68GeQOg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b520a3889b24aaf909e287d19d406862ced9ffc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay_2": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1686968542,
|
||||
"narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
|
||||
"lastModified": 1693447852,
|
||||
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
|
||||
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -75,6 +191,21 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
49
flake.nix
49
flake.nix
@@ -6,11 +6,18 @@
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
crane,
|
||||
naersk,
|
||||
...
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
@@ -27,10 +34,18 @@
|
||||
rust-overlay.overlays.default
|
||||
];
|
||||
};
|
||||
mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
in {
|
||||
overlays.default = final: prev: let
|
||||
rust = mkRustToolchain final;
|
||||
craneLib = (crane.mkLib final).overrideToolchain rust;
|
||||
naersk' = prev.callPackage naersk {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
|
||||
rustPlatform = prev.makeRustPlatform {
|
||||
cargo = rust;
|
||||
@@ -42,10 +57,32 @@
|
||||
(builtins.substring 4 2 longDate)
|
||||
(builtins.substring 6 2 longDate)
|
||||
]);
|
||||
builder = "naersk";
|
||||
in {
|
||||
ironbar = prev.callPackage ./nix/default.nix {
|
||||
ironbar = let
|
||||
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
||||
in
|
||||
if builder == "crane"
|
||||
then
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
builder = craneLib;
|
||||
}
|
||||
else if builder == "naersk"
|
||||
then
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
builder = naersk';
|
||||
}
|
||||
else
|
||||
prev.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
inherit rustPlatform;
|
||||
builderName = builder;
|
||||
};
|
||||
};
|
||||
packages = genSystems (
|
||||
@@ -82,6 +119,14 @@
|
||||
gtk-layer-shell
|
||||
pkg-config
|
||||
openssl
|
||||
gdk-pixbuf
|
||||
glib
|
||||
glib-networking
|
||||
shared-mime-info
|
||||
gnome.adwaita-icon-theme
|
||||
hicolor-icon-theme
|
||||
gsettings-desktop-schemas
|
||||
libxkbcommon
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
|
||||
@@ -19,24 +19,16 @@
|
||||
lib,
|
||||
version ? "git",
|
||||
features ? [],
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
builderName ? "nix",
|
||||
builder ? {},
|
||||
}: let
|
||||
basePkg = rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {
|
||||
lockFile = ../Cargo.lock;
|
||||
};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||
propagatedBuildInputs = [
|
||||
@@ -64,4 +56,40 @@ rustPlatform.buildRustPackage rec {
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
};
|
||||
}
|
||||
};
|
||||
flags = let
|
||||
noDefault =
|
||||
if features == []
|
||||
then ""
|
||||
else "--no-default-features";
|
||||
featuresStr =
|
||||
if features == []
|
||||
then ""
|
||||
else ''-F "${builtins.concatStringsSep "," features}"'';
|
||||
in [noDefault featuresStr];
|
||||
in
|
||||
if builderName == "naersk"
|
||||
then
|
||||
builder.buildPackage (basePkg
|
||||
// {
|
||||
cargoOptions = old: old ++ flags;
|
||||
})
|
||||
else if builderName == "crane"
|
||||
then
|
||||
builder.buildPackage (basePkg
|
||||
// {
|
||||
cargoExtraArgs = builtins.concatStringsSep " " flags;
|
||||
doCheck = false;
|
||||
})
|
||||
else
|
||||
rustPlatform.buildRustPackage (basePkg
|
||||
// {
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||
})
|
||||
|
||||
17
shell.nix
Normal file
17
shell.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustfmt
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
gcc
|
||||
openssl
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
];
|
||||
}
|
||||
47
src/bar.rs
47
src/bar.rs
@@ -4,12 +4,13 @@ use crate::modules::{
|
||||
};
|
||||
use crate::popup::Popup;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::Config;
|
||||
use crate::{Config, GlobalState};
|
||||
use color_eyre::Result;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Creates a new window for a bar,
|
||||
@@ -19,8 +20,16 @@ pub fn create_bar(
|
||||
monitor: &Monitor,
|
||||
monitor_name: &str,
|
||||
config: Config,
|
||||
global_state: &Rc<RefCell<GlobalState>>,
|
||||
) -> Result<()> {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
let bar_name = config
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("bar-{}", get_unique_usize()));
|
||||
|
||||
win.set_widget_name(&bar_name);
|
||||
info!("Creating bar {}", bar_name);
|
||||
|
||||
setup_layer_shell(
|
||||
&win,
|
||||
@@ -55,7 +64,12 @@ pub fn create_bar(
|
||||
content.set_center_widget(Some(¢er));
|
||||
content.pack_end(&end, false, false, 0);
|
||||
|
||||
load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||
let load_result = load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||
global_state
|
||||
.borrow_mut()
|
||||
.popups_mut()
|
||||
.insert(bar_name.into(), load_result.popup);
|
||||
|
||||
win.add(&content);
|
||||
|
||||
win.connect_destroy_event(|_, _| {
|
||||
@@ -136,6 +150,11 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
|
||||
container
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BarLoadResult {
|
||||
popup: Rc<RefCell<Popup>>,
|
||||
}
|
||||
|
||||
/// Loads the configured modules onto a bar.
|
||||
fn load_modules(
|
||||
left: >k::Box,
|
||||
@@ -145,7 +164,7 @@ fn load_modules(
|
||||
config: Config,
|
||||
monitor: &Monitor,
|
||||
output_name: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<BarLoadResult> {
|
||||
let icon_theme = IconTheme::new();
|
||||
if let Some(ref theme) = config.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(theme));
|
||||
@@ -166,7 +185,7 @@ fn load_modules(
|
||||
|
||||
// popup ignores module location so can bodge this for now
|
||||
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
|
||||
let popup = Arc::new(RwLock::new(popup));
|
||||
let popup = Rc::new(RefCell::new(popup));
|
||||
|
||||
if let Some(modules) = config.start {
|
||||
let info = info!(ModuleLocation::Left);
|
||||
@@ -183,7 +202,9 @@ fn load_modules(
|
||||
add_modules(right, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let result = BarLoadResult { popup };
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Adds modules into a provided GTK box,
|
||||
@@ -192,14 +213,20 @@ fn add_modules(
|
||||
content: >k::Box,
|
||||
modules: Vec<ModuleConfig>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
) -> Result<()> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
macro_rules! add_module {
|
||||
($module:expr, $id:expr) => {{
|
||||
let common = $module.common.take().expect("Common config did not exist");
|
||||
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
|
||||
let common = $module.common.take().expect("common config to exist");
|
||||
let widget_parts = create_module(
|
||||
*$module,
|
||||
$id,
|
||||
common.name.clone(),
|
||||
&info,
|
||||
&Rc::clone(&popup),
|
||||
)?;
|
||||
set_widget_identifiers(&widget_parts, &common);
|
||||
|
||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
||||
@@ -207,7 +234,7 @@ fn add_modules(
|
||||
}};
|
||||
}
|
||||
|
||||
for config in modules.into_iter() {
|
||||
for config in modules {
|
||||
let id = get_unique_usize();
|
||||
match config {
|
||||
#[cfg(feature = "clipboard")]
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::send;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// MPSC async -> sync channel.
|
||||
/// MPSC async -> GTK sync channel.
|
||||
/// The sender uses `tokio::sync::mpsc`
|
||||
/// while the receiver uses `glib::MainContext::channel`.
|
||||
///
|
||||
|
||||
145
src/cached_broadcast.rs
Normal file
145
src/cached_broadcast.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::{arc_rw, read_lock, send_async, write_lock};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, RwLock};
|
||||
// use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::sleep;
|
||||
use tracing::trace;
|
||||
|
||||
pub trait Cacheable: Debug + Clone + Send + Sync {
|
||||
type Key: Debug + Clone + Send + Sync + Eq;
|
||||
|
||||
fn get_key(&self) -> Self::Key;
|
||||
}
|
||||
|
||||
pub type Sender<T> = mpsc::Sender<Event<T>>;
|
||||
pub type Receiver<T> = mpsc::Receiver<Event<T>>;
|
||||
|
||||
pub struct CachedBroadcastChannel<T>
|
||||
where
|
||||
T: Cacheable,
|
||||
{
|
||||
capacity: usize,
|
||||
data: Vec<T>,
|
||||
channels: Arc<RwLock<Vec<mpsc::Sender<Event<T>>>>>,
|
||||
base_tx: mpsc::Sender<Event<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event<T>
|
||||
where
|
||||
T: Cacheable,
|
||||
{
|
||||
Add(T),
|
||||
Remove(T::Key),
|
||||
Replace(T::Key, T),
|
||||
}
|
||||
|
||||
impl<T> CachedBroadcastChannel<T>
|
||||
where
|
||||
T: Cacheable + 'static,
|
||||
{
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
let (tx, rx) = mpsc::channel::<Event<T>>(capacity);
|
||||
let mut rx = DropDetector(rx);
|
||||
|
||||
// spawn_blocking(move || loop {
|
||||
// let ev = rx.0.try_recv();
|
||||
// println!("{ev:?}");
|
||||
// sleep(Duration::from_secs(1))
|
||||
// });
|
||||
|
||||
let channels = arc_rw!(Vec::<Sender<T>>::new());
|
||||
|
||||
let channels = Arc::clone(&channels);
|
||||
spawn(async move {
|
||||
println!("hello");
|
||||
|
||||
while let Some(event) = rx.0.recv().await {
|
||||
println!("ev");
|
||||
// trace!("{event:?}");
|
||||
// let iter = read_lock!(channels).clone().into_iter();
|
||||
// for channel in iter {
|
||||
// send_async!(channel, event.clone());
|
||||
// }
|
||||
}
|
||||
println!("goodbye");
|
||||
});
|
||||
|
||||
Self {
|
||||
capacity,
|
||||
data: vec![],
|
||||
channels,
|
||||
base_tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, event: Event<T>) {
|
||||
match event.clone() {
|
||||
Event::Add(data) => {
|
||||
self.data.push(data);
|
||||
}
|
||||
Event::Remove(key) => {
|
||||
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
|
||||
return;
|
||||
};
|
||||
self.data.remove(index);
|
||||
}
|
||||
Event::Replace(key, data) => {
|
||||
let Some(index) = self.data.iter().position(|t| t.get_key() == key) else {
|
||||
return;
|
||||
};
|
||||
let _ = std::mem::replace(&mut self.data[index], data);
|
||||
}
|
||||
}
|
||||
|
||||
send_async!(self.base_tx, event);
|
||||
|
||||
// let mut closed = vec![];
|
||||
// for (i, channel) in read_lock!(self.channels).iter().enumerate() {
|
||||
// if channel.is_closed() {
|
||||
// closed.push(i);
|
||||
// } else {
|
||||
// send_async!(channel, event.clone());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for channel in closed.into_iter().rev() {
|
||||
// write_lock!(self.channels).remove(channel);
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> mpsc::Sender<Event<T>> {
|
||||
self.base_tx.clone()
|
||||
}
|
||||
|
||||
pub fn receiver(&mut self) -> mpsc::Receiver<Event<T>> {
|
||||
let (tx, rx) = mpsc::channel(self.capacity);
|
||||
write_lock!(self.channels).push(tx);
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Vec<T> {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DropDetector<T>(T);
|
||||
|
||||
impl<T> Drop for DropDetector<T> {
|
||||
fn drop(&mut self) {
|
||||
println!("DROPPED")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Cacheable> Drop for CachedBroadcastChannel<T> {
|
||||
fn drop(&mut self) {
|
||||
println!("Channel DROPPED")
|
||||
}
|
||||
}
|
||||
19
src/cli/mod.rs
Normal file
19
src/cli/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::ipc::commands::Command;
|
||||
use crate::ipc::responses::Response;
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Parser, Debug, Serialize, Deserialize)]
|
||||
#[command(version)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
pub fn handle_response(response: Response) {
|
||||
match response {
|
||||
Response::Ok => println!("ok"),
|
||||
Response::OkValue { value } => println!("ok\n{value}"),
|
||||
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::wayland::{self, ClipboardItem};
|
||||
use crate::{lock, try_send};
|
||||
use crate::{arc_mut, lock, try_send};
|
||||
use indexmap::map::Iter;
|
||||
use indexmap::IndexMap;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -28,9 +28,9 @@ impl ClipboardClient {
|
||||
fn new() -> Self {
|
||||
trace!("Initializing clipboard client");
|
||||
|
||||
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
|
||||
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
|
||||
|
||||
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
|
||||
let cache = arc_mut!(ClipboardCache::new());
|
||||
|
||||
{
|
||||
let senders = senders.clone();
|
||||
@@ -38,7 +38,8 @@ impl ClipboardClient {
|
||||
|
||||
spawn(async move {
|
||||
let (mut rx, item) = {
|
||||
let wl = wayland::get_client().await;
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_clipboard()
|
||||
};
|
||||
|
||||
@@ -111,7 +112,7 @@ impl ClipboardClient {
|
||||
rx
|
||||
}
|
||||
|
||||
pub async fn copy(&self, id: usize) {
|
||||
pub fn copy(&self, id: usize) {
|
||||
debug!("Copying item with id {id}");
|
||||
|
||||
let item = {
|
||||
@@ -120,7 +121,8 @@ impl ClipboardClient {
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let wl = wayland::get_client().await;
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.copy_to_clipboard(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{lock, send};
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{arc_mut, lock, send};
|
||||
use color_eyre::Result;
|
||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
use hyprland::event_listener::EventListenerMutable as EventListener;
|
||||
use hyprland::event_listener::EventListener;
|
||||
use hyprland::prelude::*;
|
||||
use hyprland::shared::WorkspaceType;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, info};
|
||||
@@ -36,28 +35,25 @@ impl EventClient {
|
||||
let mut event_listener = EventListener::new();
|
||||
|
||||
// we need a lock to ensure events don't run at the same time
|
||||
let lock = Arc::new(Mutex::new(()));
|
||||
let lock = arc_mut!(());
|
||||
|
||||
// cache the active workspace since Hyprland doesn't give us the prev active
|
||||
let active = Self::get_active_workspace().expect("Failed to get active workspace");
|
||||
let active = Arc::new(Mutex::new(Some(active)));
|
||||
let active = arc_mut!(Some(active));
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_added_handler(move |workspace_type, _state| {
|
||||
event_listener.add_workspace_added_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Added workspace: {workspace_type:?}");
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let prev_workspace = lock!(active);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||
@@ -70,7 +66,7 @@ impl EventClient {
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_change_handler(move |workspace_type, _state| {
|
||||
event_listener.add_workspace_change_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
@@ -81,10 +77,7 @@ impl EventClient {
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
workspace.map_or_else(
|
||||
|| {
|
||||
@@ -93,7 +86,7 @@ impl EventClient {
|
||||
|workspace| {
|
||||
// there may be another type of update so dispatch that regardless of focus change
|
||||
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
|
||||
if !focused {
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
},
|
||||
@@ -106,9 +99,9 @@ impl EventClient {
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_active_monitor_change_handler(move |event_data, _state| {
|
||||
event_listener.add_active_monitor_change_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.1;
|
||||
let workspace_type = event_data.workspace;
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
@@ -118,12 +111,11 @@ impl EventClient {
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let (Some(workspace), false) = (workspace, focused) {
|
||||
if let Some((false, workspace)) =
|
||||
workspace.map(|w| (w.visibility.is_focused(), w))
|
||||
{
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
} else {
|
||||
error!("Unable to locate workspace");
|
||||
@@ -135,23 +127,20 @@ impl EventClient {
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data, _state| {
|
||||
event_listener.add_workspace_moved_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.1;
|
||||
let workspace_type = event_data.workspace;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||
|
||||
if !focused {
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
}
|
||||
@@ -159,7 +148,7 @@ impl EventClient {
|
||||
}
|
||||
|
||||
{
|
||||
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
|
||||
event_listener.add_workspace_destroy_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace destroy: {workspace_type:?}");
|
||||
|
||||
@@ -181,32 +170,28 @@ impl EventClient {
|
||||
workspace: Workspace,
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
) {
|
||||
let old = prev_workspace
|
||||
.as_ref()
|
||||
.map(|w| w.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Focus {
|
||||
old,
|
||||
new: workspace.name.clone(),
|
||||
old: prev_workspace.take(),
|
||||
new: workspace.clone(),
|
||||
}
|
||||
);
|
||||
|
||||
prev_workspace.replace(workspace);
|
||||
}
|
||||
|
||||
/// Gets a workspace by name from the server.
|
||||
///
|
||||
/// Use `focused` to manually mark the workspace as focused,
|
||||
/// as this is not automatically checked.
|
||||
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
|
||||
/// Gets a workspace by name from the server, given the active workspace if known.
|
||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
|
||||
Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.find_map(|w| {
|
||||
if w.name == name {
|
||||
Some(Workspace::from((focused, w)))
|
||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
||||
create_is_visible()(w)
|
||||
}));
|
||||
|
||||
Some(Workspace::from((vis, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -215,16 +200,19 @@ impl EventClient {
|
||||
|
||||
/// Gets the active workspace from the server.
|
||||
fn get_active_workspace() -> Result<Workspace> {
|
||||
let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
|
||||
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?;
|
||||
Ok(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for EventClient {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
Dispatch::call(DispatchType::Workspace(
|
||||
WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
))?;
|
||||
let identifier = match id.parse::<i32>() {
|
||||
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
|
||||
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
};
|
||||
|
||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -234,13 +222,16 @@ impl WorkspaceClient for EventClient {
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
let active_name = HWorkspace::get_active()
|
||||
.map(|active| active.name)
|
||||
.unwrap_or_default();
|
||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||
let is_visible = create_is_visible();
|
||||
|
||||
let workspaces = Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.map(|w| Workspace::from((w.name == active_name, w)))
|
||||
.map(|w| {
|
||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||
|
||||
Workspace::from((vis, w))
|
||||
})
|
||||
.collect();
|
||||
|
||||
send!(tx, WorkspaceUpdate::Init(workspaces));
|
||||
@@ -269,13 +260,36 @@ fn get_workspace_name(name: WorkspaceType) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(bool, hyprland::data::Workspace)> for Workspace {
|
||||
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
|
||||
/// Creates a function which determines if a workspace is visible. This function makes a Hyprland call that allocates so it should be cached when possible, but it is only valid so long as workspaces do not change so it should not be stored long term
|
||||
fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
|
||||
let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), |ms| ms.to_vec());
|
||||
|
||||
move |w| monitors.iter().any(|m| m.active_workspace.id == w.id)
|
||||
}
|
||||
|
||||
impl From<(Visibility, HWorkspace)> for Workspace {
|
||||
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.monitor,
|
||||
focused,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility
|
||||
where
|
||||
F: FnOnce(&'f HWorkspace) -> bool,
|
||||
'a: 'f,
|
||||
{
|
||||
fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self {
|
||||
if Some(workspace.name.as_str()) == active_name {
|
||||
Self::focused()
|
||||
} else if is_visible(workspace) {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,38 @@ pub struct Workspace {
|
||||
pub name: String,
|
||||
/// Name of the monitor (output) the workspace is located on
|
||||
pub monitor: String,
|
||||
/// Whether the workspace is in focus
|
||||
pub focused: bool,
|
||||
/// How visible the workspace is
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
|
||||
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Visibility {
|
||||
Visible(bool),
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
pub fn visible() -> Self {
|
||||
Self::Visible(false)
|
||||
}
|
||||
|
||||
pub fn focused() -> Self {
|
||||
Self::Visible(true)
|
||||
}
|
||||
|
||||
pub fn is_visible(self) -> bool {
|
||||
matches!(self, Self::Visible(_))
|
||||
}
|
||||
|
||||
pub fn is_focused(self) -> bool {
|
||||
if let Self::Visible(focused) = self {
|
||||
focused
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -90,8 +120,8 @@ pub enum WorkspaceUpdate {
|
||||
Move(Workspace),
|
||||
/// Declares focus moved from the old workspace to the new.
|
||||
Focus {
|
||||
old: String,
|
||||
new: String,
|
||||
old: Option<Workspace>,
|
||||
new: Workspace,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{await_sync, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
@@ -105,22 +105,50 @@ pub fn get_sub_client() -> &'static SwayEventClient {
|
||||
|
||||
impl From<Node> for Workspace {
|
||||
fn from(node: Node) -> Self {
|
||||
let visibility = Visibility::from(&node);
|
||||
|
||||
Self {
|
||||
id: node.id.to_string(),
|
||||
name: node.name.unwrap_or_default(),
|
||||
monitor: node.output.unwrap_or_default(),
|
||||
focused: node.focused,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<swayipc_async::Workspace> for Workspace {
|
||||
fn from(workspace: swayipc_async::Workspace) -> Self {
|
||||
let visibility = Visibility::from(&workspace);
|
||||
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.output,
|
||||
focused: workspace.focused,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Node> for Visibility {
|
||||
fn from(node: &Node) -> Self {
|
||||
if node.focused {
|
||||
Self::focused()
|
||||
} else if node.visible.unwrap_or(false) {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&swayipc_async::Workspace> for Visibility {
|
||||
fn from(workspace: &swayipc_async::Workspace) -> Self {
|
||||
if workspace.focused {
|
||||
Self::focused()
|
||||
} else if workspace.visible {
|
||||
Self::visible()
|
||||
} else {
|
||||
Self::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,16 +167,8 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
WorkspaceChange::Focus => Self::Focus {
|
||||
old: event
|
||||
.old
|
||||
.expect("Missing old workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
new: event
|
||||
.current
|
||||
.expect("Missing current workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
old: event.old.map(Workspace::from),
|
||||
new: Workspace::from(event.current.expect("Missing current workspace")),
|
||||
},
|
||||
WorkspaceChange::Move => {
|
||||
Self::Move(event.current.expect("Missing current workspace").into())
|
||||
|
||||
@@ -9,9 +9,17 @@ pub mod mpd;
|
||||
#[cfg(feature = "music+mpris")]
|
||||
pub mod mpris;
|
||||
|
||||
pub const TICK_INTERVAL_MS: u64 = 200;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerUpdate {
|
||||
/// Triggered when the track or player state notably changes,
|
||||
/// such as a new track playing, the player being paused, or a volume change.
|
||||
Update(Box<Option<Track>>, Status),
|
||||
/// Triggered at regular intervals while a track is playing.
|
||||
/// Used to keep track of the progress through the current track.
|
||||
ProgressTick(ProgressTick),
|
||||
/// Triggered when the client disconnects from the player.
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
@@ -27,23 +35,27 @@ pub struct Track {
|
||||
pub cover_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PlayerState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Status {
|
||||
pub state: PlayerState,
|
||||
pub volume_percent: u8,
|
||||
pub duration: Option<Duration>,
|
||||
pub elapsed: Option<Duration>,
|
||||
pub volume_percent: Option<u8>,
|
||||
pub playlist_position: u32,
|
||||
pub playlist_length: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProgressTick {
|
||||
pub duration: Option<Duration>,
|
||||
pub elapsed: Option<Duration>,
|
||||
}
|
||||
|
||||
pub trait MusicClient {
|
||||
fn play(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
@@ -51,6 +63,7 @@ pub trait MusicClient {
|
||||
fn prev(&self) -> Result<()>;
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||
fn seek(&self, duration: Duration) -> Result<()>;
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::{MusicClient, Status, Track};
|
||||
use crate::await_sync;
|
||||
use crate::clients::music::{PlayerState, PlayerUpdate};
|
||||
use super::{
|
||||
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
||||
};
|
||||
use crate::{await_sync, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||
use mpd_client::commands::SeekMode;
|
||||
use mpd_client::protocol::MpdProtocolError;
|
||||
use mpd_client::responses::{PlayState, Song};
|
||||
use mpd_client::tag::Tag;
|
||||
@@ -16,7 +18,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::{TcpStream, UnixStream};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
@@ -29,8 +31,8 @@ lazy_static! {
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
music_dir: PathBuf,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -57,7 +59,7 @@ impl MpdClient {
|
||||
let (client, mut state_changes) =
|
||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||
|
||||
let (tx, rx) = channel(16);
|
||||
let (tx, rx) = broadcast::channel(16);
|
||||
|
||||
{
|
||||
let music_dir = music_dir.clone();
|
||||
@@ -78,7 +80,19 @@ impl MpdClient {
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), SendError<(Option<Track>, Status)>>(())
|
||||
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn(async move {
|
||||
loop {
|
||||
Self::send_tick_update(&client, &tx).await;
|
||||
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,9 +106,9 @@ impl MpdClient {
|
||||
|
||||
async fn send_update(
|
||||
client: &Client,
|
||||
tx: &Sender<PlayerUpdate>,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
music_dir: &Path,
|
||||
) -> Result<(), SendError<PlayerUpdate>> {
|
||||
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
let current_song = client.command(commands::CurrentSong).await;
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
@@ -102,17 +116,33 @@ impl MpdClient {
|
||||
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
||||
let status = Status::from(status);
|
||||
|
||||
tx.send(PlayerUpdate::Update(Box::new(track), status))?;
|
||||
let update = PlayerUpdate::Update(Box::new(track), status);
|
||||
send!(tx, update);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let Ok(status) = status {
|
||||
if status.state == PlayState::Playing {
|
||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||
duration: status.duration,
|
||||
elapsed: status.elapsed,
|
||||
});
|
||||
|
||||
send!(tx, update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
!self.client.is_connection_closed()
|
||||
}
|
||||
|
||||
fn send_disconnect_update(&self) -> Result<(), SendError<PlayerUpdate>> {
|
||||
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
info!("Connection to MPD server lost");
|
||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||
Ok(())
|
||||
@@ -182,7 +212,12 @@ impl MusicClient for MpdClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
await_sync(async {
|
||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||
@@ -291,9 +326,7 @@ impl From<mpd_client::responses::Status> for Status {
|
||||
fn from(status: mpd_client::responses::Status) -> Self {
|
||||
Self {
|
||||
state: PlayerState::from(status.state),
|
||||
volume_percent: status.volume,
|
||||
duration: status.duration,
|
||||
elapsed: status.elapsed,
|
||||
volume_percent: Some(status.volume),
|
||||
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
||||
playlist_length: status.playlist_length as u32,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::{MusicClient, PlayerUpdate, Status, Track};
|
||||
use crate::clients::music::PlayerState;
|
||||
use crate::{lock, send};
|
||||
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
|
||||
use crate::clients::music::ProgressTick;
|
||||
use crate::{arc_mut, lock, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::collections::HashSet;
|
||||
use std::string;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use std::{cmp, string};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
@@ -19,18 +19,18 @@ lazy_static! {
|
||||
|
||||
pub struct Client {
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = channel(32);
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
||||
let current_player = Arc::new(Mutex::new(None));
|
||||
let current_player = arc_mut!(None);
|
||||
|
||||
{
|
||||
let players_list = Arc::new(Mutex::new(HashSet::new()));
|
||||
let players_list = arc_mut!(HashSet::new());
|
||||
let current_player = current_player.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
@@ -84,6 +84,20 @@ impl Client {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let current_player = current_player.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new().expect("to get new player finder");
|
||||
|
||||
loop {
|
||||
Self::send_tick_update(&player_finder, ¤t_player, &tx);
|
||||
sleep(Duration::from_millis(TICK_INTERVAL_MS));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
current_player,
|
||||
tx,
|
||||
@@ -95,7 +109,7 @@ impl Client {
|
||||
player_id: String,
|
||||
players: Arc<Mutex<HashSet<String>>>,
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
) {
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new()?;
|
||||
@@ -138,7 +152,7 @@ impl Client {
|
||||
});
|
||||
}
|
||||
|
||||
fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
|
||||
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
|
||||
debug!("Sending update using '{}'", player.identity());
|
||||
|
||||
let metadata = player.get_metadata()?;
|
||||
@@ -148,10 +162,7 @@ impl Client {
|
||||
|
||||
let track_list = player.get_track_list();
|
||||
|
||||
let volume_percent = player
|
||||
.get_volume()
|
||||
.map(|vol| (vol * 100.0) as u8)
|
||||
.unwrap_or(0);
|
||||
let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok();
|
||||
|
||||
let status = Status {
|
||||
// MRPIS doesn't seem to provide playlist info reliably,
|
||||
@@ -159,8 +170,6 @@ impl Client {
|
||||
playlist_position: 1,
|
||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
||||
state: PlayerState::from(playback_status),
|
||||
elapsed: player.get_position().ok(),
|
||||
duration: metadata.length(),
|
||||
volume_percent,
|
||||
};
|
||||
|
||||
@@ -181,6 +190,26 @@ impl Client {
|
||||
player_finder.find_by_name(player_name).ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn send_tick_update(
|
||||
player_finder: &PlayerFinder,
|
||||
current_player: &Mutex<Option<String>>,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
) {
|
||||
if let Some(player) = lock!(current_player)
|
||||
.as_ref()
|
||||
.and_then(|name| player_finder.find_by_name(name).ok())
|
||||
{
|
||||
if let Ok(metadata) = player.get_metadata() {
|
||||
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||
elapsed: player.get_position().ok(),
|
||||
duration: metadata.length(),
|
||||
});
|
||||
|
||||
send!(tx, update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! command {
|
||||
@@ -223,7 +252,23 @@ impl MusicClient for Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
if let Some(player) = Self::get_player(self) {
|
||||
let pos = player.get_position().unwrap_or_default();
|
||||
|
||||
let duration = duration.as_micros() as i64;
|
||||
let position = pos.as_micros() as i64;
|
||||
|
||||
let seek = cmp::max(duration, 0) - position;
|
||||
|
||||
player.seek(seek)?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
debug!("Creating new subscription");
|
||||
let rx = self.tx.subscribe();
|
||||
|
||||
@@ -236,9 +281,7 @@ impl MusicClient for Client {
|
||||
playlist_position: 0,
|
||||
playlist_length: 0,
|
||||
state: PlayerState::Stopped,
|
||||
elapsed: None,
|
||||
duration: None,
|
||||
volume_percent: 0,
|
||||
volume_percent: None,
|
||||
};
|
||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||
}
|
||||
@@ -257,9 +300,18 @@ impl From<Metadata> for Track {
|
||||
const KEY_GENRE: &str = "xesam:genre";
|
||||
|
||||
Self {
|
||||
title: value.title().map(std::string::ToString::to_string),
|
||||
album: value.album_name().map(std::string::ToString::to_string),
|
||||
artist: value.artists().map(|artists| artists.join(", ")),
|
||||
title: value
|
||||
.title()
|
||||
.map(std::string::ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
album: value
|
||||
.album_name()
|
||||
.map(std::string::ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
artist: value
|
||||
.artists()
|
||||
.map(|artists| artists.join(", "))
|
||||
.and_then(replace_empty_none),
|
||||
date: value
|
||||
.get(KEY_DATE)
|
||||
.and_then(mpris::MetadataValue::as_string)
|
||||
@@ -284,3 +336,11 @@ impl From<PlaybackStatus> for PlayerState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_empty_none(string: String) -> Option<String> {
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{lock, send};
|
||||
use crate::{arc_mut, lock, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use stray::message::menu::TrayMenu;
|
||||
use stray::message::tray::StatusNotifierItem;
|
||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use stray::StatusNotifierWatcher;
|
||||
use system_tray::message::menu::TrayMenu;
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::StatusNotifierWatcher;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{debug, error, trace};
|
||||
@@ -24,16 +24,16 @@ pub struct TrayEventReceiver {
|
||||
}
|
||||
|
||||
impl TrayEventReceiver {
|
||||
async fn new() -> stray::error::Result<Self> {
|
||||
async fn new() -> system_tray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", get_unique_usize());
|
||||
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let (b_tx, b_rx) = broadcast::channel(16);
|
||||
|
||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||
let mut host = tray.create_notifier_host(&id).await?;
|
||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
||||
|
||||
let tray = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let tray = arc_mut!(BTreeMap::new());
|
||||
|
||||
{
|
||||
let b_tx = b_tx.clone();
|
||||
@@ -106,7 +106,7 @@ lazy_static! {
|
||||
let value = loop {
|
||||
retries += 1;
|
||||
|
||||
let tray = TrayEventReceiver::new().await;
|
||||
let tray = Box::pin(TrayEventReceiver::new()).await;
|
||||
|
||||
match tray {
|
||||
Ok(tray) => break Some(tray),
|
||||
|
||||
@@ -6,7 +6,7 @@ use zbus::fdo::PropertiesProxy;
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||
let dbus = zbus::Connection::system()
|
||||
let dbus = Box::pin(zbus::Connection::system())
|
||||
.await
|
||||
.expect("failed to create connection to system bus");
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ use super::wlr_foreign_toplevel::handle::ToplevelHandle;
|
||||
use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use super::wlr_foreign_toplevel::ToplevelEvent;
|
||||
use super::Environment;
|
||||
use crate::cached_broadcast::CachedBroadcastChannel;
|
||||
use crate::error::ERR_CHANNEL_RECV;
|
||||
use crate::send;
|
||||
use crate::{cached_broadcast, send};
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Report;
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
@@ -25,17 +26,14 @@ cfg_if! {
|
||||
use super::ClipboardItem;
|
||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
/// Sends a request for all the outputs.
|
||||
/// These are then sent on the `output` channel.
|
||||
Outputs,
|
||||
/// Sends a request for all the seats.
|
||||
/// These are then sent ont the `seat` channel.
|
||||
/// These are then sent on the `seat` channel.
|
||||
Seats,
|
||||
/// Sends a request for all the toplevels.
|
||||
/// These are then sent on the `toplevel_init` channel.
|
||||
@@ -53,8 +51,10 @@ pub enum Request {
|
||||
|
||||
pub struct WaylandClient {
|
||||
// External channels
|
||||
output_channel: CachedBroadcastChannel<OutputInfo>,
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
@@ -62,7 +62,6 @@ pub struct WaylandClient {
|
||||
|
||||
// Internal channels
|
||||
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
|
||||
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
|
||||
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
|
||||
@@ -71,13 +70,17 @@ pub struct WaylandClient {
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(super) async fn new() -> Self {
|
||||
pub(super) fn new() -> Self {
|
||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||
|
||||
let mut output_channel = CachedBroadcastChannel::new(8);
|
||||
let output_tx = output_channel.sender();
|
||||
|
||||
let tx2 = output_tx.clone();
|
||||
|
||||
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
||||
#[cfg(feature = "clipboard")]
|
||||
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
|
||||
let (output_tx, output_rx) = mpsc::channel();
|
||||
let (seat_tx, seat_rx) = mpsc::channel();
|
||||
|
||||
let toplevel_tx2 = toplevel_tx.clone();
|
||||
@@ -99,6 +102,7 @@ impl WaylandClient {
|
||||
|
||||
let conn =
|
||||
Connection::connect_to_env().expect("Failed to connect to Wayland compositor");
|
||||
|
||||
let (globals, queue) =
|
||||
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
|
||||
|
||||
@@ -138,7 +142,8 @@ impl WaylandClient {
|
||||
seats: vec![],
|
||||
handles: HashMap::new(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc::new(Mutex::new(None)),
|
||||
clipboard: crate::arc_mut!(None),
|
||||
output_tx,
|
||||
toplevel_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
@@ -156,11 +161,6 @@ impl WaylandClient {
|
||||
trace!("{event:?}");
|
||||
match event {
|
||||
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
|
||||
Event::Msg(Request::Outputs) => {
|
||||
trace!("Received get outputs request");
|
||||
|
||||
send!(output_tx, env.output_info());
|
||||
}
|
||||
Event::Msg(Request::Seats) => {
|
||||
trace!("Receive get seats request");
|
||||
send!(seat_tx, env.seats.clone());
|
||||
@@ -196,12 +196,12 @@ impl WaylandClient {
|
||||
});
|
||||
|
||||
Self {
|
||||
output_channel,
|
||||
toplevel_tx,
|
||||
_toplevel_rx: toplevel_rx,
|
||||
toplevel_init_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx,
|
||||
output_rx,
|
||||
seat_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
@@ -242,6 +242,10 @@ impl WaylandClient {
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
pub fn subscribe_outputs(&mut self) -> cached_broadcast::Receiver<OutputInfo> {
|
||||
self.output_channel.receiver()
|
||||
}
|
||||
|
||||
/// Force a roundtrip on the wayland connection,
|
||||
/// flushing any queued events and immediately receiving any new ones.
|
||||
pub fn roundtrip(&self) {
|
||||
@@ -249,11 +253,13 @@ impl WaylandClient {
|
||||
send!(self.request_tx, Request::Roundtrip);
|
||||
}
|
||||
|
||||
pub fn get_outputs(&self) -> Vec<OutputInfo> {
|
||||
trace!("Sending get outputs request");
|
||||
|
||||
send!(self.request_tx, Request::Outputs);
|
||||
self.output_rx.recv().expect(ERR_CHANNEL_RECV)
|
||||
/// Gets a list of all outputs.
|
||||
///
|
||||
/// This should only be used in a scenario
|
||||
/// where you need a snapshot of outputs at the current time.
|
||||
/// Prefer to listen to output events with `subscribe_output` where possible.
|
||||
pub fn get_outputs(&self) -> &Vec<OutputInfo> {
|
||||
self.output_channel.data()
|
||||
}
|
||||
|
||||
pub fn get_seats(&self) -> Vec<WlSeat> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// It is necessary to store macros in a separate file due to a compilation error.
|
||||
/// I believe this stems from the feature flags.
|
||||
/// Related issue: https://github.com/rust-lang/rust/issues/81066
|
||||
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
|
||||
|
||||
// --- Data Control Device --- \\
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ mod wl_seat;
|
||||
mod wlr_foreign_toplevel;
|
||||
|
||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||
use async_once::AsyncOnce;
|
||||
use crate::{arc_mut, cached_broadcast, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||
use cfg_if::cfg_if;
|
||||
use lazy_static::lazy_static;
|
||||
use smithay_client_toolkit::output::OutputState;
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
||||
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
@@ -18,6 +17,7 @@ use smithay_client_toolkit::{
|
||||
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
|
||||
@@ -33,7 +33,6 @@ cfg_if! {
|
||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use self::wlr_data_control::source::CopyPasteSource;
|
||||
use self::wlr_data_control::SelectionOfferItem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||
|
||||
@@ -66,6 +65,7 @@ pub struct Environment {
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
|
||||
|
||||
output_tx: cached_broadcast::Sender<OutputInfo>,
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
@@ -106,10 +106,9 @@ impl ProvidesRegistryState for Environment {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: AsyncOnce<WaylandClient> =
|
||||
AsyncOnce::new(async { WaylandClient::new().await });
|
||||
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
|
||||
}
|
||||
|
||||
pub async fn get_client() -> &'static WaylandClient {
|
||||
CLIENT.get().await
|
||||
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::Environment;
|
||||
use crate::cached_broadcast::Cacheable;
|
||||
use crate::{cached_broadcast, try_send};
|
||||
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_output;
|
||||
@@ -31,9 +33,12 @@ impl OutputHandler for Environment {
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handler received new output");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(self.output_tx, cached_broadcast::Event::Add(info));
|
||||
};
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
@@ -42,14 +47,26 @@ impl OutputHandler for Environment {
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handle received output update");
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handle received output destruction");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(self.output_tx, cached_broadcast::Event::Remove(info.id));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Cacheable for OutputInfo {
|
||||
type Key = u32;
|
||||
|
||||
fn get_key(&self) -> Self::Key {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{lock, send};
|
||||
use device::DataControlDevice;
|
||||
use glib::Bytes;
|
||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
||||
use nix::sys::epoll::{epoll_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp};
|
||||
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||
use std::cmp::min;
|
||||
@@ -239,7 +239,7 @@ impl DataControlOfferHandler for Environment {
|
||||
_offer: &mut DataControlDeviceOffer,
|
||||
_mime_type: String,
|
||||
) {
|
||||
debug!("Handler received offer");
|
||||
trace!("Handler received offer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,23 +289,22 @@ impl DataControlSourceHandler for Environment {
|
||||
trace!("Num bytes: {}", bytes.len());
|
||||
|
||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
|
||||
let epoll_fd = epoll_create().unwrap();
|
||||
epoll_ctl(
|
||||
epoll_fd,
|
||||
EpollOp::EpollCtlAdd,
|
||||
fd.as_raw_fd(),
|
||||
&mut epoll_event,
|
||||
)
|
||||
.unwrap();
|
||||
let epoll_fd =
|
||||
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
|
||||
epoll_fd
|
||||
.add(fd, epoll_event)
|
||||
.expect("to send valid epoll operation");
|
||||
|
||||
while !bytes.is_empty() {
|
||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||
|
||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
||||
|
||||
epoll_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
|
||||
epoll_fd
|
||||
.wait(&mut events, 100)
|
||||
.expect("Failed to wait to epoll");
|
||||
|
||||
match file.write(chunk) {
|
||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
||||
@@ -315,22 +314,6 @@ impl DataControlSourceHandler for Environment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for chunk in bytes.chunks(pipe_size as usize) {
|
||||
// trace!("Writing chunk");
|
||||
// file.write(chunk).expect("Failed to write chunk to buffer");
|
||||
// file.flush().expect("Failed to flush to file");
|
||||
// }
|
||||
|
||||
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
|
||||
// Ok(_) => debug!("Copied item"),
|
||||
// Err(err) => error!("{err:?}"),
|
||||
// }
|
||||
|
||||
// match file.write_all(bytes) {
|
||||
// Ok(_) => debug!("Copied item"),
|
||||
// Err(err) => error!("{err:?}"),
|
||||
// }
|
||||
} else {
|
||||
error!("Failed to find source");
|
||||
}
|
||||
@@ -375,11 +358,14 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||
|
||||
let new_size = if size > curr_size {
|
||||
trace!("Requesting pipe size increase to (at least): {size}");
|
||||
|
||||
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
|
||||
trace!("New pipe size: {res}");
|
||||
|
||||
if res < size as i32 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
size as i32
|
||||
|
||||
@@ -7,7 +7,7 @@ use smithay_client_toolkit::data_device_manager::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, warn};
|
||||
use tracing::{trace, warn};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
Event, ZwlrDataControlOfferV1,
|
||||
@@ -149,7 +149,7 @@ where
|
||||
let data = data.data_control_offer_data();
|
||||
|
||||
if let Event::Offer { mime_type } = event {
|
||||
debug!("Adding new offer with type '{mime_type}'");
|
||||
trace!("Adding new offer with type '{mime_type}'");
|
||||
data.push_mime_type(mime_type.clone());
|
||||
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
|
||||
}
|
||||
|
||||
@@ -151,9 +151,8 @@ where
|
||||
lock!(data.inner).current_info = Some(pending_info);
|
||||
}
|
||||
|
||||
if !lock!(data.inner).initial_done {
|
||||
lock!(data.inner).initial_done = true;
|
||||
state.new_handle(
|
||||
if lock!(data.inner).initial_done {
|
||||
state.update_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
@@ -161,7 +160,8 @@ where
|
||||
},
|
||||
);
|
||||
} else {
|
||||
state.update_handle(
|
||||
lock!(data.inner).initial_done = true;
|
||||
state.new_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
|
||||
@@ -30,7 +30,7 @@ impl ToplevelManagerHandler for Environment {
|
||||
|
||||
impl ToplevelHandleHandler for Environment {
|
||||
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
|
||||
debug!("Handler received new handle");
|
||||
trace!("Handler received new handle");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
@@ -50,7 +50,7 @@ impl ToplevelHandleHandler for Environment {
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
debug!("Handler received handle update");
|
||||
trace!("Handler received handle update");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use crate::send;
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tracing::trace;
|
||||
|
||||
/// Common configuration options
|
||||
@@ -15,7 +13,7 @@ pub struct CommonConfig {
|
||||
pub class: Option<String>,
|
||||
pub name: Option<String>,
|
||||
|
||||
pub show_if: Option<ScriptInput>,
|
||||
pub show_if: Option<DynamicBool>,
|
||||
pub transition_type: Option<TransitionType>,
|
||||
pub transition_duration: Option<u32>,
|
||||
|
||||
@@ -114,7 +112,7 @@ impl CommonConfig {
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
let container = container.clone();
|
||||
DynamicString::new(&tooltip, move |string| {
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
@@ -127,23 +125,13 @@ impl CommonConfig {
|
||||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
let script = Script::new_polling(show_if);
|
||||
let container = container.clone();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
{
|
||||
let revealer = revealer.clone();
|
||||
let container = container.clone();
|
||||
|
||||
rx.attach(None, move |success| {
|
||||
show_if.subscribe(move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::modules::tray::TrayModule;
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use cfg_if::cfg_if;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -30,7 +31,7 @@ pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ModuleConfig {
|
||||
#[cfg(feature = "clock")]
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
@@ -96,10 +97,13 @@ pub struct Config {
|
||||
pub margin: MarginConfig,
|
||||
#[serde(default = "default_popup_gap")]
|
||||
pub popup_gap: i32,
|
||||
pub name: Option<String>,
|
||||
|
||||
/// GTK icon theme to use.
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
||||
|
||||
pub start: Option<Vec<ModuleConfig>>,
|
||||
pub center: Option<Vec<ModuleConfig>>,
|
||||
pub end: Option<Vec<ModuleConfig>>,
|
||||
@@ -107,6 +111,36 @@ pub struct Config {
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clock")] {
|
||||
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
|
||||
}
|
||||
else {
|
||||
let end = None;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
position: BarPosition::default(),
|
||||
height: default_bar_height(),
|
||||
margin: MarginConfig::default(),
|
||||
name: None,
|
||||
popup_gap: default_popup_gap(),
|
||||
icon_theme: None,
|
||||
ironvar_defaults: None,
|
||||
start: Some(vec![ModuleConfig::Label(
|
||||
LabelModule::new("ℹ️ Using default config".to_string()).into(),
|
||||
)]),
|
||||
center: Some(vec![ModuleConfig::Focused(Box::default())]),
|
||||
end,
|
||||
anchor_to_edges: default_true(),
|
||||
monitors: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_bar_height() -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
/// Gets directories that should contain `.desktop` files
|
||||
use crate::lock;
|
||||
|
||||
type DesktopFile = HashMap<String, Vec<String>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> =
|
||||
Mutex::new(HashMap::new());
|
||||
|
||||
/// These are the keys that in the cache
|
||||
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> =
|
||||
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]);
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
||||
let user_dir = dirs::data_local_dir();
|
||||
let mut dirs = vec![
|
||||
PathBuf::from("/usr/share/applications"), // system installed apps
|
||||
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
|
||||
];
|
||||
|
||||
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
|
||||
if let Some(xdg_dirs) = xdg_dirs {
|
||||
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
|
||||
xdg_dir.push("applications");
|
||||
dirs.push(xdg_dir);
|
||||
}
|
||||
}
|
||||
|
||||
let user_dir = dirs::data_local_dir(); // user installed apps
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
@@ -19,55 +45,164 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
/// (or app class).
|
||||
///
|
||||
/// A simple case-insensitive check is performed on filename == `app_id`.
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
/// Finds all the desktop files
|
||||
fn find_desktop_files() -> Vec<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
dirs.into_iter()
|
||||
.flat_map(|dir| {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
for dir in dirs {
|
||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
// this is necessary to invalidate the cache
|
||||
let files = find_desktop_files();
|
||||
|
||||
let entry = walker.find(|entry| {
|
||||
entry.as_ref().map_or(false, |entry| {
|
||||
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
||||
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
||||
file_name == test_name
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using a simple condition check
|
||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let with_names = files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f,
|
||||
f.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
with_names
|
||||
.iter()
|
||||
// first pass - check for exact match
|
||||
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
|
||||
// second pass - check for substring
|
||||
.or_else(|| {
|
||||
with_names.iter().find(|(_, name)| {
|
||||
// this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
app_id
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part))
|
||||
})
|
||||
})
|
||||
.map(|(file, _)| file.into())
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
|
||||
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let app_id = &app_id.to_lowercase();
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let files = files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let Some(parsed_desktop_file) = parse_desktop_file(file) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = files
|
||||
.iter()
|
||||
// first pass - check name key for exact match
|
||||
.find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
// second pass - check name key for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.map(|names| {
|
||||
names
|
||||
.iter()
|
||||
.any(|name| name.to_lowercase().contains(app_id))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
})
|
||||
// third pass - check all keys for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(Ok(entry)) = entry {
|
||||
let path = entry.path().to_owned();
|
||||
return Some(path);
|
||||
}
|
||||
file.map(|(path, _)| path).cloned()
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||
let Ok(file) = fs::read_to_string(path) else {
|
||||
warn!("Couldn't Open File: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_file: DesktopFile = DesktopFile::new();
|
||||
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(value.to_string());
|
||||
});
|
||||
|
||||
/// Parses a desktop file into a flat hashmap of keys/values.
|
||||
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
||||
let file = File::open(path)?;
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for line in lines.flatten() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
Some(desktop_file)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
find_desktop_file(app_id).and_then(|file| {
|
||||
let map = parse_desktop_file(file);
|
||||
map.map_or(None, |map| {
|
||||
map.get("Icon").map(std::string::ToString::to_string)
|
||||
})
|
||||
})
|
||||
let Some(path) = find_desktop_file(app_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let desktop_file = match desktop_files_cache.get(&path) {
|
||||
Some(desktop_file) => desktop_file,
|
||||
_ => desktop_files_cache
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||
};
|
||||
|
||||
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||
|
||||
icons.next().map(std::string::ToString::to_string)
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{lock, send};
|
||||
use gtk::prelude::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
|
||||
/// A segment of a dynamic string,
|
||||
/// containing either a static string
|
||||
/// or a script.
|
||||
#[derive(Debug)]
|
||||
enum DynamicStringSegment {
|
||||
Static(String),
|
||||
Dynamic(Script),
|
||||
}
|
||||
|
||||
/// A string with embedded scripts for dynamic content.
|
||||
pub struct DynamicString;
|
||||
|
||||
impl DynamicString {
|
||||
/// Creates a new dynamic string, based off the input template.
|
||||
/// Runs `f` with the compiled string each time one of the scripts updates.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rs
|
||||
/// DynamicString::new(&text, move |string| {
|
||||
/// label.set_markup(&string);
|
||||
/// Continue(true)
|
||||
/// });
|
||||
/// ```
|
||||
pub fn new<F>(input: &str, f: F) -> Self
|
||||
where
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let segments = Self::parse_input(input);
|
||||
|
||||
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
for (i, segment) in segments.into_iter().enumerate() {
|
||||
match segment {
|
||||
DynamicStringSegment::Static(str) => {
|
||||
lock!(label_parts).push(str);
|
||||
}
|
||||
DynamicStringSegment::Dynamic(script) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
// insert blank value to preserve segment order
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, |out, _| {
|
||||
if let OutputStream::Stdout(out) = out {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||
|
||||
let string = label_parts.join("");
|
||||
send!(tx, string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize
|
||||
{
|
||||
let label_parts = lock!(label_parts).join("");
|
||||
send!(tx, label_parts);
|
||||
}
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
Self
|
||||
}
|
||||
|
||||
/// Parses the input string into static and dynamic segments
|
||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||
if !input.contains("{{") {
|
||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||
}
|
||||
|
||||
let mut segments = vec![];
|
||||
|
||||
let mut chars = input.chars().collect::<Vec<_>>();
|
||||
while !chars.is_empty() {
|
||||
let char_pair = if chars.len() > 1 {
|
||||
Some(&chars[..=1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (token, skip) = if let Some(['{', '{']) = char_pair {
|
||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||
|
||||
let str = chars
|
||||
.windows(2)
|
||||
.skip(2)
|
||||
.take_while(|win| win != &['}', '}'])
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(
|
||||
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
||||
len + SKIP_BRACKETS,
|
||||
)
|
||||
} else {
|
||||
let mut str = chars
|
||||
.windows(2)
|
||||
.take_while(|win| win != &['{', '{'])
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||
if chars.len() == str.len() + 1 {
|
||||
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||
str.push(remaining_char);
|
||||
}
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(DynamicStringSegment::Static(str), len)
|
||||
};
|
||||
|
||||
// quick runtime check to make sure the parser is working as expected
|
||||
assert_ne!(skip, 0);
|
||||
|
||||
segments.push(token);
|
||||
chars.drain(..skip);
|
||||
}
|
||||
|
||||
segments
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test() {
|
||||
// TODO: see if we can run gtk tests in ci
|
||||
if gtk::init().is_ok() {
|
||||
let label = gtk::Label::new(None);
|
||||
DynamicString::new(
|
||||
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
|
||||
move |string| {
|
||||
label.set_label(&string);
|
||||
Continue(true)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/dynamic_value/dynamic_bool.rs
Normal file
78
src/dynamic_value/dynamic_bool.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::Script;
|
||||
use crate::send;
|
||||
use cfg_if::cfg_if;
|
||||
use glib::Continue;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum DynamicBool {
|
||||
/// Either a script or variable, to be determined.
|
||||
Unknown(String),
|
||||
Script(Script),
|
||||
#[cfg(feature = "ipc")]
|
||||
Variable(Box<str>),
|
||||
}
|
||||
|
||||
impl DynamicBool {
|
||||
pub fn subscribe<F>(self, f: F)
|
||||
where
|
||||
F: FnMut(bool) -> Continue + 'static,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown(input) => {
|
||||
if input.starts_with('#') {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
Self::Variable(input.into())
|
||||
} else {
|
||||
Self::Unknown(input)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let script = Script::from(input.as_str());
|
||||
Self::Script(script)
|
||||
}
|
||||
}
|
||||
_ => self,
|
||||
};
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
spawn(async move {
|
||||
match value {
|
||||
DynamicBool::Script(script) => {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicBool::Variable(variable) => {
|
||||
let variable_manager = get_variable_manager();
|
||||
|
||||
let variable_name = variable[1..].into(); // remove hash
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||
send!(tx, has_value);
|
||||
}
|
||||
}
|
||||
DynamicBool::Unknown(_) => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a string ironvar is 'truthy'
|
||||
#[cfg(feature = "ipc")]
|
||||
fn is_truthy(string: &str) -> bool {
|
||||
!(string.is_empty() || string == "0" || string == "false")
|
||||
}
|
||||
321
src/dynamic_value/dynamic_string.rs
Normal file
321
src/dynamic_value/dynamic_string.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{arc_mut, lock, send};
|
||||
use gtk::prelude::*;
|
||||
use tokio::spawn;
|
||||
|
||||
/// A segment of a dynamic string,
|
||||
/// containing either a static string
|
||||
/// or a script.
|
||||
#[derive(Debug)]
|
||||
enum DynamicStringSegment {
|
||||
Static(String),
|
||||
Script(Script),
|
||||
#[cfg(feature = "ipc")]
|
||||
Variable(Box<str>),
|
||||
}
|
||||
|
||||
/// Creates a new dynamic string, based off the input template.
|
||||
/// Runs `f` with the compiled string each time one of the scripts or variables updates.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rs
|
||||
/// dynamic_string(&text, move |string| {
|
||||
/// label.set_markup(&string);
|
||||
/// Continue(true)
|
||||
/// });
|
||||
/// ```
|
||||
pub fn dynamic_string<F>(input: &str, f: F)
|
||||
where
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let tokens = parse_input(input);
|
||||
|
||||
let label_parts = arc_mut!(vec![]);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
for (i, segment) in tokens.into_iter().enumerate() {
|
||||
match segment {
|
||||
DynamicStringSegment::Static(str) => {
|
||||
lock!(label_parts).push(str);
|
||||
}
|
||||
DynamicStringSegment::Script(script) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
// insert blank value to preserve segment order
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, |out, _| {
|
||||
if let OutputStream::Stdout(out) = out {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||
|
||||
let string = label_parts.join("");
|
||||
send!(tx, string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicStringSegment::Variable(name) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
// insert blank value to preserve segment order
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
spawn(async move {
|
||||
let variable_manager = get_variable_manager();
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
if let Some(value) = value {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _: String = std::mem::replace(&mut label_parts[i], value);
|
||||
|
||||
let string = label_parts.join("");
|
||||
send!(tx, string);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
// initialize
|
||||
{
|
||||
let label_parts = lock!(label_parts).join("");
|
||||
send!(tx, label_parts);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the input string into static and dynamic segments
|
||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||
// short-circuit parser if it's all static
|
||||
if !input.contains("{{") && !input.contains('#') {
|
||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||
}
|
||||
|
||||
let mut tokens = vec![];
|
||||
|
||||
let mut chars = input.chars().collect::<Vec<_>>();
|
||||
while !chars.is_empty() {
|
||||
let char_pair = if chars.len() > 1 {
|
||||
Some(&chars[..=1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (token, skip) = match char_pair {
|
||||
Some(['{', '{']) => parse_script(&chars),
|
||||
Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2),
|
||||
#[cfg(feature = "ipc")]
|
||||
Some(['#', _]) => parse_variable(&chars),
|
||||
_ => parse_static(&chars),
|
||||
};
|
||||
|
||||
// quick runtime check to make sure the parser is working as expected
|
||||
assert_ne!(skip, 0);
|
||||
|
||||
tokens.push(token);
|
||||
chars.drain(..skip);
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||
|
||||
let str = chars
|
||||
.windows(2)
|
||||
.skip(2)
|
||||
.take_while(|win| win != &['}', '}'])
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_BRACKETS;
|
||||
let script = Script::from(str.as_str());
|
||||
|
||||
(DynamicStringSegment::Script(script), len)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
const SKIP_HASH: usize = 1;
|
||||
|
||||
let str = chars
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take_while(|&c| !c.is_whitespace())
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_HASH;
|
||||
let value = str.into();
|
||||
|
||||
(DynamicStringSegment::Variable(value), len)
|
||||
}
|
||||
|
||||
fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
let mut str = chars
|
||||
.windows(2)
|
||||
.take_while(|&win| win != ['{', '{'] && win[0] != '#')
|
||||
.map(|w| w[0])
|
||||
.collect::<String>();
|
||||
|
||||
let mut char_count = str.chars().count();
|
||||
|
||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||
if chars.len() == char_count + 1 {
|
||||
let remaining_char = *chars.get(char_count).expect("Failed to find last char");
|
||||
str.push(remaining_char);
|
||||
char_count += 1;
|
||||
}
|
||||
|
||||
(DynamicStringSegment::Static(str), char_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_static() {
|
||||
const INPUT: &str = "hello world";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_odd_char_count() {
|
||||
const INPUT: &str = "hello";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script() {
|
||||
const INPUT: &str = "{{echo hello}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable() {
|
||||
const INPUT: &str = "#variable";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script() {
|
||||
const INPUT: &str = "hello {{echo world}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_variable() {
|
||||
const INPUT: &str = "hello #subject";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script_static() {
|
||||
const INPUT: &str = "hello {{echo world}} foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_variable_static() {
|
||||
const INPUT: &str = "hello #subject foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_script_variable() {
|
||||
const INPUT: &str = "hello {{echo world}} #foo";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 4);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||
assert!(
|
||||
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||
);
|
||||
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
|
||||
assert!(
|
||||
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_hash() {
|
||||
const INPUT: &str = "number ###num";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 3);
|
||||
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
|
||||
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
|
||||
assert!(
|
||||
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_with_hash() {
|
||||
const INPUT: &str = "{{echo #hello}}";
|
||||
let tokens = parse_input(INPUT);
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(
|
||||
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/dynamic_value/mod.rs
Normal file
7
src/dynamic_value/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#![doc = include_str!("../../docs/Dynamic values.md")]
|
||||
|
||||
mod dynamic_bool;
|
||||
mod dynamic_string;
|
||||
|
||||
pub use dynamic_bool::DynamicBool;
|
||||
pub use dynamic_string::dynamic_string;
|
||||
@@ -2,7 +2,6 @@
|
||||
pub enum ExitCode {
|
||||
GtkDisplay = 1,
|
||||
CreateBars = 2,
|
||||
Config = 3,
|
||||
}
|
||||
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
|
||||
43
src/global_state.rs
Normal file
43
src/global_state.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::popup::Popup;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Global application state shared across all bars.
|
||||
///
|
||||
/// Data that needs to be accessed from anywhere
|
||||
/// that is not otherwise accessible should be placed on here.
|
||||
#[derive(Debug)]
|
||||
pub struct GlobalState {
|
||||
popups: HashMap<Box<str>, Rc<RefCell<Popup>>>,
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
popups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn popups(&self) -> &HashMap<Box<str>, Rc<RefCell<Popup>>> {
|
||||
&self.popups
|
||||
}
|
||||
|
||||
pub fn popups_mut(&mut self) -> &mut HashMap<Box<str>, Rc<RefCell<Popup>>> {
|
||||
&mut self.popups
|
||||
}
|
||||
|
||||
pub fn with_popup_mut<F, T>(&self, monitor_name: &str, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(RefMut<Popup>) -> T,
|
||||
{
|
||||
let popup = self.popups().get(monitor_name);
|
||||
|
||||
if let Some(popup) = popup {
|
||||
let popup = popup.borrow_mut();
|
||||
Some(f(popup))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,77 @@
|
||||
use glib::IsA;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Widget;
|
||||
use gtk::{Orientation, Widget};
|
||||
|
||||
/// Adds a new CSS class to a widget.
|
||||
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
|
||||
widget.style_context().add_class(class);
|
||||
/// Represents a widget's size
|
||||
/// and location relative to the bar's start edge.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct WidgetGeometry {
|
||||
/// Position of the start edge of the widget
|
||||
/// from the start edge of the bar.
|
||||
pub position: i32,
|
||||
/// The length of the widget.
|
||||
pub size: i32,
|
||||
/// The length of the bar.
|
||||
pub bar_size: i32,
|
||||
}
|
||||
|
||||
pub trait IronbarGtkExt {
|
||||
/// Adds a new CSS class to the widget.
|
||||
fn add_class(&self, class: &str);
|
||||
/// Gets the geometry for the widget
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
||||
|
||||
/// Gets a data tag on a widget, if it exists.
|
||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
|
||||
/// Sets a data tag on a widget.
|
||||
fn set_tag<V: 'static>(&self, key: &str, value: V);
|
||||
}
|
||||
|
||||
impl<W: IsA<Widget>> IronbarGtkExt for W {
|
||||
fn add_class(&self, class: &str) {
|
||||
self.style_context().add_class(class);
|
||||
}
|
||||
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
|
||||
let allocation = self.allocation();
|
||||
|
||||
let widget_size = if orientation == Orientation::Horizontal {
|
||||
allocation.width()
|
||||
} else {
|
||||
allocation.height()
|
||||
};
|
||||
|
||||
let top_level = self.toplevel().expect("Failed to get top-level widget");
|
||||
let top_level_allocation = top_level.allocation();
|
||||
|
||||
let bar_size = if orientation == Orientation::Horizontal {
|
||||
top_level_allocation.width()
|
||||
} else {
|
||||
top_level_allocation.height()
|
||||
};
|
||||
|
||||
let (widget_x, widget_y) = self
|
||||
.translate_coordinates(&top_level, 0, 0)
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
let widget_pos = if orientation == Orientation::Horizontal {
|
||||
widget_x
|
||||
} else {
|
||||
widget_y
|
||||
};
|
||||
|
||||
WidgetGeometry {
|
||||
position: widget_pos,
|
||||
size: widget_size,
|
||||
bar_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
|
||||
unsafe { self.data(key).map(|val| val.as_ref()) }
|
||||
}
|
||||
|
||||
fn set_tag<V: 'static>(&self, key: &str, value: V) {
|
||||
unsafe { self.set_data(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
|
||||
@@ -9,10 +9,10 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
add_class(&image, "image");
|
||||
add_class(&image, "icon");
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, size)
|
||||
match ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Some(_) => {
|
||||
@@ -36,17 +36,17 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
add_class(&image, "icon");
|
||||
add_class(&image, "image");
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
ImageProvider::parse(input, icon_theme, size)
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
add_class(&label, "icon");
|
||||
add_class(&label, "text-icon");
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
@@ -41,23 +41,44 @@ impl<'a> ImageProvider<'a> {
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size)?;
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
||||
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
|
||||
fn get_location(
|
||||
input: &str,
|
||||
theme: &'a IconTheme,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
recurse_depth: usize,
|
||||
) -> Option<ImageLocation<'a>> {
|
||||
macro_rules! fallback {
|
||||
() => {
|
||||
if use_fallback {
|
||||
Some(Self::get_fallback_icon(theme))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_RECURSE_DEPTH: usize = 2;
|
||||
|
||||
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
|
||||
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
@@ -92,21 +113,26 @@ impl<'a> ImageProvider<'a> {
|
||||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
None
|
||||
fallback!()
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name)
|
||||
.map(|input| Self::get_location(&input, theme, size))
|
||||
{
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
||||
None if should_parse_desktop_file => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
||||
}) {
|
||||
location
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
None
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,4 +274,11 @@ impl<'a> ImageProvider<'a> {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
|
||||
ImageLocation::Icon {
|
||||
name: "dialog-question-symbolic".to_string(),
|
||||
theme,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/ipc/client.rs
Normal file
28
src/ipc/client.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use super::Ipc;
|
||||
use crate::ipc::{Command, Response};
|
||||
use color_eyre::Result;
|
||||
use color_eyre::{Help, Report};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
impl Ipc {
|
||||
/// Sends a command to the IPC server.
|
||||
/// The server response is returned.
|
||||
pub async fn send(&self, command: Command) -> Result<Response> {
|
||||
let mut stream = match UnixStream::connect(&self.path).await {
|
||||
Ok(stream) => Ok(stream),
|
||||
Err(err) => Err(Report::new(err)
|
||||
.wrap_err("Failed to connect to Ironbar IPC server")
|
||||
.suggestion("Is Ironbar running?")),
|
||||
}?;
|
||||
|
||||
let write_buffer = serde_json::to_vec(&command)?;
|
||||
stream.write_all(&write_buffer).await?;
|
||||
|
||||
let mut read_buffer = vec![0; 1024];
|
||||
let bytes = stream.read(&mut read_buffer).await?;
|
||||
|
||||
let response = serde_json::from_slice(&read_buffer[..bytes])?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
79
src/ipc/commands.rs
Normal file
79
src/ipc/commands.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Command {
|
||||
/// Return "ok"
|
||||
Ping,
|
||||
|
||||
/// Open the GTK inspector
|
||||
Inspect,
|
||||
|
||||
/// Reload the config
|
||||
Reload,
|
||||
|
||||
/// Set an `ironvar` value.
|
||||
/// This creates it if it does not already exist, and updates it if it does.
|
||||
/// Any references to this variable are automatically and immediately updated.
|
||||
/// Keys and values can be any valid UTF-8 string.
|
||||
Set {
|
||||
/// Variable key. Can be any alphanumeric ASCII string.
|
||||
key: Box<str>,
|
||||
/// Variable value. Can be any valid UTF-8 string.
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Get the current value of an `ironvar`.
|
||||
Get {
|
||||
/// Variable key.
|
||||
key: Box<str>,
|
||||
},
|
||||
|
||||
/// Load an additional CSS stylesheet.
|
||||
/// The sheet is automatically hot-reloaded.
|
||||
LoadCss {
|
||||
/// The path to the sheet.
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Set the visibility of the bar with the given name.
|
||||
SetVisible {
|
||||
///Bar name to target.
|
||||
bar_name: String,
|
||||
/// The visibility status.
|
||||
#[arg(short, long)]
|
||||
visible: bool,
|
||||
},
|
||||
|
||||
/// Get the visibility of the bar with the given name.
|
||||
GetVisible {
|
||||
/// Bar name to target.
|
||||
bar_name: String,
|
||||
},
|
||||
|
||||
/// Toggle a popup open/closed.
|
||||
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
|
||||
TogglePopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
/// The name of the widget.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Open a popup, regardless of current state.
|
||||
OpenPopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
/// The name of the widget.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Close a popup, regardless of current state.
|
||||
ClosePopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_name: String,
|
||||
},
|
||||
}
|
||||
42
src/ipc/mod.rs
Normal file
42
src/ipc/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
mod client;
|
||||
pub mod commands;
|
||||
pub mod responses;
|
||||
mod server;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::GlobalState;
|
||||
pub use commands::Command;
|
||||
pub use responses::Response;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ipc {
|
||||
path: PathBuf,
|
||||
global_state: Rc<RefCell<GlobalState>>,
|
||||
}
|
||||
|
||||
impl Ipc {
|
||||
/// Creates a new IPC instance.
|
||||
/// This can be used as both a server and client.
|
||||
pub fn new(global_state: Rc<RefCell<GlobalState>>) -> Self {
|
||||
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
|
||||
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
|
||||
.join("ironbar-ipc.sock");
|
||||
|
||||
if format!("{}", ipc_socket_file.display()).len() > 100 {
|
||||
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
|
||||
}
|
||||
|
||||
Self {
|
||||
path: ipc_socket_file,
|
||||
global_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
18
src/ipc/responses.rs
Normal file
18
src/ipc/responses.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
OkValue { value: String },
|
||||
Err { message: Option<String> },
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response::Error`.
|
||||
pub fn error(message: &str) -> Self {
|
||||
Self::Err {
|
||||
message: Some(message.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
256
src/ipc/server.rs
Normal file
256
src/ipc/server.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use color_eyre::{Report, Result};
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::ipc::{Command, Response};
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::modules::PopupButton;
|
||||
use crate::style::load_css;
|
||||
use crate::{await_sync, read_lock, send_async, try_send, write_lock, GlobalState};
|
||||
|
||||
use super::Ipc;
|
||||
|
||||
impl Ipc {
|
||||
/// Starts the IPC server on its socket.
|
||||
///
|
||||
/// Once started, the server will begin accepting connections.
|
||||
pub fn start(&self, application: &Application) {
|
||||
let bridge = BridgeChannel::<Command>::new();
|
||||
let cmd_tx = bridge.create_sender();
|
||||
let (res_tx, mut res_rx) = mpsc::channel(32);
|
||||
|
||||
let path = self.path.clone();
|
||||
|
||||
if path.exists() {
|
||||
warn!("Socket already exists. Did Ironbar exit abruptly?");
|
||||
warn!("Attempting IPC shutdown to allow binding to address");
|
||||
Self::shutdown(&path);
|
||||
}
|
||||
|
||||
spawn(async move {
|
||||
info!("Starting IPC on {}", path.display());
|
||||
|
||||
let listener = match UnixListener::bind(&path) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Unable to start IPC server")
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, _addr)) => {
|
||||
if let Err(err) =
|
||||
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
|
||||
{
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let application = application.clone();
|
||||
let global_state = self.global_state.clone();
|
||||
bridge.recv(move |command| {
|
||||
let res = Self::handle_command(command, &application, &global_state);
|
||||
try_send!(res_tx, res);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
/// Takes an incoming connections,
|
||||
/// reads the command message, and sends the response.
|
||||
///
|
||||
/// The connection is closed once the response has been written.
|
||||
async fn handle_connection(
|
||||
mut stream: UnixStream,
|
||||
cmd_tx: &Sender<Command>,
|
||||
res_rx: &mut Receiver<Response>,
|
||||
) -> Result<()> {
|
||||
let (mut stream_read, mut stream_write) = stream.split();
|
||||
|
||||
let mut read_buffer = vec![0; 1024];
|
||||
let bytes = stream_read.read(&mut read_buffer).await?;
|
||||
|
||||
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
|
||||
|
||||
debug!("Received command: {command:?}");
|
||||
|
||||
send_async!(cmd_tx, command);
|
||||
let res = res_rx
|
||||
.recv()
|
||||
.await
|
||||
.unwrap_or(Response::Err { message: None });
|
||||
let res = serde_json::to_vec(&res)?;
|
||||
|
||||
stream_write.write_all(&res).await?;
|
||||
stream_write.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes an input command, runs it and returns with the appropriate response.
|
||||
///
|
||||
/// This runs on the main thread, allowing commands to interact with GTK.
|
||||
fn handle_command(
|
||||
command: Command,
|
||||
application: &Application,
|
||||
global_state: &Rc<RefCell<GlobalState>>,
|
||||
) -> Response {
|
||||
match command {
|
||||
Command::Inspect => {
|
||||
gtk::Window::set_interactive_debugging(true);
|
||||
Response::Ok
|
||||
}
|
||||
Command::Reload => {
|
||||
await_sync(async move { crate::reload(application, global_state).await }).unwrap();
|
||||
Response::Ok
|
||||
}
|
||||
Command::Set { key, value } => {
|
||||
let variable_manager = get_variable_manager();
|
||||
let mut variable_manager = write_lock!(variable_manager);
|
||||
match variable_manager.set(key, value) {
|
||||
Ok(_) => Response::Ok,
|
||||
Err(err) => Response::error(&format!("{err}")),
|
||||
}
|
||||
}
|
||||
Command::Get { key } => {
|
||||
let variable_manager = get_variable_manager();
|
||||
let value = read_lock!(variable_manager).get(&key);
|
||||
match value {
|
||||
Some(value) => Response::OkValue { value },
|
||||
None => Response::error("Variable not found"),
|
||||
}
|
||||
}
|
||||
Command::LoadCss { path } => {
|
||||
if path.exists() {
|
||||
load_css(path);
|
||||
Response::Ok
|
||||
} else {
|
||||
Response::error("File not found")
|
||||
}
|
||||
}
|
||||
Command::TogglePopup { bar_name, name } => {
|
||||
let global_state = global_state.borrow();
|
||||
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
|
||||
let current_widget = popup.current_widget();
|
||||
popup.hide();
|
||||
|
||||
let data = popup
|
||||
.cache
|
||||
.iter()
|
||||
.find(|(_, (module_name, _))| module_name == &name)
|
||||
.map(|module| (module, module.1 .1.buttons.first()));
|
||||
|
||||
match data {
|
||||
Some(((&id, _), Some(button))) if current_widget != Some(id) => {
|
||||
let button_id = button.popup_id();
|
||||
popup.show(id, button_id);
|
||||
|
||||
Response::Ok
|
||||
}
|
||||
Some((_, None)) => Response::error("Module has no popup functionality"),
|
||||
Some(_) => Response::Ok,
|
||||
None => Response::error("Invalid module name"),
|
||||
}
|
||||
});
|
||||
|
||||
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
|
||||
}
|
||||
Command::OpenPopup { bar_name, name } => {
|
||||
let global_state = global_state.borrow();
|
||||
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
|
||||
// only one popup per bar, so hide if open for another widget
|
||||
popup.hide();
|
||||
|
||||
let data = popup
|
||||
.cache
|
||||
.iter()
|
||||
.find(|(_, (module_name, _))| module_name == &name)
|
||||
.map(|module| (module, module.1 .1.buttons.first()));
|
||||
|
||||
match data {
|
||||
Some(((&id, _), Some(button))) => {
|
||||
let button_id = button.popup_id();
|
||||
popup.show(id, button_id);
|
||||
|
||||
Response::Ok
|
||||
}
|
||||
Some((_, None)) => Response::error("Module has no popup functionality"),
|
||||
None => Response::error("Invalid module name"),
|
||||
}
|
||||
});
|
||||
|
||||
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
|
||||
}
|
||||
Command::ClosePopup { bar_name } => {
|
||||
let global_state = global_state.borrow();
|
||||
let popup_found = global_state
|
||||
.with_popup_mut(&bar_name, |mut popup| popup.hide())
|
||||
.is_some();
|
||||
|
||||
if popup_found {
|
||||
Response::Ok
|
||||
} else {
|
||||
Response::error("Invalid monitor name")
|
||||
}
|
||||
}
|
||||
Command::Ping => Response::Ok,
|
||||
Command::SetVisible { bar_name, visible } => {
|
||||
let windows = application.windows();
|
||||
let found = windows
|
||||
.iter()
|
||||
.find(|window| window.widget_name() == bar_name);
|
||||
|
||||
if let Some(window) = found {
|
||||
window.set_visible(visible);
|
||||
Response::Ok
|
||||
} else {
|
||||
Response::error("Bar not found")
|
||||
}
|
||||
}
|
||||
Command::GetVisible { bar_name } => {
|
||||
let windows = application.windows();
|
||||
let found = windows
|
||||
.iter()
|
||||
.find(|window| window.widget_name() == bar_name);
|
||||
|
||||
if let Some(window) = found {
|
||||
Response::OkValue {
|
||||
value: window.is_visible().to_string(),
|
||||
}
|
||||
} else {
|
||||
Response::error("Bar not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shuts down the IPC server,
|
||||
/// removing the socket file in the process.
|
||||
///
|
||||
/// Note this is static as the `Ipc` struct is not `Send`.
|
||||
pub fn shutdown<P: AsRef<Path>>(path: P) {
|
||||
fs::remove_file(&path).ok();
|
||||
}
|
||||
}
|
||||
107
src/ironvar.rs
Normal file
107
src/ironvar.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
#![doc = include_str!("../docs/Ironvars.md")]
|
||||
|
||||
use crate::{arc_rw, send};
|
||||
use color_eyre::{Report, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
lazy_static! {
|
||||
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
|
||||
}
|
||||
|
||||
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
|
||||
VARIABLE_MANAGER.clone()
|
||||
}
|
||||
|
||||
/// Global singleton manager for `IronVar` variables.
|
||||
pub struct VariableManager {
|
||||
variables: HashMap<Box<str>, IronVar>,
|
||||
}
|
||||
|
||||
impl VariableManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value for a variable,
|
||||
/// creating it if it does not exist.
|
||||
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
|
||||
if Self::key_is_valid(&key) {
|
||||
if let Some(var) = self.variables.get_mut(&key) {
|
||||
var.set(Some(value));
|
||||
} else {
|
||||
let var = IronVar::new(Some(value));
|
||||
self.variables.insert(key, var);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Report::msg("Invalid key"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current value of an `ironvar`.
|
||||
/// Prefer to use `subscribe` where possible.
|
||||
pub fn get(&self, key: &str) -> Option<String> {
|
||||
self.variables.get(key).and_then(IronVar::get)
|
||||
}
|
||||
|
||||
/// Subscribes to an `ironvar`, creating it if it does not exist.
|
||||
/// Any time the var is set, its value is sent on the channel.
|
||||
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
||||
self.variables
|
||||
.entry(key)
|
||||
.or_insert_with(|| IronVar::new(None))
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
fn key_is_valid(key: &str) -> bool {
|
||||
!key.is_empty()
|
||||
&& key
|
||||
.chars()
|
||||
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
|
||||
}
|
||||
}
|
||||
|
||||
/// Ironbar dynamic variable representation.
|
||||
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
|
||||
#[derive(Debug)]
|
||||
struct IronVar {
|
||||
value: Option<String>,
|
||||
tx: broadcast::Sender<Option<String>>,
|
||||
_rx: broadcast::Receiver<Option<String>>,
|
||||
}
|
||||
|
||||
impl IronVar {
|
||||
/// Creates a new variable.
|
||||
fn new(value: Option<String>) -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
||||
Self { value, tx, _rx: rx }
|
||||
}
|
||||
|
||||
/// Gets the current variable value.
|
||||
/// Prefer to subscribe to changes where possible.
|
||||
fn get(&self) -> Option<String> {
|
||||
self.value.clone()
|
||||
}
|
||||
|
||||
/// Sets the current variable value.
|
||||
/// The change is broadcast to all receivers.
|
||||
fn set(&mut self, value: Option<String>) {
|
||||
self.value = value.clone();
|
||||
send!(self.tx, value);
|
||||
}
|
||||
|
||||
/// Subscribes to the variable.
|
||||
/// The latest value is immediately sent to all receivers.
|
||||
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
|
||||
let rx = self.tx.subscribe();
|
||||
send!(self.tx, self.value.clone());
|
||||
rx
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Sends a message on an asynchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// send_async!(tx, "my message");
|
||||
@@ -16,7 +16,7 @@ macro_rules! send_async {
|
||||
/// Sends a message on an synchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// send!(tx, "my message");
|
||||
@@ -31,7 +31,7 @@ macro_rules! send {
|
||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// try_send!(tx, "my message");
|
||||
@@ -46,7 +46,7 @@ macro_rules! try_send {
|
||||
/// Locks a `Mutex`.
|
||||
/// Panics if the `Mutex` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut val = lock!(my_mutex);
|
||||
@@ -62,7 +62,7 @@ macro_rules! lock {
|
||||
/// Gets a read lock on a `RwLock`.
|
||||
/// Panics if the `RwLock` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = read_lock!(my_rwlock);
|
||||
@@ -77,7 +77,7 @@ macro_rules! read_lock {
|
||||
/// Gets a write lock on a `RwLock`.
|
||||
/// Panics if the `RwLock` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut val = write_lock!(my_rwlock);
|
||||
@@ -88,3 +88,33 @@ macro_rules! write_lock {
|
||||
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
||||
};
|
||||
}
|
||||
|
||||
/// Wraps `val` in a new `Arc<Mutex<T>>`.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = arc_mut!(MyService::new());
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! arc_mut {
|
||||
($val:expr) => {
|
||||
std::sync::Arc::new(std::sync::Mutex::new($val))
|
||||
};
|
||||
}
|
||||
|
||||
/// Wraps `val` in a new `Arc<RwLock<T>>`.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = arc_rw!(MyService::new());
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! arc_rw {
|
||||
($val:expr) => {
|
||||
std::sync::Arc::new(std::sync::RwLock::new($val))
|
||||
};
|
||||
}
|
||||
|
||||
310
src/main.rs
310
src/main.rs
@@ -1,14 +1,56 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use dirs::config_dir;
|
||||
use gtk::gdk::{Display, Monitor};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use smithay_client_toolkit::output::OutputInfo;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::spawn;
|
||||
use tokio::task::{block_in_place, spawn_blocking};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use universal_config::ConfigLoader;
|
||||
|
||||
use clients::wayland;
|
||||
|
||||
use crate::bar::create_bar;
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::cached_broadcast::Event;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::error::ExitCode;
|
||||
use crate::global_state::GlobalState;
|
||||
use crate::style::load_css;
|
||||
|
||||
mod bar;
|
||||
mod bridge_channel;
|
||||
mod cached_broadcast;
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli;
|
||||
mod clients;
|
||||
mod config;
|
||||
mod desktop_file;
|
||||
mod dynamic_string;
|
||||
mod dynamic_value;
|
||||
mod error;
|
||||
mod global_state;
|
||||
mod gtk_helpers;
|
||||
mod image;
|
||||
#[cfg(feature = "ipc")]
|
||||
mod ipc;
|
||||
#[cfg(feature = "ipc")]
|
||||
mod ironvar;
|
||||
mod logging;
|
||||
mod macros;
|
||||
mod modules;
|
||||
@@ -17,29 +59,6 @@ mod script;
|
||||
mod style;
|
||||
mod unique_id;
|
||||
|
||||
use crate::bar::create_bar;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::style::load_css;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use dirs::config_dir;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use std::cell::Cell;
|
||||
use std::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::rc::Rc;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::block_in_place;
|
||||
|
||||
use crate::error::ExitCode;
|
||||
use clients::wayland::{self, WaylandClient};
|
||||
use tracing::{debug, error, info};
|
||||
use universal_config::ConfigLoader;
|
||||
|
||||
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -47,15 +66,45 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
async fn main() {
|
||||
let _guard = logging::install_logging();
|
||||
|
||||
let global_state = Rc::new(RefCell::new(GlobalState::new()));
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "cli")] {
|
||||
run_with_args(global_state).await;
|
||||
} else {
|
||||
start_ironbar(global_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
async fn run_with_args(global_state: Rc<RefCell<GlobalState>>) {
|
||||
let args = cli::Args::parse();
|
||||
|
||||
match args.command {
|
||||
Some(command) => {
|
||||
let ipc = ipc::Ipc::new(global_state);
|
||||
match ipc.send(command).await {
|
||||
Ok(res) => cli::handle_response(res),
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
}
|
||||
None => start_ironbar(global_state).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
|
||||
info!("Ironbar version {}", VERSION);
|
||||
info!("Starting application");
|
||||
|
||||
let wayland_client = wayland::get_client().await;
|
||||
|
||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||
|
||||
let output_bridge = BridgeChannel::<Event<OutputInfo>>::new();
|
||||
let output_tx = output_bridge.create_sender();
|
||||
|
||||
let running = Rc::new(Cell::new(false));
|
||||
|
||||
let global_state2 = global_state.clone();
|
||||
app.connect_activate(move |app| {
|
||||
if running.get() {
|
||||
info!("Ironbar already running, returning");
|
||||
@@ -64,37 +113,13 @@ async fn main() {
|
||||
|
||||
running.set(true);
|
||||
|
||||
let display = Display::default().map_or_else(
|
||||
|| {
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
error!("{:?}", report);
|
||||
exit(ExitCode::GtkDisplay as i32)
|
||||
},
|
||||
|display| display,
|
||||
);
|
||||
|
||||
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|
||||
|_| ConfigLoader::new("ironbar").find_and_load(),
|
||||
ConfigLoader::load,
|
||||
);
|
||||
|
||||
let config = match config_res {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::Config as i32)
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
let ipc = ipc::Ipc::new(global_state2.clone());
|
||||
ipc.start(app);
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Loaded config file");
|
||||
|
||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::CreateBars as i32);
|
||||
}
|
||||
|
||||
debug!("Created bars");
|
||||
|
||||
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
||||
|| {
|
||||
config_dir().map_or_else(
|
||||
@@ -112,67 +137,162 @@ async fn main() {
|
||||
if style_path.exists() {
|
||||
load_css(style_path);
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
let ipc_path = ipc.path().to_path_buf();
|
||||
spawn_blocking(move || {
|
||||
rx.recv().expect("to receive from channel");
|
||||
|
||||
info!("Shutting down");
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
ipc::Ipc::shutdown(ipc_path);
|
||||
|
||||
exit(0);
|
||||
});
|
||||
|
||||
let wc = wayland::get_client();
|
||||
|
||||
let output_tx = output_tx.clone();
|
||||
let mut output_rx = lock!(wc).subscribe_outputs();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = output_rx.recv().await {
|
||||
try_send!(output_tx.clone(), event);
|
||||
}
|
||||
});
|
||||
|
||||
ctrlc::set_handler(move || send!(tx, ())).expect("Error setting Ctrl-C handler");
|
||||
});
|
||||
|
||||
let config = load_config();
|
||||
|
||||
{
|
||||
let app = app.clone();
|
||||
let global_state = global_state.clone();
|
||||
|
||||
output_bridge.recv(move |event: cached_broadcast::Event<_>| {
|
||||
let display = get_display();
|
||||
match event {
|
||||
Event::Add(output) => {
|
||||
debug!("Adding bar(s) for monitor {:?}", &output.name);
|
||||
create_bars_for_monitor(&app, &display, &output, config.clone(), &global_state)
|
||||
.unwrap();
|
||||
}
|
||||
// TODO: Implement
|
||||
Event::Remove(_) => {}
|
||||
Event::Replace(_, _) => {}
|
||||
}
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
// Ignore CLI args
|
||||
// Some are provided by swaybar_config but not currently supported
|
||||
app.run_with_args(&Vec::<&str>::new());
|
||||
|
||||
info!("Shutting down");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/// Creates each of the bars across each of the (configured) outputs.
|
||||
fn create_bars(
|
||||
app: &Application,
|
||||
display: &Display,
|
||||
wl: &WaylandClient,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
/// Closes all current bars and entirely reloads Ironbar.
|
||||
/// This re-reads the config file.
|
||||
pub async fn reload(app: &Application, global_state: &Rc<RefCell<GlobalState>>) -> Result<()> {
|
||||
info!("Closing existing bars");
|
||||
let windows = app.windows();
|
||||
for window in windows {
|
||||
window.close();
|
||||
}
|
||||
|
||||
let config = load_config();
|
||||
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
let outputs = wl.get_outputs();
|
||||
|
||||
debug!("Received {} outputs from Wayland", outputs.len());
|
||||
debug!("Outputs: {:?}", outputs);
|
||||
let display = get_display();
|
||||
for output in outputs.iter() {
|
||||
create_bars_for_monitor(app, &display, output, config.clone(), global_state)?;
|
||||
}
|
||||
|
||||
let num_monitors = display.n_monitors();
|
||||
Ok(())
|
||||
}
|
||||
fn create_bars_for_monitor(
|
||||
app: &Application,
|
||||
display: &Display,
|
||||
output: &OutputInfo,
|
||||
config: Config,
|
||||
global_state: &Rc<RefCell<GlobalState>>,
|
||||
) -> Result<()> {
|
||||
let Some(monitor_name) = &output.name else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for i in 0..num_monitors {
|
||||
let monitor = display
|
||||
.monitor(i)
|
||||
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
||||
let output = outputs
|
||||
.get(i as usize)
|
||||
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
||||
let monitor = match get_monitor(&monitor_name, display) {
|
||||
Ok(monitor) => monitor,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let Some(monitor_name) = &output.name else { continue };
|
||||
let Some(monitor_config) = config.get_monitor_config(&monitor_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
config.monitors.as_ref().map_or_else(
|
||||
match monitor_config {
|
||||
MonitorConfig::Single(config) => {
|
||||
create_bar(&app, &monitor, &monitor_name, config, global_state)
|
||||
}
|
||||
MonitorConfig::Multiple(configs) => configs
|
||||
.into_iter()
|
||||
.map(|config| create_bar(&app, &monitor, &monitor_name, config, global_state))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
fn get_display() -> Display {
|
||||
Display::default().map_or_else(
|
||||
|| {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
error!("{:?}", report);
|
||||
exit(ExitCode::GtkDisplay as i32)
|
||||
},
|
||||
|config| {
|
||||
let config = config.get(monitor_name);
|
||||
match &config {
|
||||
Some(MonitorConfig::Single(config)) => {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())
|
||||
}
|
||||
Some(MonitorConfig::Multiple(configs)) => {
|
||||
for config in configs {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())?;
|
||||
|display| display,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn get_monitor(name: &str, display: &Display) -> Result<Monitor> {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
let outputs = wl.get_outputs();
|
||||
|
||||
let monitor = (0..display.n_monitors()).into_iter().find_map(|i| {
|
||||
let monitor = display.monitor(i)?;
|
||||
let output = outputs.get(i as usize)?;
|
||||
|
||||
let is_match = output.name.as_ref().map(|n| n == name).unwrap_or_default();
|
||||
if is_match {
|
||||
Some(monitor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
},
|
||||
)?;
|
||||
});
|
||||
|
||||
monitor.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn load_config() -> Config {
|
||||
let config = env::var("IRONBAR_CONFIG")
|
||||
.map_or_else(
|
||||
|_| ConfigLoader::new("ironbar").find_and_load(),
|
||||
ConfigLoader::load,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to load config: {}", err);
|
||||
warn!("Falling back to the default config");
|
||||
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
|
||||
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
|
||||
|
||||
Config::default()
|
||||
});
|
||||
|
||||
debug!("Loaded config file");
|
||||
config
|
||||
}
|
||||
|
||||
/// Blocks on a `Future` until it resolves.
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::clients::clipboard::{self, ClipboardEvent};
|
||||
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
||||
};
|
||||
use crate::try_send;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
@@ -111,7 +112,7 @@ impl Module<Button> for ClipboardModule {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let client = clipboard::get_client();
|
||||
match event {
|
||||
UIEvent::Copy(id) => client.copy(id).await,
|
||||
UIEvent::Copy(id) => client.copy(id),
|
||||
UIEvent::Remove(id) => client.remove(id),
|
||||
}
|
||||
}
|
||||
@@ -124,25 +125,26 @@ impl Module<Button> for ClipboardModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> color_eyre::Result<ModuleWidget<Button>> {
|
||||
let position = info.bar_position;
|
||||
|
||||
) -> color_eyre::Result<ModuleParts<Button>> {
|
||||
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
||||
button.style_context().add_class("btn");
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
let pos = Popup::widget_geometry(button, position.get_orientation());
|
||||
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(button.popup_id())
|
||||
);
|
||||
});
|
||||
|
||||
// we need to bind to the receiver as the channel does not open
|
||||
// until the popup is first opened.
|
||||
context.widget_rx.attach(None, |_| Continue(true));
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
|
||||
})
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{send_async, try_send};
|
||||
use chrono::{DateTime, Local};
|
||||
use std::env;
|
||||
|
||||
use chrono::{DateTime, Local, Locale};
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
@@ -13,6 +10,13 @@ use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
||||
};
|
||||
use crate::{send_async, try_send};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ClockModule {
|
||||
/// Date/time format string.
|
||||
@@ -23,14 +27,48 @@ pub struct ClockModule {
|
||||
#[serde(default = "default_format")]
|
||||
format: String,
|
||||
|
||||
#[serde(default = "default_popup_format")]
|
||||
format_popup: String,
|
||||
|
||||
#[serde(default = "default_locale")]
|
||||
locale: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
impl Default for ClockModule {
|
||||
fn default() -> Self {
|
||||
ClockModule {
|
||||
format: default_format(),
|
||||
format_popup: default_popup_format(),
|
||||
locale: default_locale(),
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_format() -> String {
|
||||
String::from("%d/%m/%Y %H:%M")
|
||||
}
|
||||
|
||||
fn default_popup_format() -> String {
|
||||
String::from("%H:%M:%S")
|
||||
}
|
||||
|
||||
fn default_locale() -> String {
|
||||
env::var("LC_TIME")
|
||||
.or_else(|_| env::var("LANG"))
|
||||
.map_or_else(|_| "POSIX".to_string(), strip_tail)
|
||||
}
|
||||
|
||||
fn strip_tail(string: String) -> String {
|
||||
string
|
||||
.split_once('.')
|
||||
.map(|(head, _)| head.to_string())
|
||||
.unwrap_or(string)
|
||||
}
|
||||
|
||||
impl Module<Button> for ClockModule {
|
||||
type SendMessage = DateTime<Local>;
|
||||
type ReceiveMessage = ();
|
||||
@@ -60,35 +98,33 @@ impl Module<Button> for ClockModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let button = Button::new();
|
||||
let label = Label::new(None);
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
button.add(&label);
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||
ModuleUpdateEvent::TogglePopup(button.popup_id())
|
||||
);
|
||||
});
|
||||
|
||||
let format = self.format.clone();
|
||||
{
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
context.widget_rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(&format));
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
label.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup,
|
||||
})
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
@@ -100,22 +136,22 @@ impl Module<Button> for ClockModule {
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
let clock = Label::builder().halign(Align::Center).build();
|
||||
add_class(&clock, "calendar-clock");
|
||||
let format = "%H:%M:%S";
|
||||
clock.add_class("calendar-clock");
|
||||
|
||||
container.add(&clock);
|
||||
|
||||
let calendar = Calendar::new();
|
||||
add_class(&calendar, "calendar");
|
||||
calendar.add_class("calendar");
|
||||
container.add(&calendar);
|
||||
|
||||
{
|
||||
let format = self.format_popup;
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(format));
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
clock.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
for widget in widgets {
|
||||
widget.widget.add_to(&container, context, widget.common);
|
||||
widget.widget.add_to(&container, &context, widget.common);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::popup::Popup;
|
||||
use crate::{build, try_send};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Label};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::PopupButton;
|
||||
use crate::{build, try_send};
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ButtonWidget {
|
||||
name: Option<String>,
|
||||
@@ -19,20 +21,20 @@ impl CustomWidget for ButtonWidget {
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let button = build!(self, Self::Widget);
|
||||
context.popup_buttons.borrow_mut().push(button.clone());
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
button.add(&label);
|
||||
|
||||
DynamicString::new(&text, move |string| {
|
||||
dynamic_string(&text, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(exec) = self.on_click {
|
||||
let bar_orientation = context.bar_orientation;
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
@@ -41,7 +43,7 @@ impl CustomWidget for ButtonWidget {
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
args: None,
|
||||
geometry: Popup::widget_geometry(button, bar_orientation),
|
||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::image::ImageProvider;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Image;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::image::ImageProvider;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ImageWidget {
|
||||
name: Option<String>,
|
||||
@@ -29,8 +31,8 @@ impl CustomWidget for ImageWidget {
|
||||
let gtk_image = gtk_image.clone();
|
||||
let icon_theme = context.icon_theme.clone();
|
||||
|
||||
DynamicString::new(&self.src, move |src| {
|
||||
ImageProvider::parse(&src, &icon_theme, self.size)
|
||||
dynamic_string(&self.src, move |src| {
|
||||
ImageProvider::parse(&src, &icon_theme, false, self.size)
|
||||
.map(|image| image.load_into_image(gtk_image.clone()));
|
||||
|
||||
Continue(true)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LabelWidget {
|
||||
name: Option<String>,
|
||||
@@ -22,7 +24,7 @@ impl CustomWidget for LabelWidget {
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
DynamicString::new(&self.label, move |string| {
|
||||
dynamic_string(&self.label, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
@@ -13,15 +13,16 @@ use crate::config::CommonConfig;
|
||||
use crate::modules::custom::button::ButtonWidget;
|
||||
use crate::modules::custom::progress::ProgressWidget;
|
||||
use crate::modules::{
|
||||
wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
|
||||
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::popup::WidgetGeometry;
|
||||
use crate::script::Script;
|
||||
use crate::send_async;
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Orientation};
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use serde::Deserialize;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
@@ -56,11 +57,12 @@ pub enum Widget {
|
||||
Progress(ProgressWidget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
struct CustomWidgetContext<'a> {
|
||||
tx: &'a Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &'a IconTheme,
|
||||
popup_buttons: Rc<RefCell<Vec<Button>>>,
|
||||
}
|
||||
|
||||
trait CustomWidget {
|
||||
@@ -115,11 +117,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(self, parent: >k::Box, context: CustomWidgetContext, common: CommonConfig) {
|
||||
fn add_to(self, parent: >k::Box, context: &CustomWidgetContext, common: CommonConfig) {
|
||||
macro_rules! create {
|
||||
($widget:expr) => {
|
||||
wrap_widget(
|
||||
&$widget.into_widget(context),
|
||||
&$widget.into_widget(context.clone()),
|
||||
common,
|
||||
context.bar_orientation,
|
||||
)
|
||||
@@ -143,7 +145,7 @@ impl Widget {
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
args: Option<Vec<String>>,
|
||||
geometry: WidgetGeometry,
|
||||
id: usize,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
@@ -173,9 +175,9 @@ impl Module<gtk::Box> for CustomModule {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
|
||||
} else if event.cmd == "popup:open" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
} else {
|
||||
@@ -191,25 +193,30 @@ impl Module<gtk::Box> for CustomModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let container = gtk::Box::builder().orientation(orientation).build();
|
||||
|
||||
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let custom_context = CustomWidgetContext {
|
||||
tx: &context.controller_tx,
|
||||
bar_orientation: orientation,
|
||||
icon_theme: info.icon_theme,
|
||||
popup_buttons: popup_buttons.clone(),
|
||||
};
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
widget
|
||||
.widget
|
||||
.add_to(&container, custom_context, widget.common);
|
||||
.add_to(&container, &custom_context, widget.common);
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts_owned(popup_buttons.take());
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
@@ -231,12 +238,13 @@ impl Module<gtk::Box> for CustomModule {
|
||||
tx: &tx,
|
||||
bar_orientation: info.bar_position.get_orientation(),
|
||||
icon_theme: info.icon_theme,
|
||||
popup_buttons: Rc::new(RefCell::new(vec![])),
|
||||
};
|
||||
|
||||
for widget in popup {
|
||||
widget
|
||||
.widget
|
||||
.add_to(&container, custom_context, widget.common);
|
||||
.add_to(&container, &custom_context, widget.common);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, send};
|
||||
use gtk::prelude::*;
|
||||
use gtk::ProgressBar;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, send};
|
||||
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ProgressWidget {
|
||||
name: Option<String>,
|
||||
@@ -69,7 +71,7 @@ impl CustomWidget for ProgressWidget {
|
||||
let progress = progress.clone();
|
||||
progress.set_show_text(true);
|
||||
|
||||
DynamicString::new(&text, move |string| {
|
||||
dynamic_string(&text, move |string| {
|
||||
progress.set_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::popup::Popup;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, send, try_send};
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::Scale;
|
||||
use serde::Deserialize;
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, send, try_send};
|
||||
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct SliderWidget {
|
||||
name: Option<String>,
|
||||
@@ -78,7 +80,7 @@ impl CustomWidget for SliderWidget {
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
scale.connect_change_value(move |scale, _, val| {
|
||||
scale.connect_change_value(move |_, _, val| {
|
||||
// GTK will send values outside min/max range
|
||||
let val = val.clamp(min, max);
|
||||
|
||||
@@ -88,7 +90,7 @@ impl CustomWidget for SliderWidget {
|
||||
ExecEvent {
|
||||
cmd: on_change.clone(),
|
||||
args: Some(vec![val.to_string()]),
|
||||
geometry: Popup::widget_geometry(scale, context.bar_orientation),
|
||||
id: usize::MAX // ignored
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{send_async, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{lock, send_async, try_send};
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
@@ -32,6 +32,18 @@ pub struct FocusedModule {
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
impl Default for FocusedModule {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_icon: crate::config::default_true(),
|
||||
show_title: crate::config::default_true(),
|
||||
icon_size: default_icon_size(),
|
||||
truncate: None,
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
@@ -52,7 +64,8 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
let (mut wlrx, handles) = {
|
||||
let wl = wayland::get_client().await;
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_toplevels()
|
||||
};
|
||||
|
||||
@@ -91,19 +104,19 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
||||
|
||||
let icon = gtk::Image::new();
|
||||
if self.show_icon {
|
||||
add_class(&icon, "icon");
|
||||
icon.add_class("icon");
|
||||
container.add(&icon);
|
||||
}
|
||||
|
||||
let label = Label::new(None);
|
||||
add_class(&label, "label");
|
||||
label.add_class("label");
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
@@ -115,7 +128,7 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
let icon_theme = icon_theme.clone();
|
||||
context.widget_rx.attach(None, move |(name, id)| {
|
||||
if self.show_icon {
|
||||
match ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
Some(Ok(_)) => icon.show(),
|
||||
@@ -131,7 +144,7 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::try_send;
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
@@ -17,6 +17,15 @@ pub struct LabelModule {
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
impl LabelModule {
|
||||
pub(crate) fn new(label: String) -> Self {
|
||||
Self {
|
||||
label,
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<Label> for LabelModule {
|
||||
type SendMessage = String;
|
||||
type ReceiveMessage = ();
|
||||
@@ -31,7 +40,7 @@ impl Module<Label> for LabelModule {
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
DynamicString::new(&self.label, move |string| {
|
||||
dynamic_string(&self.label, move |string| {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||
Continue(true)
|
||||
});
|
||||
@@ -43,18 +52,19 @@ impl Module<Label> for LabelModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<Label>> {
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
context.widget_rx.attach(None, move |string| {
|
||||
label.set_label(&string);
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use super::open_state::OpenState;
|
||||
use crate::clients::wayland::ToplevelHandle;
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::popup::Popup;
|
||||
use crate::{read_lock, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use gtk::{Button, IconTheme};
|
||||
use indexmap::IndexMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
@@ -176,7 +177,7 @@ impl ItemButton {
|
||||
item: &Item,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
orientation: Orientation,
|
||||
bar_position: BarPosition,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
) -> Self {
|
||||
@@ -191,7 +192,7 @@ impl ItemButton {
|
||||
if appearance.show_icons {
|
||||
let gtk_image = gtk::Image::new();
|
||||
let image =
|
||||
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
|
||||
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
|
||||
if let Some(image) = image {
|
||||
button.set_image(Some(>k_image));
|
||||
button.set_always_show_image(true);
|
||||
@@ -249,7 +250,9 @@ impl ItemButton {
|
||||
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
|
||||
ModuleUpdateEvent::OpenPopupAt(
|
||||
button.geometry(bar_position.get_orientation())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
@@ -259,6 +262,31 @@ impl ItemButton {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
|
||||
button.connect_leave_notify_event(move |button, ev| {
|
||||
const THRESHOLD: f64 = 5.0;
|
||||
|
||||
let alloc = button.allocation();
|
||||
|
||||
let (x, y) = ev.position();
|
||||
|
||||
let close = match bar_position {
|
||||
BarPosition::Top => y + THRESHOLD < alloc.height() as f64,
|
||||
BarPosition::Bottom => y > THRESHOLD,
|
||||
BarPosition::Left => x + THRESHOLD < alloc.width() as f64,
|
||||
BarPosition::Right => x > THRESHOLD,
|
||||
};
|
||||
|
||||
if close {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
button.show_all();
|
||||
|
||||
Self {
|
||||
|
||||
@@ -7,8 +7,10 @@ use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::modules::launcher::item::AppearanceOptions;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{lock, send_async, try_send, write_lock};
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{arc_mut, lock, send_async, try_send, write_lock};
|
||||
use color_eyre::{Help, Report};
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
@@ -16,7 +18,7 @@ use gtk::{Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error, trace};
|
||||
@@ -108,7 +110,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
.collect::<IndexMap<_, _>>()
|
||||
});
|
||||
|
||||
let items = Arc::new(Mutex::new(items));
|
||||
let items = arc_mut!(items);
|
||||
|
||||
let items2 = Arc::clone(&items);
|
||||
let tx2 = tx.clone();
|
||||
@@ -117,7 +119,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
let tx = tx2;
|
||||
|
||||
let (mut wlrx, handles) = {
|
||||
let wl = wayland::get_client().await;
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_toplevels()
|
||||
};
|
||||
|
||||
@@ -162,6 +165,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
match item {
|
||||
None => {
|
||||
let item: Item = handle.try_into()?;
|
||||
|
||||
items.insert(info.app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
@@ -270,14 +274,18 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
} else {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
|
||||
let wl = wayland::get_client().await;
|
||||
let wl = wayland::get_client();
|
||||
let items = lock!(items);
|
||||
|
||||
let id = match event {
|
||||
ItemEvent::FocusItem(app_id) => items
|
||||
.get(&app_id)
|
||||
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
|
||||
ItemEvent::FocusWindow(id) => Some(id), // FIXME: Broken on wlroots-git
|
||||
ItemEvent::FocusItem(app_id) => items.get(&app_id).and_then(|item| {
|
||||
item.windows
|
||||
.iter()
|
||||
.find(|(_, win)| !win.open_state.is_focused())
|
||||
.or_else(|| item.windows.first())
|
||||
.map(|(_, win)| win.id)
|
||||
}),
|
||||
ItemEvent::FocusWindow(id) => Some(id),
|
||||
ItemEvent::OpenItem(_) => unreachable!(),
|
||||
};
|
||||
|
||||
@@ -285,13 +293,18 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
if let Some(window) =
|
||||
items.iter().find_map(|(_, item)| item.windows.get(&id))
|
||||
{
|
||||
let seat = wl.get_seats().pop().expect("Failed to get Wayland seat");
|
||||
debug!("Focusing window {id}: {}", window.name);
|
||||
|
||||
let seat = lock!(wl)
|
||||
.get_seats()
|
||||
.pop()
|
||||
.expect("Failed to get Wayland seat");
|
||||
window.focus(&seat);
|
||||
}
|
||||
}
|
||||
|
||||
// roundtrip to immediately send activate event
|
||||
wl.roundtrip();
|
||||
lock!(wl).roundtrip();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -303,7 +316,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> crate::Result<ModuleWidget<gtk::Box>> {
|
||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
||||
@@ -321,7 +334,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
};
|
||||
|
||||
let show_names = self.show_names;
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let bar_position = info.bar_position;
|
||||
|
||||
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||
|
||||
@@ -337,7 +350,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
&item,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
orientation,
|
||||
bar_position,
|
||||
&context.tx,
|
||||
&controller_tx,
|
||||
);
|
||||
@@ -346,9 +359,10 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
buttons.insert(item.app_id, button);
|
||||
}
|
||||
}
|
||||
LauncherUpdate::AddWindow(app_id, _) => {
|
||||
LauncherUpdate::AddWindow(app_id, win) => {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_open(true);
|
||||
button.set_focused(win.open_state.is_focused());
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows += 1;
|
||||
@@ -369,8 +383,12 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveWindow(app_id, _) => {
|
||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||
debug!("Removing window {win_id} with id {app_id}");
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(false);
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows -= 1;
|
||||
}
|
||||
@@ -398,8 +416,11 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
});
|
||||
}
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
Ok(ModuleWidget {
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
@@ -466,7 +487,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
{
|
||||
let tx = controller_tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
button.connect_clicked(move |_button| {
|
||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use color_eyre::Result;
|
||||
use glib::IsA;
|
||||
use gtk::gdk::{EventMask, Monitor};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::config::{BarPosition, CommonConfig, TransitionType};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||
use crate::popup::Popup;
|
||||
use crate::send;
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub mod clipboard;
|
||||
/// Displays the current date and time.
|
||||
@@ -24,19 +41,6 @@ pub mod upower;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod workspaces;
|
||||
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::config::{BarPosition, CommonConfig, TransitionType};
|
||||
use crate::popup::{Popup, WidgetGeometry};
|
||||
use crate::{read_lock, send, write_lock};
|
||||
use color_eyre::Result;
|
||||
use glib::IsA;
|
||||
use gtk::gdk::{EventMask, Monitor};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ModuleLocation {
|
||||
Left,
|
||||
@@ -54,13 +58,15 @@ pub struct ModuleInfo<'a> {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ModuleUpdateEvent<T> {
|
||||
/// Sends an update to the module UI
|
||||
/// Sends an update to the module UI.
|
||||
Update(T),
|
||||
/// Toggles the open state of the popup.
|
||||
TogglePopup(WidgetGeometry),
|
||||
/// Takes the button ID.
|
||||
TogglePopup(usize),
|
||||
/// Force sets the popup open.
|
||||
/// Takes the button X position and width.
|
||||
OpenPopup(WidgetGeometry),
|
||||
/// Takes the button ID.
|
||||
OpenPopup(usize),
|
||||
OpenPopupAt(WidgetGeometry),
|
||||
/// Force sets the popup closed.
|
||||
ClosePopup,
|
||||
}
|
||||
@@ -73,9 +79,62 @@ pub struct WidgetContext<TSend, TReceive> {
|
||||
pub popup_rx: glib::Receiver<TSend>,
|
||||
}
|
||||
|
||||
pub struct ModuleWidget<W: IsA<Widget>> {
|
||||
pub struct ModuleParts<W: IsA<Widget>> {
|
||||
pub widget: W,
|
||||
pub popup: Option<gtk::Box>,
|
||||
pub popup: Option<ModulePopupParts>,
|
||||
}
|
||||
|
||||
impl<W: IsA<Widget>> ModuleParts<W> {
|
||||
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
|
||||
Self { widget, popup }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModulePopupParts {
|
||||
/// The popup container, with all its contents
|
||||
pub container: gtk::Box,
|
||||
/// An array of buttons which can be used for opening the popup.
|
||||
/// For most modules, this will only be a single button.
|
||||
/// For some advanced modules, such as `Launcher`, this is all item buttons.
|
||||
pub buttons: Vec<Button>,
|
||||
}
|
||||
|
||||
pub trait ModulePopup {
|
||||
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
|
||||
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
|
||||
}
|
||||
|
||||
impl ModulePopup for Option<gtk::Box> {
|
||||
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
|
||||
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
|
||||
}
|
||||
|
||||
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
|
||||
self.map(|container| ModulePopupParts { container, buttons })
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PopupButton {
|
||||
fn try_popup_id(&self) -> Option<usize>;
|
||||
fn popup_id(&self) -> usize;
|
||||
}
|
||||
|
||||
impl PopupButton for Button {
|
||||
/// Gets the popup ID associated with this button, if there is one.
|
||||
/// Will return `None` if this is not a popup button.
|
||||
fn try_popup_id(&self) -> Option<usize> {
|
||||
self.get_tag("popup-id").copied()
|
||||
}
|
||||
|
||||
/// Gets the popup ID associated with this button.
|
||||
/// This should only be called on buttons which are known to be associated with popups.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if an ID has not been set.
|
||||
fn popup_id(&self) -> usize {
|
||||
self.try_popup_id().expect("id to exist")
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Module<W>
|
||||
@@ -98,7 +157,7 @@ where
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<W>>;
|
||||
) -> Result<ModuleParts<W>>;
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
@@ -118,9 +177,10 @@ where
|
||||
pub fn create_module<TModule, TWidget, TSend, TRec>(
|
||||
module: TModule,
|
||||
id: usize,
|
||||
name: Option<String>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<ModuleWidget<TWidget>>
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
) -> Result<ModuleParts<TWidget>>
|
||||
where
|
||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||
TWidget: IsA<Widget>,
|
||||
@@ -142,29 +202,46 @@ where
|
||||
controller_tx: ui_tx,
|
||||
};
|
||||
|
||||
let name = TModule::name();
|
||||
let module_name = TModule::name();
|
||||
let instance_name = name.unwrap_or_else(|| module_name.to_string());
|
||||
|
||||
let module_parts = module.into_widget(context, info)?;
|
||||
module_parts.widget.style_context().add_class(name);
|
||||
module_parts.widget.add_class("widget");
|
||||
module_parts.widget.add_class(module_name);
|
||||
|
||||
let mut has_popup = false;
|
||||
if let Some(popup_content) = module_parts.popup.clone() {
|
||||
let has_popup = if let Some(popup_content) = module_parts.popup.clone() {
|
||||
popup_content
|
||||
.container
|
||||
.style_context()
|
||||
.add_class(&format!("popup-{name}"));
|
||||
.add_class(&format!("popup-{module_name}"));
|
||||
|
||||
register_popup_content(popup, id, popup_content);
|
||||
has_popup = true;
|
||||
}
|
||||
register_popup_content(popup, id, instance_name, popup_content);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
|
||||
setup_receiver(
|
||||
channel,
|
||||
w_tx,
|
||||
p_tx,
|
||||
popup.clone(),
|
||||
module_name,
|
||||
id,
|
||||
has_popup,
|
||||
);
|
||||
|
||||
Ok(module_parts)
|
||||
}
|
||||
|
||||
/// Registers the popup content with the popup.
|
||||
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
|
||||
write_lock!(popup).register_content(id, popup_content);
|
||||
fn register_popup_content(
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
id: usize,
|
||||
name: String,
|
||||
popup_content: ModulePopupParts,
|
||||
) {
|
||||
popup.borrow_mut().register_content(id, name, popup_content);
|
||||
}
|
||||
|
||||
/// Sets up the bridge channel receiver
|
||||
@@ -176,7 +253,7 @@ fn setup_receiver<TSend>(
|
||||
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
|
||||
w_tx: glib::Sender<TSend>,
|
||||
p_tx: glib::Sender<TSend>,
|
||||
popup: Arc<RwLock<Popup>>,
|
||||
popup: Rc<RefCell<Popup>>,
|
||||
name: &'static str,
|
||||
id: usize,
|
||||
has_popup: bool,
|
||||
@@ -196,40 +273,51 @@ fn setup_receiver<TSend>(
|
||||
|
||||
send!(w_tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(geometry) => {
|
||||
ModuleUpdateEvent::TogglePopup(button_id) => {
|
||||
debug!("Toggling popup for {} [#{}]", name, id);
|
||||
let popup = read_lock!(popup);
|
||||
let mut popup = popup.borrow_mut();
|
||||
if popup.is_visible() {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
popup.show(id, button_id);
|
||||
|
||||
// force re-render on initial open to try and fix size issue
|
||||
if !has_popup_opened {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
popup.show(id, button_id);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||
ModuleUpdateEvent::OpenPopup(button_id) => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
let mut popup = popup.borrow_mut();
|
||||
popup.hide();
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
popup.show(id, button_id);
|
||||
|
||||
// force re-render on initial open to try and fix size issue
|
||||
if !has_popup_opened {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
popup.show(id, button_id);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopupAt(geometry) => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
let mut popup = popup.borrow_mut();
|
||||
popup.hide();
|
||||
popup.show_at(id, geometry);
|
||||
|
||||
// force re-render on initial open to try and fix size issue
|
||||
if !has_popup_opened {
|
||||
popup.show_at(id, geometry);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
let mut popup = popup.borrow_mut();
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
@@ -239,14 +327,14 @@ fn setup_receiver<TSend>(
|
||||
}
|
||||
|
||||
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
|
||||
widget_parts: &ModuleWidget<TWidget>,
|
||||
widget_parts: &ModuleParts<TWidget>,
|
||||
common: &CommonConfig,
|
||||
) {
|
||||
if let Some(ref name) = common.name {
|
||||
widget_parts.widget.set_widget_name(name);
|
||||
|
||||
if let Some(ref popup) = widget_parts.popup {
|
||||
popup.set_widget_name(&format!("popup-{name}"));
|
||||
popup.container.set_widget_name(&format!("popup-{name}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +346,10 @@ pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
|
||||
|
||||
if let Some(ref popup) = widget_parts.popup {
|
||||
for part in class.split(' ') {
|
||||
popup.style_context().add_class(&format!("popup-{part}"));
|
||||
popup
|
||||
.container
|
||||
.style_context()
|
||||
.add_class(&format!("popup-{part}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,6 +377,8 @@ pub fn wrap_widget<W: IsA<Widget>>(
|
||||
revealer.set_reveal_child(true);
|
||||
|
||||
let container = EventBox::new();
|
||||
container.add_class("widget-container");
|
||||
|
||||
container.add_events(EventMask::SCROLL_MASK);
|
||||
container.add(&revealer);
|
||||
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
mod config;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use glib::{Continue, PropertySet};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::error;
|
||||
|
||||
use crate::clients::music::{
|
||||
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
|
||||
};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||
use crate::modules::PopupButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{send_async, try_send};
|
||||
|
||||
pub use self::config::MusicModule;
|
||||
use self::config::PlayerType;
|
||||
|
||||
mod config;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PlayerCommand {
|
||||
Previous,
|
||||
@@ -28,6 +35,7 @@ pub enum PlayerCommand {
|
||||
Pause,
|
||||
Next,
|
||||
Volume(u8),
|
||||
Seek(Duration),
|
||||
}
|
||||
|
||||
/// Formats a duration given in seconds
|
||||
@@ -47,6 +55,12 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ControllerEvent {
|
||||
Update(Option<SongUpdate>),
|
||||
UpdateProgress(ProgressTick),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SongUpdate {
|
||||
song: Track,
|
||||
@@ -67,7 +81,7 @@ async fn get_client(
|
||||
}
|
||||
|
||||
impl Module<Button> for MusicModule {
|
||||
type SendMessage = Option<SongUpdate>;
|
||||
type SendMessage = ControllerEvent;
|
||||
type ReceiveMessage = PlayerCommand;
|
||||
|
||||
fn name() -> &'static str {
|
||||
@@ -103,7 +117,7 @@ impl Module<Button> for MusicModule {
|
||||
PlayerUpdate::Update(track, status) => match *track {
|
||||
Some(track) => {
|
||||
let display_string =
|
||||
replace_tokens(format.as_str(), &tokens, &track, &status);
|
||||
replace_tokens(format.as_str(), &tokens, &track);
|
||||
|
||||
let update = SongUpdate {
|
||||
song: track,
|
||||
@@ -111,10 +125,24 @@ impl Module<Button> for MusicModule {
|
||||
display_string,
|
||||
};
|
||||
|
||||
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
|
||||
update
|
||||
)))
|
||||
);
|
||||
}
|
||||
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
|
||||
None => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
|
||||
),
|
||||
},
|
||||
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
|
||||
progress_tick
|
||||
))
|
||||
),
|
||||
PlayerUpdate::Disconnect => break,
|
||||
}
|
||||
}
|
||||
@@ -137,6 +165,7 @@ impl Module<Button> for MusicModule {
|
||||
PlayerCommand::Pause => client.pause(),
|
||||
PlayerCommand::Next => client.next(),
|
||||
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
|
||||
PlayerCommand::Seek(duration) => client.seek(duration),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
@@ -153,10 +182,10 @@ impl Module<Button> for MusicModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let button = Button::new();
|
||||
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
add_class(&button_contents, "contents");
|
||||
button_contents.add_class("contents");
|
||||
|
||||
button.add(&button_contents);
|
||||
|
||||
@@ -174,16 +203,11 @@ impl Module<Button> for MusicModule {
|
||||
button_contents.add(&icon_play);
|
||||
button_contents.add(&label);
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
{
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
|
||||
);
|
||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +215,11 @@ impl Module<Button> for MusicModule {
|
||||
let button = button.clone();
|
||||
let tx = context.tx.clone();
|
||||
|
||||
context.widget_rx.attach(None, move |mut event| {
|
||||
context.widget_rx.attach(None, move |event| {
|
||||
let ControllerEvent::Update(mut event) = event else {
|
||||
return Continue(true);
|
||||
};
|
||||
|
||||
if let Some(event) = event.take() {
|
||||
label.set_label(&event.display_string);
|
||||
|
||||
@@ -225,12 +253,11 @@ impl Module<Button> for MusicModule {
|
||||
});
|
||||
};
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup,
|
||||
})
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
@@ -241,13 +268,14 @@ impl Module<Button> for MusicModule {
|
||||
) -> Option<gtk::Box> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
|
||||
let album_image = gtk::Image::builder()
|
||||
.width_request(128)
|
||||
.height_request(128)
|
||||
.build();
|
||||
add_class(&album_image, "album-art");
|
||||
album_image.add_class("album-art");
|
||||
|
||||
let icons = self.icons;
|
||||
|
||||
@@ -256,28 +284,28 @@ impl Module<Button> for MusicModule {
|
||||
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
||||
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
||||
|
||||
add_class(&title_label.container, "title");
|
||||
add_class(&album_label.container, "album");
|
||||
add_class(&artist_label.container, "artist");
|
||||
title_label.container.add_class("title");
|
||||
album_label.container.add_class("album");
|
||||
artist_label.container.add_class("artist");
|
||||
|
||||
info_box.add(&title_label.container);
|
||||
info_box.add(&album_label.container);
|
||||
info_box.add(&artist_label.container);
|
||||
|
||||
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
add_class(&controls_box, "controls");
|
||||
controls_box.add_class("controls");
|
||||
|
||||
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
|
||||
add_class(&btn_prev, "btn-prev");
|
||||
btn_prev.add_class("btn-prev");
|
||||
|
||||
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
|
||||
add_class(&btn_play, "btn-play");
|
||||
btn_play.add_class("btn-play");
|
||||
|
||||
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
|
||||
add_class(&btn_pause, "btn-pause");
|
||||
btn_pause.add_class("btn-pause");
|
||||
|
||||
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
|
||||
add_class(&btn_next, "btn-next");
|
||||
btn_next.add_class("btn-next");
|
||||
|
||||
controls_box.add(&btn_prev);
|
||||
controls_box.add(&btn_play);
|
||||
@@ -287,21 +315,22 @@ impl Module<Button> for MusicModule {
|
||||
info_box.add(&controls_box);
|
||||
|
||||
let volume_box = gtk::Box::new(Orientation::Vertical, 5);
|
||||
add_class(&volume_box, "volume");
|
||||
volume_box.add_class("volume");
|
||||
|
||||
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
|
||||
volume_slider.set_inverted(true);
|
||||
add_class(&volume_slider, "slider");
|
||||
volume_slider.add_class("slider");
|
||||
|
||||
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
|
||||
add_class(&volume_icon, "icon");
|
||||
volume_icon.add_class("icon");
|
||||
|
||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||
|
||||
container.add(&album_image);
|
||||
container.add(&info_box);
|
||||
container.add(&volume_box);
|
||||
main_container.add(&album_image);
|
||||
main_container.add(&info_box);
|
||||
main_container.add(&volume_box);
|
||||
container.add(&main_container);
|
||||
|
||||
let tx_prev = tx.clone();
|
||||
btn_prev.connect_clicked(move |_| {
|
||||
@@ -323,12 +352,49 @@ impl Module<Button> for MusicModule {
|
||||
try_send!(tx_next, PlayerCommand::Next);
|
||||
});
|
||||
|
||||
let tx_vol = tx;
|
||||
let tx_vol = tx.clone();
|
||||
volume_slider.connect_change_value(move |_, _, val| {
|
||||
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
progress_box.add_class("progress");
|
||||
|
||||
let progress_label = Label::new(None);
|
||||
progress_label.add_class("label");
|
||||
|
||||
let progress = Scale::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.draw_value(false)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
progress.add_class("slider");
|
||||
|
||||
progress_box.add(&progress);
|
||||
progress_box.add(&progress_label);
|
||||
container.add(&progress_box);
|
||||
|
||||
let drag_lock = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
let drag_lock = drag_lock.clone();
|
||||
progress.connect_button_press_event(move |_, _| {
|
||||
drag_lock.set(true);
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let drag_lock = drag_lock.clone();
|
||||
progress.connect_button_release_event(move |scale, _| {
|
||||
let value = scale.value();
|
||||
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
|
||||
|
||||
drag_lock.set(false);
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
{
|
||||
@@ -336,18 +402,21 @@ impl Module<Button> for MusicModule {
|
||||
let image_size = self.cover_image_size;
|
||||
|
||||
let mut prev_cover = None;
|
||||
rx.attach(None, move |update| {
|
||||
if let Some(update) = update {
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
ControllerEvent::Update(Some(update)) => {
|
||||
// only update art when album changes
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover = new_cover.clone();
|
||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
||||
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
|
||||
}) {
|
||||
album_image.show();
|
||||
image.load_into_image(album_image.clone())
|
||||
} else {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
Ok(())
|
||||
};
|
||||
|
||||
@@ -356,15 +425,9 @@ impl Module<Button> for MusicModule {
|
||||
}
|
||||
}
|
||||
|
||||
title_label
|
||||
.label
|
||||
.set_text(&update.song.title.unwrap_or_default());
|
||||
album_label
|
||||
.label
|
||||
.set_text(&update.song.album.unwrap_or_default());
|
||||
artist_label
|
||||
.label
|
||||
.set_text(&update.song.artist.unwrap_or_default());
|
||||
update_popup_metadata_label(update.song.title, &title_label);
|
||||
update_popup_metadata_label(update.song.album, &album_label);
|
||||
update_popup_metadata_label(update.song.artist, &artist_label);
|
||||
|
||||
match update.status.state {
|
||||
PlayerState::Stopped => {
|
||||
@@ -396,8 +459,34 @@ impl Module<Button> for MusicModule {
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
volume_slider.set_value(update.status.volume_percent as f64);
|
||||
if let Some(volume) = update.status.volume_percent {
|
||||
volume_slider.set_value(volume as f64);
|
||||
volume_box.show();
|
||||
} else {
|
||||
volume_box.hide();
|
||||
}
|
||||
}
|
||||
ControllerEvent::UpdateProgress(progress_tick)
|
||||
if !drag_lock.load(Ordering::Relaxed) =>
|
||||
{
|
||||
if let (Some(elapsed), Some(duration)) =
|
||||
(progress_tick.elapsed, progress_tick.duration)
|
||||
{
|
||||
progress_label.set_label(&format!(
|
||||
"{}/{}",
|
||||
format_time(elapsed),
|
||||
format_time(duration)
|
||||
));
|
||||
|
||||
progress.set_value(elapsed.as_secs_f64());
|
||||
progress.set_range(0.0, duration.as_secs_f64());
|
||||
progress_box.show_all();
|
||||
} else {
|
||||
progress_box.hide();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
@@ -407,17 +496,24 @@ impl Module<Button> for MusicModule {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
|
||||
match text {
|
||||
Some(value) => {
|
||||
label.label.set_text(&value);
|
||||
label.container.show_all();
|
||||
}
|
||||
None => {
|
||||
label.container.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces each of the formatting tokens in the formatting string
|
||||
/// with actual data pulled from the music player
|
||||
fn replace_tokens(
|
||||
format_string: &str,
|
||||
tokens: &Vec<String>,
|
||||
song: &Track,
|
||||
status: &Status,
|
||||
) -> String {
|
||||
fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> String {
|
||||
let mut compiled_string = format_string.to_string();
|
||||
for token in tokens {
|
||||
let value = get_token_value(song, status, token);
|
||||
let value = get_token_value(song, token);
|
||||
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
|
||||
}
|
||||
compiled_string
|
||||
@@ -425,7 +521,7 @@ fn replace_tokens(
|
||||
|
||||
/// Converts a string format token value
|
||||
/// into its respective value.
|
||||
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
|
||||
fn get_token_value(song: &Track, token: &str) -> String {
|
||||
match token {
|
||||
"title" => song.title.clone(),
|
||||
"album" => song.album.clone(),
|
||||
@@ -434,8 +530,6 @@ fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
|
||||
"disc" => song.disc.map(|x| x.to_string()),
|
||||
"genre" => song.genre.clone(),
|
||||
"track" => song.track.map(|x| x.to_string()),
|
||||
"duration" => status.duration.map(format_time),
|
||||
"elapsed" => status.elapsed.map(format_time),
|
||||
_ => Some(token.to_string()),
|
||||
}
|
||||
.unwrap_or_default()
|
||||
@@ -454,8 +548,8 @@ impl IconLabel {
|
||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||
let label = Label::new(label);
|
||||
|
||||
add_class(&icon, "icon-box");
|
||||
add_class(&label, "label");
|
||||
icon.add_class("icon-box");
|
||||
label.add_class("label");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::script::{OutputStream, Script, ScriptMode};
|
||||
use crate::try_send;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
@@ -83,7 +83,7 @@ impl Module<Label> for ScriptModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<Label>> {
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Module<Label> for ScriptModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::send_async;
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
@@ -186,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let re = Regex::new(r"\{([^}]+)}")?;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
|
||||
@@ -196,7 +196,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
for format in &self.format {
|
||||
let label = Label::builder().label(format).use_markup(true).build();
|
||||
|
||||
add_class(&label, "item");
|
||||
label.add_class("item");
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
|
||||
container.add(&label);
|
||||
@@ -220,7 +220,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::clients::system_tray::get_tray_event_client;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{await_sync, try_send};
|
||||
use color_eyre::Result;
|
||||
use gtk::gdk_pixbuf::{Colorspace, InterpType};
|
||||
@@ -11,9 +11,9 @@ use gtk::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
||||
use stray::message::tray::StatusNotifierItem;
|
||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
@@ -172,7 +172,7 @@ impl Module<MenuBar> for TrayModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<MenuBar>> {
|
||||
) -> Result<ModuleParts<MenuBar>> {
|
||||
let container = MenuBar::new();
|
||||
|
||||
{
|
||||
@@ -238,7 +238,7 @@ impl Module<MenuBar> for TrayModule {
|
||||
});
|
||||
};
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
use crate::clients::upower::get_display_proxy;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{await_sync, error, send_async, try_send};
|
||||
use color_eyre::Result;
|
||||
use futures_lite::stream::StreamExt;
|
||||
use gtk::{prelude::*, Button};
|
||||
@@ -15,6 +8,16 @@ use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use upower_dbus::BatteryState;
|
||||
use zbus;
|
||||
|
||||
use crate::clients::upower::get_display_proxy;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::PopupButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{await_sync, error, send_async, try_send};
|
||||
|
||||
const DAY: i64 = 24 * 60 * 60;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
const MINUTE: i64 = 60;
|
||||
@@ -24,6 +27,9 @@ pub struct UpowerModule {
|
||||
#[serde(default = "default_format")]
|
||||
format: String,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
@@ -32,6 +38,10 @@ fn default_format() -> String {
|
||||
String::from("{percentage}%")
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
24
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpowerProperties {
|
||||
percentage: f64,
|
||||
@@ -143,32 +153,31 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let icon = gtk::Image::new();
|
||||
add_class(&icon, "icon");
|
||||
icon.add_class("icon");
|
||||
|
||||
let label = Label::builder()
|
||||
.label(&self.format)
|
||||
.use_markup(true)
|
||||
.build();
|
||||
add_class(&label, "label");
|
||||
label.add_class("label");
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
add_class(&container, "contents");
|
||||
container.add_class("contents");
|
||||
|
||||
let button = Button::new();
|
||||
add_class(&button, "button");
|
||||
button.add_class("button");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
button.add(&container);
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||
ModuleUpdateEvent::TogglePopup(button.popup_id())
|
||||
);
|
||||
});
|
||||
|
||||
@@ -180,18 +189,17 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
.attach(None, move |properties: UpowerProperties| {
|
||||
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||
ImageProvider::parse(&icon_name, &icon_theme, 24)
|
||||
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
|
||||
.map(|provider| provider.load_into_image(icon.clone()));
|
||||
label.set_markup(format.as_ref());
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup,
|
||||
})
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
@@ -208,7 +216,7 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
.build();
|
||||
|
||||
let label = Label::new(None);
|
||||
add_class(&label, "upower-details");
|
||||
label.add_class("upower-details");
|
||||
container.add(&label);
|
||||
|
||||
rx.attach(None, move |properties| {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
|
||||
use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme};
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::trace;
|
||||
@@ -29,11 +29,32 @@ impl Default for SortOrder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum Favorites {
|
||||
ByMonitor(HashMap<String, Vec<String>>),
|
||||
Global(Vec<String>),
|
||||
}
|
||||
|
||||
impl Default for Favorites {
|
||||
fn default() -> Self {
|
||||
Self::Global(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
name_map: Option<HashMap<String, String>>,
|
||||
|
||||
/// Array of always shown workspaces, and what monitor to show on
|
||||
#[serde(default)]
|
||||
favorites: Favorites,
|
||||
|
||||
/// List of workspace names to never show
|
||||
#[serde(default)]
|
||||
hidden: Vec<String>,
|
||||
|
||||
/// Whether to display buttons for all monitors.
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
@@ -55,7 +76,7 @@ const fn default_icon_size() -> i32 {
|
||||
/// Creates a button from a workspace
|
||||
fn create_button(
|
||||
name: &str,
|
||||
focused: bool,
|
||||
visibility: Visibility,
|
||||
name_map: &HashMap<String, String>,
|
||||
icon_theme: &IconTheme,
|
||||
icon_size: i32,
|
||||
@@ -69,7 +90,11 @@ fn create_button(
|
||||
let style_context = button.style_context();
|
||||
style_context.add_class("item");
|
||||
|
||||
if focused {
|
||||
if visibility.is_visible() {
|
||||
style_context.add_class("visible");
|
||||
}
|
||||
|
||||
if visibility.is_focused() {
|
||||
style_context.add_class("focused");
|
||||
}
|
||||
|
||||
@@ -105,6 +130,13 @@ fn reorder_workspaces(container: >k::Box) {
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspacesModule {
|
||||
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
|
||||
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
|
||||
&& (self.all_monitors || output == &work.monitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for WorkspacesModule {
|
||||
type SendMessage = WorkspaceUpdate;
|
||||
type ReceiveMessage = String;
|
||||
@@ -154,10 +186,12 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
||||
|
||||
let name_map = self.name_map.unwrap_or_default();
|
||||
let name_map = self.name_map.clone().unwrap_or_default();
|
||||
let favs = self.favorites.clone();
|
||||
let mut fav_names: Vec<String> = vec![];
|
||||
|
||||
let mut button_map: HashMap<String, Button> = HashMap::new();
|
||||
|
||||
@@ -176,19 +210,48 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
WorkspaceUpdate::Init(workspaces) => {
|
||||
if !has_initialized {
|
||||
trace!("Creating workspace buttons");
|
||||
for workspace in workspaces {
|
||||
if self.all_monitors || workspace.monitor == output_name {
|
||||
|
||||
let mut added = HashSet::new();
|
||||
|
||||
let mut add_workspace = |name: &str, visibility: Visibility| {
|
||||
let item = create_button(
|
||||
&workspace.name,
|
||||
workspace.focused,
|
||||
name,
|
||||
visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
container.add(&item);
|
||||
|
||||
button_map.insert(workspace.name, item);
|
||||
container.add(&item);
|
||||
button_map.insert(name.to_string(), item);
|
||||
};
|
||||
|
||||
// add workspaces from client
|
||||
for workspace in &workspaces {
|
||||
if self.show_workspace_check(&output_name, workspace) {
|
||||
add_workspace(&workspace.name, workspace.visibility);
|
||||
added.insert(workspace.name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut add_favourites = |names: &Vec<String>| {
|
||||
for name in names {
|
||||
if !added.contains(name) {
|
||||
add_workspace(name, Visibility::Hidden);
|
||||
added.insert(name.to_string());
|
||||
fav_names.push(name.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// add workspaces from favourites
|
||||
match &favs {
|
||||
Favorites::Global(names) => add_favourites(names),
|
||||
Favorites::ByMonitor(map) => {
|
||||
if let Some(to_add) = map.get(&output_name) {
|
||||
add_favourites(to_add);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,22 +264,33 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Focus { old, new } => {
|
||||
let old = button_map.get(&old);
|
||||
if let Some(old) = old {
|
||||
old.style_context().remove_class("focused");
|
||||
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) {
|
||||
if Some(new.monitor) == old.map(|w| w.monitor) {
|
||||
btn.style_context().remove_class("visible");
|
||||
}
|
||||
|
||||
let new = button_map.get(&new);
|
||||
if let Some(new) = new {
|
||||
new.style_context().add_class("focused");
|
||||
btn.style_context().remove_class("focused");
|
||||
}
|
||||
|
||||
let new = button_map.get(&new.name);
|
||||
if let Some(btn) = new {
|
||||
let style = btn.style_context();
|
||||
|
||||
style.add_class("visible");
|
||||
style.add_class("focused");
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Add(workspace) => {
|
||||
if self.all_monitors || workspace.monitor == output_name {
|
||||
if fav_names.contains(&workspace.name) {
|
||||
let btn = button_map.get(&workspace.name);
|
||||
if let Some(btn) = btn {
|
||||
btn.style_context().remove_class("inactive");
|
||||
}
|
||||
} else if self.show_workspace_check(&output_name, &workspace) {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.focused,
|
||||
workspace.visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
@@ -236,12 +310,12 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Move(workspace) => {
|
||||
if !self.all_monitors {
|
||||
if !self.hidden.contains(&workspace.name) && !self.all_monitors {
|
||||
if workspace.monitor == output_name {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.focused,
|
||||
workspace.visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
@@ -267,9 +341,13 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
WorkspaceUpdate::Remove(workspace) => {
|
||||
let button = button_map.get(&workspace);
|
||||
if let Some(item) = button {
|
||||
if fav_names.contains(&workspace) {
|
||||
item.style_context().add_class("inactive");
|
||||
} else {
|
||||
container.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Update(_) => {}
|
||||
};
|
||||
|
||||
@@ -277,7 +355,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
114
src/popup.rs
114
src/popup.rs
@@ -1,18 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::config::BarPosition;
|
||||
use crate::modules::ModuleInfo;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{ApplicationWindow, Orientation};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
|
||||
use crate::unique_id::get_unique_usize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Popup {
|
||||
pub window: ApplicationWindow,
|
||||
pub cache: HashMap<usize, gtk::Box>,
|
||||
pub cache: HashMap<usize, (String, ModulePopupParts)>,
|
||||
monitor: Monitor,
|
||||
pos: BarPosition,
|
||||
current_widget: Option<usize>,
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
@@ -28,6 +32,7 @@ impl Popup {
|
||||
.build();
|
||||
|
||||
gtk_layer_shell::init_for_window(&win);
|
||||
gtk_layer_shell::set_monitor(&win, module_info.monitor);
|
||||
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
||||
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
|
||||
|
||||
@@ -108,20 +113,54 @@ impl Popup {
|
||||
cache: HashMap::new(),
|
||||
monitor: module_info.monitor.clone(),
|
||||
pos,
|
||||
current_widget: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_content(&mut self, key: usize, content: gtk::Box) {
|
||||
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
|
||||
debug!("Registered popup content for #{}", key);
|
||||
self.cache.insert(key, content);
|
||||
|
||||
for button in &content.buttons {
|
||||
let id = get_unique_usize();
|
||||
button.set_tag("popup-id", id);
|
||||
}
|
||||
|
||||
pub fn show_content(&self, key: usize) {
|
||||
self.cache.insert(key, (name, content));
|
||||
}
|
||||
|
||||
pub fn show(&mut self, widget_id: usize, button_id: usize) {
|
||||
self.clear_window();
|
||||
|
||||
if let Some(content) = self.cache.get(&key) {
|
||||
content.style_context().add_class("popup");
|
||||
self.window.add(content);
|
||||
if let Some((_name, content)) = self.cache.get(&widget_id) {
|
||||
self.current_widget = Some(widget_id);
|
||||
|
||||
content.container.style_context().add_class("popup");
|
||||
self.window.add(&content.container);
|
||||
|
||||
self.window.show();
|
||||
|
||||
let button = content
|
||||
.buttons
|
||||
.iter()
|
||||
.find(|b| b.popup_id() == button_id)
|
||||
.expect("to find valid button");
|
||||
|
||||
let orientation = self.pos.get_orientation();
|
||||
let geometry = button.geometry(orientation);
|
||||
|
||||
self.set_pos(geometry);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
|
||||
self.clear_window();
|
||||
|
||||
if let Some((_name, content)) = self.cache.get(&widget_id) {
|
||||
content.container.style_context().add_class("popup");
|
||||
self.window.add(&content.container);
|
||||
|
||||
self.window.show();
|
||||
self.set_pos(geometry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,14 +171,9 @@ impl Popup {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the popup
|
||||
pub fn show(&self, geometry: WidgetGeometry) {
|
||||
self.window.show();
|
||||
self.set_pos(geometry);
|
||||
}
|
||||
|
||||
/// Hides the popover
|
||||
pub fn hide(&self) {
|
||||
pub fn hide(&mut self) {
|
||||
self.current_widget = None;
|
||||
self.window.hide();
|
||||
}
|
||||
|
||||
@@ -148,6 +182,10 @@ impl Popup {
|
||||
self.window.is_visible()
|
||||
}
|
||||
|
||||
pub fn current_widget(&self) -> Option<usize> {
|
||||
self.current_widget
|
||||
}
|
||||
|
||||
/// Sets the popup's X/Y position relative to the left or border of the screen
|
||||
/// (depending on orientation).
|
||||
fn set_pos(&self, geometry: WidgetGeometry) {
|
||||
@@ -187,48 +225,4 @@ impl Popup {
|
||||
|
||||
gtk_layer_shell::set_margin(&self.window, edge, offset as i32);
|
||||
}
|
||||
|
||||
/// Gets the absolute X position of the button
|
||||
/// and its width / height (depending on orientation).
|
||||
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
|
||||
where
|
||||
W: IsA<gtk::Widget>,
|
||||
{
|
||||
let widget_size = if orientation == Orientation::Horizontal {
|
||||
widget.allocation().width()
|
||||
} else {
|
||||
widget.allocation().height()
|
||||
};
|
||||
|
||||
let top_level = widget.toplevel().expect("Failed to get top-level widget");
|
||||
|
||||
let bar_size = if orientation == Orientation::Horizontal {
|
||||
top_level.allocation().width()
|
||||
} else {
|
||||
top_level.allocation().height()
|
||||
};
|
||||
|
||||
let (widget_x, widget_y) = widget
|
||||
.translate_coordinates(&top_level, 0, 0)
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
let widget_pos = if orientation == Orientation::Horizontal {
|
||||
widget_x
|
||||
} else {
|
||||
widget_y
|
||||
};
|
||||
|
||||
WidgetGeometry {
|
||||
position: widget_pos,
|
||||
size: widget_size,
|
||||
bar_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct WidgetGeometry {
|
||||
position: i32,
|
||||
size: i32,
|
||||
bar_size: i32,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::send;
|
||||
use color_eyre::{Help, Report};
|
||||
use glib::Continue;
|
||||
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||
use gtk::prelude::CssProviderExt;
|
||||
use gtk::{gdk, gio, CssProvider, StyleContext};
|
||||
use notify::event::{DataChange, ModifyKind};
|
||||
@@ -29,7 +30,11 @@ pub fn load_css(style_path: PathBuf) {
|
||||
};
|
||||
|
||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||
StyleContext::add_provider_for_screen(&screen, &provider, 800);
|
||||
StyleContext::add_provider_for_screen(
|
||||
&screen,
|
||||
&provider,
|
||||
GTK_STYLE_PROVIDER_PRIORITY_USER as u32,
|
||||
);
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user