Compare commits
356 Commits
feat/volum
...
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 | ||
|
|
c8c84446d6 | ||
|
|
103a224355 | ||
|
|
18b36423e2 | ||
|
|
09a26d04bc | ||
|
|
96323801d9 | ||
|
|
de98cf3dae | ||
|
|
b3b96673b0 | ||
|
|
de3aa5d7b1 | ||
|
|
ac34c05d2e | ||
|
|
6f7af07cdd | ||
|
|
96d36c43d4 | ||
|
|
e11177fea3 | ||
|
|
cbba2bc614 | ||
|
|
658d040607 | ||
|
|
81c69f644e | ||
|
|
55327c6f98 | ||
|
|
fc2e93491f | ||
|
|
a10cbd6287 | ||
|
|
1991662bc4 | ||
|
|
b4f1c7ac2d | ||
|
|
1e799f7635 | ||
|
|
a9fe75b2f7 | ||
|
|
8ce7f8cb80 | ||
|
|
e709200242 | ||
|
|
f8a2c0f002 | ||
|
|
3b18e1d8a1 | ||
|
|
f37bc80292 | ||
|
|
e0bc05acb5 | ||
|
|
5a675153b4 | ||
|
|
090a6669b8 | ||
|
|
80655883ef | ||
|
|
ce015f8d19 | ||
|
|
5a09e46b28 | ||
|
|
0fce762eef | ||
|
|
0a6da15bb2 | ||
|
|
19d1414daa | ||
|
|
4456bb5d20 | ||
|
|
942d401472 | ||
|
|
952fef270e | ||
|
|
a5ecb363fd | ||
|
|
e036ff03c1 | ||
|
|
6c48d40e5b | ||
|
|
a0881fc909 | ||
|
|
d938298e7b | ||
|
|
e18fb0661d | ||
|
|
6950c79906 | ||
|
|
c4af6a8069 | ||
|
|
7ede33c9c1 | ||
|
|
7f0fdf2391 | ||
|
|
807a31bf92 | ||
|
|
e1b0c9b43d | ||
|
|
c3e9654cd3 | ||
|
|
e6a70f7663 | ||
|
|
b4d7344200 | ||
|
|
a6b686624b | ||
|
|
b9740cba8f | ||
|
|
1f980ca783 | ||
|
|
48d6af0281 | ||
|
|
22b630a10b | ||
|
|
5877f773aa | ||
|
|
87ca399220 | ||
|
|
960da55a05 | ||
|
|
0e65f93a23 | ||
|
|
91ed1ee384 | ||
|
|
9012feee4f | ||
|
|
3b54d527b2 | ||
|
|
242b70ed39 | ||
|
|
bd144e87a8 | ||
|
|
3ccb54b49c | ||
|
|
ff315ff5db | ||
|
|
cdeafbdc72 | ||
|
|
13d39235ad | ||
|
|
327e345630 | ||
|
|
f82f897982 | ||
|
|
fe12251af8 | ||
|
|
d116a51083 | ||
|
|
6836abefd1 | ||
|
|
91e766d6ab | ||
|
|
dea66415c2 | ||
|
|
528a8d6dd6 | ||
|
|
e1abadcf39 | ||
|
|
cf32870f8a | ||
|
|
139bc5d23f | ||
|
|
735f5cc9f1 | ||
|
|
aed04c1ccf | ||
|
|
c1ea5fad7e | ||
|
|
38da59cd41 | ||
|
|
7f46cb4976 | ||
|
|
5c18ec8ba0 | ||
|
|
81acc176ed | ||
|
|
618b7ef552 | ||
|
|
2a155b9aa8 | ||
|
|
31a57ae637 | ||
|
|
bc87c7f0d4 | ||
|
|
bde469816a | ||
|
|
98aaaa0d14 | ||
|
|
51e95d9e01 | ||
|
|
ea9f7caaf7 | ||
|
|
338829e275 | ||
|
|
610c3528af | ||
|
|
f95e1e8f74 | ||
|
|
35dfbbf91d | ||
|
|
14b6c1a69f | ||
|
|
0e3102de8c | ||
|
|
ad3c171eca | ||
|
|
e5bc44168f | ||
|
|
cc62927f15 | ||
|
|
76e2b7ba3e | ||
|
|
033d0f7e6e | ||
|
|
dc16b1e15a | ||
|
|
03cd263095 | ||
|
|
db0868a3fc | ||
|
|
0382b50cf4 | ||
|
|
338f5a0e1b | ||
|
|
20949a7744 | ||
|
|
2da28b9bf5 | ||
|
|
618e97f1e8 | ||
|
|
dd7c9f30db | ||
|
|
1fa0c0e977 | ||
|
|
74d18aedfb | ||
|
|
2c88c99cb6 | ||
|
|
236bb09170 | ||
|
|
83f44fd92f | ||
|
|
1855416db4 | ||
|
|
e63509a3a7 | ||
|
|
4a09b70854 | ||
|
|
9d09855fce | ||
|
|
e9d0273176 | ||
|
|
7926bb07eb | ||
|
|
6fd69d657c | ||
|
|
27d11de661 | ||
|
|
07df51c249 | ||
|
|
b038e7671a | ||
|
|
e5ab9f33b5 | ||
|
|
68bc8230dd | ||
|
|
246313136f | ||
|
|
15a9d8d42c | ||
|
|
a87d8d5c30 | ||
|
|
8e99fd4d0f | ||
|
|
1e1d65ae49 | ||
|
|
2815cef440 | ||
|
|
138b5b3903 | ||
|
|
7355db74ec | ||
|
|
c214f65ecb | ||
|
|
3d308ab572 | ||
|
|
b770ae716c | ||
|
|
3613aef5c5 | ||
|
|
a9d1233909 | ||
|
|
72b14b6c4e | ||
|
|
910945306c | ||
|
|
dfe1964abf | ||
|
|
e928b30f99 | ||
|
|
2ab06f044e | ||
|
|
4b4f1ffc21 | ||
|
|
0691db3b87 | ||
|
|
cac064f479 | ||
|
|
6c622864b3 | ||
|
|
55c06c4766 | ||
|
|
1b0287becc | ||
|
|
7bf44ca75d | ||
|
|
fb04ceab7d | ||
|
|
102d2478a9 | ||
|
|
80a414ab67 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
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
|
||||
194
CHANGELOG.md
194
CHANGELOG.md
@@ -4,6 +4,194 @@ 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))*:
|
||||
|
||||
To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`6c62286`](https://github.com/JakeStanger/ironbar/commit/6c622864b388548eaaa595f41993606cc151d585) - new label module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cac064f`](https://github.com/JakeStanger/ironbar/commit/cac064f4795e9f418cc0820f04944f91121c426a) - ability to configure popup gap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dfe1964`](https://github.com/JakeStanger/ironbar/commit/dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d) - **custom**: slider widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`72b14b6`](https://github.com/JakeStanger/ironbar/commit/72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2) - **custom**: progress bar widget. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a9d1233`](https://github.com/JakeStanger/ironbar/commit/a9d12339097cbe0fef1628460ef538319a048223) - **custom**: support dynamic strings on buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3d308ab`](https://github.com/JakeStanger/ironbar/commit/3d308ab572a39ada2501ddc1b822e50e1f8a8363) - **custom**: support dynamic string in image source *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4a09b70`](https://github.com/JakeStanger/ironbar/commit/4a09b70854dad33bf890a3fe766f854d9195e786) - **custom**: support common options in widgets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83f44fd`](https://github.com/JakeStanger/ironbar/commit/83f44fd92fe74b45fcdfc242fb90fc932dd2b00b) - wrap modules in a revealer to support animated show/hide *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1fa0c0e`](https://github.com/JakeStanger/ironbar/commit/1fa0c0e9774c302727d414f5aef999ab71a7acb8) - **custom**: support mouse wheel on slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2da28b9`](https://github.com/JakeStanger/ironbar/commit/2da28b9bf5790adfc46c58b6f6d5fdd13cc17195) - ability to configure image icon sizes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`033d0f7`](https://github.com/JakeStanger/ironbar/commit/033d0f7e6e450b3f2d62d9a75210d52611cf346d) - **custom**: option to toggle slider label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`76e2b7b`](https://github.com/JakeStanger/ironbar/commit/76e2b7ba3e788f273039d74635881ddb96264258) - **music**: option to hide status icon on widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ad3c171`](https://github.com/JakeStanger/ironbar/commit/ad3c171ecacaebf10408c2583ed7361ed029075e) - implement upower module *(commit by [@p00f](https://github.com/p00f))*
|
||||
- [`2a155b9`](https://github.com/JakeStanger/ironbar/commit/2a155b9aa8a3634908512d9b83680925962d478f) - **music**: add css selector for button contents *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c1ea5fa`](https://github.com/JakeStanger/ironbar/commit/c1ea5fad7ec308895f0454b6de05a3177563626c) - **logging**: include line numbers *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`9109453`](https://github.com/JakeStanger/ironbar/commit/910945306c3261190a16300da2ed28efb945a6ac) - **dynamic string**: parser issue related to incorrectly matching braces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7355db7`](https://github.com/JakeStanger/ironbar/commit/7355db74ec9118c2cb46899534a3adac8d7165d9) - **image**: http provider not handling non-success codes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a87d8d5`](https://github.com/JakeStanger/ironbar/commit/a87d8d5c3071a1d8ab149deae17d261ae97368ea) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`15a9d8d`](https://github.com/JakeStanger/ironbar/commit/15a9d8d42c9319a7062e6a90086e0c1c3323f5d8) - **script**: parser incorrectly handling colons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`68bc823`](https://github.com/JakeStanger/ironbar/commit/68bc8230ddf3352cc0de9f8cc770632744c22747) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b038e76`](https://github.com/JakeStanger/ironbar/commit/b038e7671af4bfa41060adf724deb8c6151fac1f) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7926bb0`](https://github.com/JakeStanger/ironbar/commit/7926bb07eb181edaf6da2f11a7dc00f8be2240eb) - **nix**: Fix `nix run` support *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`2c88c99`](https://github.com/JakeStanger/ironbar/commit/2c88c99cb605d312e2d76d620f502c7e7cd8866e) - **dynamic string**: crash when last segment is static and a single char *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`338f5a0`](https://github.com/JakeStanger/ironbar/commit/338f5a0e1b58dc9b52caee61d6a9748cf13153c5) - **nix**: Attempt to fix image blurriness *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`db0868a`](https://github.com/JakeStanger/ironbar/commit/db0868a3fc0734daa61067e377018c692599ebff) - **image**: not scaling icons for hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`14b6c1a`](https://github.com/JakeStanger/ironbar/commit/14b6c1a69f28836ed9e3b74eeb97a42ea60ffc27) - bars duplicate when starting second instance *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`98aaaa0`](https://github.com/JakeStanger/ironbar/commit/98aaaa0d1407681b3d790c933c4972b8122f8007) - fallback to default icon theme for notifier items *(commit by [@oknozor](https://github.com/oknozor))*
|
||||
- [`735f5cc`](https://github.com/JakeStanger/ironbar/commit/735f5cc9f1518c256785d42f3d21ed5c68b11711) - **launcher**: crash when focusing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e1abadc`](https://github.com/JakeStanger/ironbar/commit/e1abadcf39a2d39078e75179a167e9277ee5e550) - **clipboard**: copying large images filling write pipe *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`2ab06f0`](https://github.com/JakeStanger/ironbar/commit/2ab06f044ec300628d6648852d395889b6752b76) - **custom**: split into enum with separate file per widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3613aef`](https://github.com/JakeStanger/ironbar/commit/3613aef5c5a4051b5a44e33342c0eaaab3d4a690) - **custom**: reduce a lot of repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c214f65`](https://github.com/JakeStanger/ironbar/commit/c214f65ecb86a0da6559025203701661924f65bb) - fix strict clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27d11de`](https://github.com/JakeStanger/ironbar/commit/27d11de6616c410422d7abd579d09b3abc02f43a) - **config**: split common code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6fd69d6`](https://github.com/JakeStanger/ironbar/commit/6fd69d657c6224bc47c9b3cb5affcf74b63a6aa6) - move module creation code to module module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e63509a`](https://github.com/JakeStanger/ironbar/commit/e63509a3a7673ea41b4c937089a1cf6d2362fed3) - fix a few new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7f46cb4`](https://github.com/JakeStanger/ironbar/commit/7f46cb49767bd722be8d42999a9ba69887efcd40) - **wayland**: update to 0.30.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dd7c9f3`](https://github.com/JakeStanger/ironbar/commit/dd7c9f30db6e4e1ede4d57255122b359636b8f58) - add transition module-level options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`610c352`](https://github.com/JakeStanger/ironbar/commit/610c3528af98b8c6b02af7ce5c07190776522c3a) - add missing link to upower page *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ea9f7ca`](https://github.com/JakeStanger/ironbar/commit/ea9f7caaf7a35eebd603ce2854672d5af2901018) - add missing `upower` feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`618b7ef`](https://github.com/JakeStanger/ironbar/commit/618b7ef5520de6f3796b66e42422a36c5a191ab0) - improve example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`139bc5d`](https://github.com/JakeStanger/ironbar/commit/139bc5d23f7f887b7b65d50adc21fa6679ea291e) - **compiling**: improve requirements list *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cf32870`](https://github.com/JakeStanger/ironbar/commit/cf32870f8a380c305a436593950c3da524a2296f) - **compiling**: add ron feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.11.0] - 2023-04-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`d253c4b`](https://github.com/JakeStanger/ironbar/commit/d253c4bd7f306c7b8fef223d1beb7b1f6e77629b) - add configurable margins around bar *(commit by [@ttoino](https://github.com/ttoino))*
|
||||
- [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - **truncate**: ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`575d6cc`](https://github.com/JakeStanger/ironbar/commit/575d6cc30f9e28079aed8425566048abd3d9e022) - new clipboard manager module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9984b63`](https://github.com/JakeStanger/ironbar/commit/9984b638b55adea11ba90412346fbb8220f05682) - **nix**: initial nix feature flags impl *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`b1475a1`](https://github.com/JakeStanger/ironbar/commit/b1475a1affd2f101f1f707ab1a0e8e5509a1d99f) - **nix**: use cargo default features *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`102d247`](https://github.com/JakeStanger/ironbar/commit/102d2478a9d0ecc8be12c5ea6019a5a5411cc6ab) - module hover options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`2ac5071`](https://github.com/JakeStanger/ironbar/commit/2ac507144b42a80507f8d2df214889c114c069df) - not setting layer shell namespace *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7dff3e6`](https://github.com/JakeStanger/ironbar/commit/7dff3e6f8b989132ff0c4406caa72f063dd57c9f) - **image**: widgets missing names *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`54b9b28`](https://github.com/JakeStanger/ironbar/commit/54b9b28c75b2fe300e2bad1436d315da1950953e) - make readme more concise *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`8cbb73b`](https://github.com/JakeStanger/ironbar/commit/8cbb73b75e7aca1aa163406f4583273e6ff4bac2) - **dynamic string**: dynamic sections not respecting ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`d0b7bdb`](https://github.com/JakeStanger/ironbar/commit/d0b7bdbafcc34967dd5b048ea12e6267ba293566) - **nix**: home manager module, and features *(commit by [@yavko](https://github.com/yavko))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`d84139a`](https://github.com/JakeStanger/ironbar/commit/d84139a914f9b35054dc6048715e1ed7e79d7441) - general tidy up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7212bbc`](https://github.com/JakeStanger/ironbar/commit/7212bbcf61e097b35a7ab341e19e9daefd2edf95) - **dynamic string**: use vec instead of indexmap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ecdd71a`](https://github.com/JakeStanger/ironbar/commit/ecdd71a43d267161f84e3c4a3c22e9454c0f7184) - **config**: use `universal-config` crate. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`82875cd`](https://github.com/JakeStanger/ironbar/commit/82875cde687628f3ee3436343068825440128599) - update CHANGELOG.md for v0.10.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7c36f5c`](https://github.com/JakeStanger/ironbar/commit/7c36f5cb0cf03191c9b03e2455b63829a64e402e) - fix a couple of issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83a4916`](https://github.com/JakeStanger/ironbar/commit/83a49165c42fa793ef1224f93cbc147bc69de894) - **compiling**: add info about build deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5bbe64b`](https://github.com/JakeStanger/ironbar/commit/5bbe64bb86fb2db0921e284a1560db2f6c1a1920) - **clock**: format table *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2b26eaf`](https://github.com/JakeStanger/ironbar/commit/2b26eaf41036609be4dfc57689ca8d770dcb6b9b) - **clipboard**: fix incorrect setting description *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0125ce5`](https://github.com/JakeStanger/ironbar/commit/0125ce5916c003d1ea9a141fe5a0f6a54b2778ab) - **examples**: update styles example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.10.0] - 2023-02-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
@@ -233,4 +421,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0
|
||||
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
|
||||
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
|
||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.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
|
||||
@@ -4,7 +4,8 @@ I welcome contributions of any kind with open arms. That said, please do stick t
|
||||
- Fix any `cargo clippy` warnings, using at least the default configuration.
|
||||
- Make sure your code is formatted using `cargo fmt`.
|
||||
- Keep any documentation up to date.
|
||||
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
- Please use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
This ensures your contributions are automatically included in the changelog.
|
||||
|
||||
|
||||
- For PRs:
|
||||
|
||||
1840
Cargo.lock
generated
1840
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
93
Cargo.toml
93
Cargo.toml
@@ -1,12 +1,17 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.10.0"
|
||||
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",
|
||||
@@ -14,16 +19,27 @@ default = [
|
||||
"music+all",
|
||||
"sys_info",
|
||||
"tray",
|
||||
"upower",
|
||||
"workspaces+all"
|
||||
]
|
||||
|
||||
cli = ["dep:clap", "ipc"]
|
||||
ipc = ["dep:serde_json"]
|
||||
|
||||
http = ["dep:reqwest"]
|
||||
|
||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"]
|
||||
"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"]
|
||||
"config+corn" = ["universal-config/corn"]
|
||||
"config+ron" = ["universal-config/ron"]
|
||||
|
||||
clipboard = ["nix"]
|
||||
|
||||
@@ -36,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"]
|
||||
@@ -47,51 +65,76 @@ workspaces = ["futures-util"]
|
||||
# core
|
||||
gtk = "0.17.0"
|
||||
gtk-layer-shell = "0.6.0"
|
||||
glib = "0.17.5"
|
||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
||||
glib = "0.17.10"
|
||||
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.16", features = ["env-filter"] }
|
||||
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.141", features = ["derive"] }
|
||||
indexmap = "1.9.1"
|
||||
dirs = "4.0.0"
|
||||
walkdir = "2.3.2"
|
||||
notify = { version = "5.0.0", default-features = false }
|
||||
wayland-client = "0.29.5"
|
||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
||||
universal-config = { version = "0.2.1", default_features = false }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
indexmap = "2.0.0"
|
||||
dirs = "5.0.1"
|
||||
walkdir = "2.4.0"
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
wayland-client = "0.30.2"
|
||||
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.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.14", optional = true }
|
||||
reqwest = { version = "0.11.20", optional = true }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.26.2", optional = true }
|
||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.19", 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.27.0", 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.14.1", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "0.3.0", optional = true }
|
||||
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
|
||||
futures-util = { version = "0.3.21", optional = true }
|
||||
|
||||
# shared
|
||||
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
|
||||
regex = { version = "1.9.5", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true } # music, sys_info
|
||||
133
README.md
133
README.md
@@ -1,49 +1,94 @@
|
||||
# Ironbar
|
||||
<h1 align="center" >--- Ironbar ---</h1>
|
||||
|
||||
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
|
||||
It uses GTK3 and gtk-layer-shell.
|
||||
<div align="center">
|
||||
<a href="https://github.com/JakeStanger/ironbar/releases">
|
||||
<img src="https://img.shields.io/crates/v/ironbar?label=version&style=for-the-badge" alt="Current version" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/actions/workflows/build.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/jakestanger/ironbar/build.yml?style=for-the-badge" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/issues">
|
||||
<img src="https://img.shields.io/github/issues/jakestanger/ironbar?style=for-the-badge" alt="Open issues" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/jakestanger/ironbar?style=for-the-badge" alt="License" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/ironbar">
|
||||
<img src="https://img.shields.io/crates/d/ironbar?label=crates.io%20downloads&style=for-the-badge" alt="Crates.io downloads" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
The bar can be styled to your liking using CSS and hot-loads style changes.
|
||||
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
A customisable and feature-rich GTK bar for wlroots compositors, written in Rust.
|
||||
|
||||
Ironbar is designed to support anything from a lightweight bar to a full desktop panel with ease.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
[Wiki](https://github.com/JakeStanger/ironbar/wiki)
|
||||
|
|
||||
[Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide)
|
||||
|
|
||||
[Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide)
|
||||
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
✨ Looking for a starting point, or want to show off? Head to [Show and tell](https://github.com/JakeStanger/ironbar/discussions/categories/show-and-tell) ✨
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- First-class support for Sway and Hyprland, but should (mostly) work on any wlroots compositor.
|
||||
- Fully themeable with CSS and hot-loaded styles.
|
||||
- Support for multiple configuration languages.
|
||||
- Popups used by widgets to show rich content and controls on click.
|
||||
- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
|
||||
- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
|
||||
- First-class support for Sway and Hyprland
|
||||
- Fully themeable with hot-loaded CSS
|
||||
- Popups to show rich content
|
||||
- Ability to create custom widgets, run scripts and embed dynamic content
|
||||
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
||||
- Support for multiple config languages
|
||||
|
||||
## Installation
|
||||
|
||||
### Cargo
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
cargo install ironbar
|
||||
```
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
### Arch Linux
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
|
||||
```sh
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
### Nix
|
||||
|
||||
### Nix Flake
|
||||
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
|
||||
|
||||
A flake is included with the repo which can be used with home-manager.
|
||||
```sh
|
||||
nix-shell -p ironbar
|
||||
```
|
||||
|
||||
#### Example
|
||||
#### Flake
|
||||
|
||||
Here is an example nix flake that uses Ironbar.
|
||||
A flake is included with the repo which can be used with Home Manager.
|
||||
|
||||
<details>
|
||||
<summary>Example usage</summary>
|
||||
|
||||
```nix
|
||||
{
|
||||
@@ -80,13 +125,14 @@ Here is an example nix flake that uses Ironbar.
|
||||
}
|
||||
```
|
||||
|
||||
#### Binary Caching
|
||||
</details>
|
||||
|
||||
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`
|
||||
in case you don't want to compile Ironbar.
|
||||
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
|
||||
|
||||
### Source
|
||||
|
||||
[repo](https://github.com/jakestanger/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
@@ -100,53 +146,36 @@ install target/release/ironbar ~/.local/bin/ironbar
|
||||
By default, all features are enabled.
|
||||
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
||||
|
||||
[repo](https://github.com/jakestanger/ironbar)
|
||||
|
||||
## Running
|
||||
|
||||
All of the above installation methods provide a binary called `ironbar`.
|
||||
Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
|
||||
See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
|
||||
|
||||
Ironbar can be launched using the `ironbar` binary.
|
||||
|
||||
Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
|
||||
|
||||
You can set the `IRONBAR_LOG` or `IRONBAR_FILE_LOG` environment variables to
|
||||
`error`, `warn`, `info`, `debug` or `trace` to configure the log output level.
|
||||
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||
|
||||
File output can be found at `~/.local/share/ironbar/error.log`.
|
||||
|
||||
## Configuration
|
||||
## Status
|
||||
|
||||
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||
and options for scaling complexity: you can use a single config across all monitors,
|
||||
or configure different/multiple bars per monitor.
|
||||
Ironbar is an **alpha** project.
|
||||
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
|
||||
|
||||
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
|
||||
If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
|
||||
|
||||
## Styling
|
||||
I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
|
||||
|
||||
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the
|
||||
file.
|
||||
|
||||
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
|
||||
|
||||
## Project Status
|
||||
|
||||
This project is in alpha, but should be usable.
|
||||
Everything that is implemented works and should be documented.
|
||||
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
|
||||
|
||||
There is currently room for lots more modules, and lots more configuration options for the existing modules.
|
||||
The current configuration schema is not set in stone and breaking changes could come along at any point;
|
||||
until the project matures I am more interested in ease of use than backwards compatibility.
|
||||
|
||||
A few bugs do exist, and I am sure there are plenty more to be found.
|
||||
|
||||
The project will be *actively developed* as I am using it on my daily driver.
|
||||
Bugs will be fixed, features will be added, code will be refactored.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
|
||||
All are welcome, but I ask a few basic things to help make things easier. Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
||||
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
||||
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
||||
|
||||
@@ -12,6 +12,7 @@ install target/release/ironbar ~/.local/bin/ironbar
|
||||
## Build requirements
|
||||
|
||||
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
|
||||
You also need rust; only the latest stable version is supported.
|
||||
|
||||
### Arch
|
||||
|
||||
@@ -22,7 +23,9 @@ pacman -S gtk3 gtk-layer-shell
|
||||
### Ubuntu/Debian
|
||||
|
||||
```shell
|
||||
apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
||||
# for http support
|
||||
apt install libssl-dev
|
||||
```
|
||||
|
||||
### Fedora
|
||||
@@ -55,11 +58,14 @@ 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. |
|
||||
| config+toml | Enables configuration support for TOML. |
|
||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
|
||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
|
||||
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
|
||||
| **Modules** | |
|
||||
| clipboard | Enables the `clipboard` module. |
|
||||
| clock | Enables the `clock` module. |
|
||||
@@ -68,6 +74,7 @@ cargo build --release --no-default-features \
|
||||
| music+mpd | Enables the `music` module with MPD support. |
|
||||
| sys_info | Enables the `sys_info` module. |
|
||||
| tray | Enables the `tray` module. |
|
||||
| upower | Enables the `upower` module. |
|
||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||
|
||||
@@ -267,19 +267,22 @@ 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 |
|
||||
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `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. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
| 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. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `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. |
|
||||
|
||||
### 3.2 Module-level options
|
||||
|
||||
@@ -288,12 +291,33 @@ For details on available modules and each of their config options, check the sid
|
||||
|
||||
For information on the `Script` type, and embedding scripts in strings, see [here](script).
|
||||
|
||||
#### Events
|
||||
|
||||
| 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. |
|
||||
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
|
||||
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
|
||||
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
|
||||
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
|
||||
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
|
||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
|
||||
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
|
||||
|
||||
#### Visibility
|
||||
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
|
||||
#### Appearance
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
|
||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
|
||||
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
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.
|
||||
@@ -4,17 +4,39 @@ To style the bar, create a file at `~/.config/ironbar/style.css`.
|
||||
|
||||
Style changes are hot-loaded so there is no need to reload the bar.
|
||||
|
||||
A reminder: since the bar is GTK-based, it uses GTK's implementation of CSS,
|
||||
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html),
|
||||
which only includes a subset of the full web spec (plus a few non-standard properties).
|
||||
|
||||
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 |
|
||||
| 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.
|
||||
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
|
||||
|
||||
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
|
||||
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
|
||||
|
||||
You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
|
||||
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
|
||||
|
||||
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
|
||||
|
||||
```css
|
||||
@define-color color_bg #2d2d2d;
|
||||
|
||||
box, menubar {
|
||||
background-color: @color_bg;
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
@@ -21,9 +27,11 @@
|
||||
- [Clock](clock)
|
||||
- [Custom](custom)
|
||||
- [Focused](focused)
|
||||
- [Label](label)
|
||||
- [Launcher](launcher)
|
||||
- [Music](music)
|
||||
- [Script](script)
|
||||
- [Sys_Info](sys-info)
|
||||
- [Tray](tray)
|
||||
- [Workspaces](workspaces)
|
||||
- [Upower](upower)
|
||||
- [Workspaces](workspaces)
|
||||
|
||||
@@ -9,16 +9,15 @@ Supports plain text and images.
|
||||
|
||||
> Type: `clipboard`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `icon` | `string/image` | `` | Icon to show on the widget button. |
|
||||
| `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.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.
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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.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. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -83,11 +82,16 @@ end:
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------------|------------------------------------------------------|
|
||||
| `#clipboard` | Clipboard widget. |
|
||||
| `#clipboard .btn` | Clipboard widget button. |
|
||||
| `#popup-clipboard` | Clipboard popup box. |
|
||||
| `#popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||
| `#popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||
| `#popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
||||
| `#popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
||||
| `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
||||
| `.clipboard` | Clipboard widget. |
|
||||
| `.clipboard .btn` | Clipboard widget button. |
|
||||
| `.clipboard .btn .icon` | Clipboard widget button icon (any type). |
|
||||
| `.clipboard .btn .text-icon` | Clipboard widget button icon (textual only). |
|
||||
| `.clipboard .btn .image` | Clipboard widget button icon (image only). |
|
||||
| `.popup-clipboard` | Clipboard popup box. |
|
||||
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
||||
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
||||
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -8,9 +8,13 @@ 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> |
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| `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>
|
||||
@@ -71,7 +75,9 @@ end:
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------|------------------------------------------------------------------------------------|
|
||||
| `#clock` | Clock widget button |
|
||||
| `#popup-clock` | Clock popup box |
|
||||
| `#popup-clock #calendar-clock` | Clock inside the popup |
|
||||
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||
| `.clock` | Clock widget button |
|
||||
| `.popup-clock` | Clock popup box |
|
||||
| `.popup-clock .calendar-clock` | Clock inside the popup |
|
||||
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -1,7 +1,10 @@
|
||||
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
|
||||
|
||||
@@ -10,29 +13,140 @@ Labels can display dynamic content from scripts, and buttons can interact with t
|
||||
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
|
||||
It is well worth looking at the examples.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|------------|---------|--------------------------------------|
|
||||
| `class` | `string` | `null` | Container class name. |
|
||||
| `bar` | `Widget[]` | `null` | List of widgets to add to the bar. |
|
||||
| `popup` | `Widget[]` | `[]` | List of widgets to add to the popup. |
|
||||
|
||||
### `Widget`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------|
|
||||
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. |
|
||||
| `name` | `string` | `null` | Widget name. |
|
||||
| `class` | `string` | `null` | Widget class name. |
|
||||
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
|
||||
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
|
||||
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
|
||||
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
|
||||
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
|
||||
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
|
||||
There are many widget types, each with their own config options.
|
||||
You can think of these like HTML elements and their attributes.
|
||||
|
||||
### Labels
|
||||
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. |
|
||||
| `name` | `string` | `null` | Widget name. |
|
||||
| `class` | `string` | `null` | Widget class name. |
|
||||
|
||||
#### Box
|
||||
|
||||
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. |
|
||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||
|
||||
#### Label
|
||||
|
||||
A text label. Pango markup is supported.
|
||||
|
||||
> Type `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
|
||||
#### Button
|
||||
|
||||
A clickable button, which can run a command when clicked.
|
||||
|
||||
> Type `button`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `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
|
||||
|
||||
An image or icon from disk or http.
|
||||
|
||||
> Type `image`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
||||
| `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
|
||||
|
||||
A draggable slider.
|
||||
|
||||
> Type: `slider`
|
||||
|
||||
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 |
|
||||
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `max` | `float` | `100` | Maximum slider value. |
|
||||
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
||||
|
||||
The example slider widget below shows a volume control for MPC,
|
||||
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
||||
|
||||
```corn
|
||||
$slider = {
|
||||
type = "custom"
|
||||
bar = [
|
||||
{
|
||||
type = "slider"
|
||||
length = 100
|
||||
max = 100
|
||||
on_change="!mpc volume ${0%.*}"
|
||||
value = "200:mpc volume | cut -d ':' -f2 | cut -d '%' -f1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Progress
|
||||
|
||||
A progress bar.
|
||||
|
||||
> Type: `progress`
|
||||
|
||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
|
||||
The example below shows progress for the current playing song in MPD,
|
||||
and displays the elapsed/length timestamps as a label above:
|
||||
|
||||
```corn
|
||||
$progress = {
|
||||
type = "custom"
|
||||
bar = [
|
||||
{
|
||||
type = "progress"
|
||||
value = "500:mpc | sed -n 2p | awk '{ print $4 }' | grep -Eo '[0-9]+' || echo 0"
|
||||
label = "{{500:mpc | sed -n 2p | awk '{ print $3 }'}} elapsed"
|
||||
length = 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Label Attributes
|
||||
|
||||
> ℹ This is different to the `label` widget, although applies to it.
|
||||
|
||||
Any widgets with a `label` attribute support embedded scripts,
|
||||
meaning you can interpolate text from scripts to dynamically show content.
|
||||
|
||||
Labels can interpolate text from scripts to dynamically show content.
|
||||
This can be done by including scripts in `{{double braces}}` using the shorthand script syntax.
|
||||
|
||||
For example, the following label would output your system uptime, updated every 30 seconds.
|
||||
@@ -52,6 +166,9 @@ To execute shell commands, prefix them with an `!`.
|
||||
For example, if you want to run `~/.local/bin/my-script.sh` on click,
|
||||
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
|
||||
|
||||
Some widgets provide a value when they run the command, such as `slider`.
|
||||
This is passed as an argument and can be accessed using `$0`.
|
||||
|
||||
The following bar commands are supported:
|
||||
|
||||
- `popup:toggle`
|
||||
@@ -238,27 +355,32 @@ end:
|
||||
|
||||
```corn
|
||||
let {
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" 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 = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
popup = [ {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" 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 = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
} ]
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
} in {
|
||||
end = [ $power_menu ]
|
||||
@@ -269,8 +391,13 @@ let {
|
||||
|
||||
## Styling
|
||||
|
||||
Since the widgets are all custom, you can target them using `#name` and `.class`.
|
||||
Since the widgets are all custom, you can use their `name` and `class` attributes, then target them using `#name` and `.class`.
|
||||
|
||||
| Selector | Description |
|
||||
|-----------|-------------------------|
|
||||
| `#custom` | Custom widget container |
|
||||
The following top-level selectors are always available:
|
||||
|
||||
| Selector | Description |
|
||||
|-----------------|--------------------------------|
|
||||
| `.custom` | Custom widget container. |
|
||||
| `.popup-custom` | Custom widget popup container. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -7,15 +7,15 @@ 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. |
|
||||
| `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. |
|
||||
| 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. |
|
||||
| `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. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -86,8 +86,10 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------|--------------------|
|
||||
| `#focused` | Focused widget box |
|
||||
| `#focused #icon` | App icon |
|
||||
| `#focused #label` | App name |
|
||||
| Selector | Description |
|
||||
|-------------------|--------------------|
|
||||
| `.focused` | Focused widget box |
|
||||
| `.focused .icon` | App icon |
|
||||
| `.focused .label` | App name |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
75
docs/modules/Label.md
Normal file
75
docs/modules/Label.md
Normal file
@@ -0,0 +1,75 @@
|
||||
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` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "label"
|
||||
label: "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|----------|------------------------------------------------------------------------------------|
|
||||
| `.label` | Label widget |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -14,6 +14,7 @@ Optionally displays a launchable set of favourites.
|
||||
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
|
||||
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
|
||||
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -89,10 +90,12 @@ start:
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------------------|--------------------------|
|
||||
| `#launcher` | Launcher widget box |
|
||||
| `#launcher .item` | App button |
|
||||
| `#launcher .item.open` | App button (open app) |
|
||||
| `#launcher .item.focused` | App button (focused app) |
|
||||
| `#launcher .item.urgent` | App button (urgent app) |
|
||||
| `#launcher-popup` | Popup container |
|
||||
| `#launcher-popup .popup-item` | Window button in popup |
|
||||
| `.launcher` | Launcher widget box |
|
||||
| `.launcher .item` | App button |
|
||||
| `.launcher .item.open` | App button (open app) |
|
||||
| `.launcher .item.focused` | App button (focused app) |
|
||||
| `.launcher .item.urgent` | App button (urgent app) |
|
||||
| `.popup-launcher` | Popup container |
|
||||
| `.popup-launcher .popup-item` | Window button in popup |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -11,24 +11,27 @@ 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. |
|
||||
| `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.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. |
|
||||
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
| | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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.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` 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. |
|
||||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
|
||||
See [here](images) for information on images.
|
||||
|
||||
@@ -125,30 +128,46 @@ 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
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------------------------|------------------------------------------|
|
||||
| `#music` | Tray widget button |
|
||||
| `#popup-music` | Popup box |
|
||||
| `#popup-music #album-art` | Album art image inside popup box |
|
||||
| `#popup-music #title` | Track title container inside popup box |
|
||||
| `#popup-music #title .icon` | Track title icon label inside popup box |
|
||||
| `#popup-music #title .label` | Track title label inside popup box |
|
||||
| `#popup-music #album` | Track album container inside popup box |
|
||||
| `#popup-music #album .icon` | Track album icon label inside popup box |
|
||||
| `#popup-music #album .label` | Track album label inside popup box |
|
||||
| `#popup-music #artist` | Track artist container inside popup box |
|
||||
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
|
||||
| `#popup-music #artist .label` | Track artist label inside popup box |
|
||||
| `#popup-music #controls` | Controls container inside popup box |
|
||||
| `#popup-music #controls #btn-prev` | Previous button inside popup box |
|
||||
| `#popup-music #controls #btn-play` | Play button inside popup box |
|
||||
| `#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 |
|
||||
| Selector | Description |
|
||||
|---------------------------------------------|-------------------------------------------------------|
|
||||
| `.music` | Tray widget button |
|
||||
| `.music .contents` | Tray widget button contents box |
|
||||
| `.music .contents .icon` | Tray widget button icon (any type) |
|
||||
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
|
||||
| `.music .contents .image` | Tray widget button icon (image only) |
|
||||
| `.popup-music` | Popup box |
|
||||
| `.popup-music .album-art` | Album art image inside popup box |
|
||||
| `.popup-music .title` | Track title container inside popup box |
|
||||
| `.popup-music .title .icon-box` | Track title icon container inside popup box |
|
||||
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
|
||||
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
|
||||
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
|
||||
| `.popup-music .title .label` | Track title label inside popup box |
|
||||
| `.popup-music .album` | Track album container inside popup box |
|
||||
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
|
||||
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
|
||||
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
|
||||
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
|
||||
| `.popup-music .album .label` | Track album label inside popup box |
|
||||
| `.popup-music .artist` | Track artist container inside popup box |
|
||||
| `.popup-music .artist .icon-box` | Track artist icon container inside popup box |
|
||||
| `.popup-music .artist .icon-box .icon` | Track artist icon inside its container (any type) |
|
||||
| `.popup-music .artist .icon-box .text-icon` | Track artist icon inside its container (textual only) |
|
||||
| `.popup-music .artist .icon-box .image` | Track artist icon inside its container (image only) |
|
||||
| `.popup-music .artist .label` | Track artist label inside popup box |
|
||||
| `.popup-music .controls` | Controls container inside popup box |
|
||||
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
|
||||
| `.popup-music .controls .btn-play` | Play button inside popup box |
|
||||
| `.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` | 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`
|
||||
@@ -82,6 +85,8 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------|---------------------|
|
||||
| `#script` | Script widget label |
|
||||
| Selector | Description |
|
||||
|-----------|---------------------|
|
||||
| `.script` | Script widget label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -168,9 +168,13 @@ The following tokens can be used in the `format` configuration option:
|
||||
| `{load_average:15}` | 15-minute load average. |
|
||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
||||
|
||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|------------------|------------------------------|
|
||||
| `#sysinfo` | Sysinfo widget box |
|
||||
| `#sysinfo #item` | Individual information label |
|
||||
| `.sysinfo` | Sysinfo widget box |
|
||||
| `.sysinfo .item` | Individual information label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
|
||||
@@ -60,5 +60,7 @@ end:
|
||||
|
||||
| Selector | Description |
|
||||
|---------------|------------------|
|
||||
| `#tray` | Tray widget box |
|
||||
| `#tray .item` | Tray icon button |
|
||||
| `.tray` | Tray widget box |
|
||||
| `.tray .item` | Tray icon button |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
84
docs/modules/Upower.md
Normal file
84
docs/modules/Upower.md
Normal file
@@ -0,0 +1,84 @@
|
||||
Displays system power information such as the battery percentage, and estimated time to empty.
|
||||
|
||||
`TODO: ADD SCREENSHOT`
|
||||
|
||||
[//]: # ()
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> 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>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "upower",
|
||||
"format": "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "upower"
|
||||
format: "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------------------|--------------------------------|
|
||||
| `.upower` | Upower widget container. |
|
||||
| `.upower .button` | Upower widget button. |
|
||||
| `.upower .button .contents` | Upower widget button contents. |
|
||||
| `.upower .button .icon` | Upower widget battery icon. |
|
||||
| `.upower .button .label` | Upower widget button label. |
|
||||
| `.popup-upower` | Upower popup box. |
|
||||
| `.popup-upower .upower-details` | Label inside the popup. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -8,11 +8,14 @@ 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. |
|
||||
| `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. |
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -27,6 +30,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
"2": "",
|
||||
"3": ""
|
||||
},
|
||||
"favorites": ["1", "2", "3"],
|
||||
"all_monitors": false
|
||||
}
|
||||
]
|
||||
@@ -42,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 = ""
|
||||
@@ -62,6 +67,10 @@ end:
|
||||
1: ""
|
||||
2: ""
|
||||
3: ""
|
||||
favorites:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
all_monitors: false
|
||||
```
|
||||
|
||||
@@ -78,6 +87,7 @@ end:
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
favorites = [ "1" "2" "3" ]
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
@@ -88,8 +98,15 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-----------------------------|--------------------------------------|
|
||||
| `#workspaces` | Workspaces widget box |
|
||||
| `#workspaces .item` | Workspace button |
|
||||
| `#workspaces .item.focused` | Workspace button (workspace focused) |
|
||||
| Selector | Description |
|
||||
|--------------------------------|--------------------------------------|
|
||||
| `.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) |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
|
||||
@@ -3,7 +3,7 @@ let {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Games = "icon:steam"
|
||||
@@ -15,7 +15,7 @@ let {
|
||||
|
||||
$launcher = {
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord" "Steam"]
|
||||
favorites = ["firefox" "discord" "steam"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
}
|
||||
@@ -67,6 +67,8 @@ let {
|
||||
|
||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||
|
||||
$label = { type = "label" label = "random num: {{500:echo FIXME}}" }
|
||||
|
||||
// -- begin custom --
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
@@ -97,7 +99,7 @@ let {
|
||||
}
|
||||
// -- end custom --
|
||||
|
||||
$left = [ $workspaces $launcher ]
|
||||
$left = [ $workspaces $launcher $label ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
|
||||
}
|
||||
in {
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "ﭮ",
|
||||
"1": "",
|
||||
"2": "icon:firefox",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
@@ -121,11 +121,15 @@
|
||||
"favorites": [
|
||||
"firefox",
|
||||
"discord",
|
||||
"Steam"
|
||||
"steam"
|
||||
],
|
||||
"show_icons": true,
|
||||
"show_names": false,
|
||||
"type": "launcher"
|
||||
},
|
||||
{
|
||||
"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,73 +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 FIXME}}"
|
||||
type = "label"
|
||||
|
||||
|
||||
@@ -1,85 +1,87 @@
|
||||
anchor_to_edges: true
|
||||
end:
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- 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}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- 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}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
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: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
icon_theme: Paper
|
||||
position: bottom
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1': ﭮ
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- Steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1':
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo FIXME}}'
|
||||
type: label
|
||||
|
||||
|
||||
@@ -1,240 +1,201 @@
|
||||
@define-color color_bg #2d2d2d;
|
||||
@define-color color_bg_dark #1c1c1c;
|
||||
@define-color color_border #424242;
|
||||
@define-color color_border_active #6699cc;
|
||||
@define-color color_text #ffffff;
|
||||
@define-color color_urgent #8f0a0a;
|
||||
|
||||
/* -- base styles -- */
|
||||
|
||||
* {
|
||||
/* `otf-font-awesome` is required to be installed for icons */
|
||||
font-family: Noto Sans Nerd Font, sans-serif;
|
||||
font-size: 16px;
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
box, menubar, button {
|
||||
background-color: @color_bg;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
button, label {
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
#bar {
|
||||
border-top: 1px solid #424242;
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.background, .container {
|
||||
background-color: #2d2d2d;
|
||||
.popup {
|
||||
border: 1px solid @color_border;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#workspaces .item {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 0;
|
||||
|
||||
/* -- clipboard -- */
|
||||
|
||||
.clipboard {
|
||||
margin-left: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: #1c1c1c;
|
||||
.popup-clipboard .item {
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
#workspaces *:not(.focused):hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
#launcher .item {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
margin-right: 4px;
|
||||
}
|
||||
/* -- clock -- */
|
||||
|
||||
#launcher .item:not(.focused):hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#launcher .open {
|
||||
border-bottom: 2px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .focused {
|
||||
color: white;
|
||||
background-color: #1c1c1c;
|
||||
border-bottom: 4px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .urgent {
|
||||
color: white;
|
||||
background-color: #8f0a0a;
|
||||
}
|
||||
|
||||
#popup-launcher .popup-item {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#popup-launcher .popup-item:hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#popup-launcher .popup-item:not(:first-child) {
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
#clock {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
.clock {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#clock:hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#script {
|
||||
padding-left: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sysinfo {
|
||||
margin-left: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sysinfo #item {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#tray {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#tray .item {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
#music {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clock {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#calendar-clock {
|
||||
color: white;
|
||||
.popup-clock .calendar-clock {
|
||||
color: @color_text;
|
||||
font-size: 2.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-clock .calendar {
|
||||
background-color: @color_bg;
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
#calendar .header {
|
||||
.popup-clock .calendar .header {
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #424242;
|
||||
border-top: 1px solid @color_border;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#calendar:selected {
|
||||
background-color: #6699cc;
|
||||
.popup-clock .calendar:selected {
|
||||
background-color: @color_border_active;
|
||||
}
|
||||
|
||||
#music:hover {
|
||||
background-color: #1c1c1c;
|
||||
|
||||
/* -- launcher -- */
|
||||
|
||||
.launcher .item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#popup-music {
|
||||
color: white;
|
||||
padding: 1em;
|
||||
.launcher .item:not(.focused):hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
#popup-music #album-art {
|
||||
.launcher .open {
|
||||
border-bottom: 1px solid @color_text;
|
||||
}
|
||||
|
||||
.launcher .focused {
|
||||
border-bottom: 2px solid @color_border_active;
|
||||
}
|
||||
|
||||
.launcher .urgent {
|
||||
border-bottom-color: @color_urgent;
|
||||
}
|
||||
|
||||
.popup-launcher {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popup-launcher .popup-item:not(:first-child) {
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
|
||||
/* -- music -- */
|
||||
|
||||
.music:hover * {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.popup-music .album-art {
|
||||
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;
|
||||
}
|
||||
|
||||
#popup-music #controls * {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-music .controls *:disabled {
|
||||
color: @color_border;
|
||||
}
|
||||
|
||||
#popup-music #controls *:disabled {
|
||||
color: #424242;
|
||||
.popup-music .volume .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#popup-music #volume > box:last-child label {
|
||||
margin-left: 6px;
|
||||
.popup-music .volume .icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#focused {
|
||||
color: white;
|
||||
.popup-music .progress .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.power-menu {
|
||||
/* -- script -- */
|
||||
|
||||
.script {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* -- sys_info -- */
|
||||
|
||||
.sysinfo {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.power-menu #power-btn {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
.sysinfo .item {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.power-menu #power-btn:hover {
|
||||
background-color: #1c1c1c;
|
||||
|
||||
/* -- tray -- */
|
||||
|
||||
.tray {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.popup-power-menu {
|
||||
padding: 1em;
|
||||
|
||||
/* -- workspaces -- */
|
||||
|
||||
.workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.workspaces .item:hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
|
||||
/* -- custom: power menu -- */
|
||||
|
||||
.popup-power-menu #header {
|
||||
color: white;
|
||||
font-size: 1.4em;
|
||||
border-bottom: 1px solid white;
|
||||
padding-bottom: 0.4em;
|
||||
margin-bottom: 0.8em;
|
||||
margin-bottom: 0.6em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid white;
|
||||
border: 1px solid @color_border;
|
||||
padding: 0.6em 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn + .power-btn {
|
||||
margin-left: 1em;
|
||||
.popup-power-menu #buttons > *:nth-child(1) .power-btn {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn:hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#clipboard * {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
#popup-clipboard {
|
||||
padding: 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#popup-clipboard .item {
|
||||
border-bottom: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clipboard .btn > *:nth-child(2) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#popup-clipboard .btn-remove {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
border-left: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clipboard .btn-remove:hover {
|
||||
color: #fcc;
|
||||
}
|
||||
175
flake.lock
generated
175
flake.lock
generated
@@ -1,12 +1,54 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"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": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -15,13 +57,63 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"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": 1680213900,
|
||||
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
|
||||
"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": "e3652e0735fbec227f342712f180f4f21f0594f2",
|
||||
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -33,23 +125,29 @@
|
||||
},
|
||||
"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": 1680229280,
|
||||
"narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=",
|
||||
"lastModified": 1691374719,
|
||||
"narHash": "sha256-HCodqnx1Mi2vN4f3hjRPc7+lSQy18vRn8xWW68GeQOg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "aa480d799023141e1b9e5d6108700de63d9ad002",
|
||||
"rev": "b520a3889b24aaf909e287d19d406862ced9ffc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -57,6 +155,57 @@
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay_2": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1693447852,
|
||||
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"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",
|
||||
|
||||
65
flake.nix
65
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,11 +57,33 @@
|
||||
(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");
|
||||
inherit rustPlatform;
|
||||
};
|
||||
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 (
|
||||
system: let
|
||||
@@ -57,6 +94,18 @@
|
||||
default = self.packages.${system}.ironbar;
|
||||
}
|
||||
);
|
||||
apps = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
in {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
ironbar = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
});
|
||||
devShells = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
@@ -70,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";
|
||||
|
||||
107
nix/default.nix
107
nix/default.nix
@@ -1,36 +1,95 @@
|
||||
{
|
||||
gtk3,
|
||||
gdk-pixbuf,
|
||||
librsvg,
|
||||
webp-pixbuf-loader,
|
||||
gobject-introspection,
|
||||
glib-networking,
|
||||
glib,
|
||||
shared-mime-info,
|
||||
gsettings-desktop-schemas,
|
||||
wrapGAppsHook,
|
||||
gtk-layer-shell,
|
||||
gnome,
|
||||
libxkbcommon,
|
||||
openssl,
|
||||
pkg-config,
|
||||
hicolor-icon-theme,
|
||||
rustPlatform,
|
||||
lib,
|
||||
version ? "git",
|
||||
features ? [],
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
builderName ? "nix",
|
||||
builder ? {},
|
||||
}: let
|
||||
basePkg = rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
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 = [
|
||||
gtk3
|
||||
];
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# Thumbnailers
|
||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||
)
|
||||
'';
|
||||
passthru = {
|
||||
updateScript = gnome.updateScript {
|
||||
packageName = pname;
|
||||
attrPath = "gnome.${pname}";
|
||||
};
|
||||
};
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
};
|
||||
};
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
nativeBuildInputs = [pkg-config];
|
||||
buildInputs = [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl];
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "Hyprland";
|
||||
};
|
||||
}
|
||||
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=";
|
||||
})
|
||||
|
||||
5
scripts/generate-examples.sh
Executable file
5
scripts/generate-examples.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
corn examples/config.corn -t json > examples/config.json
|
||||
corn examples/config.corn -t toml > examples/config.toml
|
||||
corn examples/config.corn -t yaml > examples/config.yaml
|
||||
72
scripts/migrate-styles.sh
Executable file
72
scripts/migrate-styles.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Migrates CSS selectors from widget names to CSS classes.
|
||||
# These changed as part of the 0.12 release.
|
||||
|
||||
# ⚠ This script will **NOT** check for custom styles and may mangle them!
|
||||
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
|
||||
|
||||
style_path="$HOME/.config/ironbar/style.css"
|
||||
|
||||
# general
|
||||
sed -i 's/#icon/.icon/g' "$style_path"
|
||||
sed -i 's/#label/.label/g' "$style_path"
|
||||
sed -i 's/#image/.image/g' "$style_path"
|
||||
|
||||
# clipboard
|
||||
sed -i 's/#clipboard/.clipboard/g' "$style_path"
|
||||
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
|
||||
|
||||
# clock
|
||||
sed -i 's/#clock/.clock/g' "$style_path"
|
||||
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
|
||||
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
|
||||
sed -i 's/#calendar/.calendar/g' "$style_path"
|
||||
|
||||
# custom
|
||||
sed -i 's/#custom/.custom/g' "$style_path"
|
||||
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
|
||||
|
||||
# focused
|
||||
sed -i 's/#focused/.focused/g' "$style_path"
|
||||
|
||||
# launcher
|
||||
sed -i 's/#launcher/.launcher/g' "$style_path"
|
||||
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
|
||||
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
|
||||
|
||||
# music
|
||||
sed -i 's/#music/.music/g' "$style_path"
|
||||
sed -i 's/#contents/.contents/g' "$style_path"
|
||||
sed -i 's/#popup-music/.popup-music/g' "$style_path"
|
||||
sed -i 's/#album-art/.album-art/g' "$style_path"
|
||||
sed -i 's/#title/.title/g' "$style_path"
|
||||
sed -i 's/#album/.album/g' "$style_path"
|
||||
sed -i 's/#artist/.artist/g' "$style_path"
|
||||
sed -i 's/#controls/.controls/g' "$style_path"
|
||||
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
|
||||
sed -i 's/#btn-play/.btn-play/g' "$style_path"
|
||||
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
|
||||
sed -i 's/#btn-next/.btn-next/g' "$style_path"
|
||||
sed -i 's/#volume/.volume/g' "$style_path"
|
||||
sed -i 's/#slider/.slider/g' "$style_path"
|
||||
|
||||
# script
|
||||
sed -i 's/#script/.script/g' "$style_path"
|
||||
|
||||
# sys_info
|
||||
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
|
||||
# tray
|
||||
sed -i 's/#tray/.tray/g' "$style_path"
|
||||
|
||||
# upower
|
||||
sed -i 's/#upower/.upower/g' "$style_path"
|
||||
sed -i 's/#button/.button/g' "$style_path"
|
||||
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
|
||||
sed -i 's/#upower-details/.upower-details/g' "$style_path"
|
||||
|
||||
# workspaces
|
||||
sed -i 's/#workspaces/.workspaces/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
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
|
||||
];
|
||||
}
|
||||
303
src/bar.rs
303
src/bar.rs
@@ -1,18 +1,17 @@
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::config::{BarPosition, CommonConfig, MarginConfig, ModuleConfig};
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
||||
use crate::modules::{
|
||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
||||
};
|
||||
use crate::popup::Popup;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{await_sync, read_lock, send, write_lock, Config};
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{Config, GlobalState};
|
||||
use color_eyre::Result;
|
||||
use gtk::gdk::{EventMask, Monitor, ScrollDirection};
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, EventBox, IconTheme, Orientation, Widget};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, trace};
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Creates a new window for a bar,
|
||||
/// sets it up and adds its widgets.
|
||||
@@ -21,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,
|
||||
@@ -57,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(|_, _| {
|
||||
@@ -138,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,
|
||||
@@ -147,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,42 +183,59 @@ 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 = Rc::new(RefCell::new(popup));
|
||||
|
||||
if let Some(modules) = config.start {
|
||||
let info = info!(ModuleLocation::Left);
|
||||
add_modules(left, modules, &info)?;
|
||||
add_modules(left, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
if let Some(modules) = config.center {
|
||||
let info = info!(ModuleLocation::Center);
|
||||
add_modules(center, modules, &info)?;
|
||||
add_modules(center, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
if let Some(modules) = config.end {
|
||||
let info = info!(ModuleLocation::Right);
|
||||
add_modules(right, modules, &info)?;
|
||||
add_modules(right, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let result = BarLoadResult { popup };
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Adds modules into a provided GTK box,
|
||||
/// which should be one of its left, center or right containers.
|
||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
|
||||
let popup = Popup::new(info);
|
||||
let popup = Arc::new(RwLock::new(popup));
|
||||
fn add_modules(
|
||||
content: >k::Box,
|
||||
modules: Vec<ModuleConfig>,
|
||||
info: &ModuleInfo,
|
||||
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 = 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);
|
||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
||||
content.add(&container);
|
||||
setup_module_common_options(container, common);
|
||||
}};
|
||||
}
|
||||
|
||||
for (id, config) in modules.into_iter().enumerate() {
|
||||
for config in modules {
|
||||
let id = get_unique_usize();
|
||||
match config {
|
||||
#[cfg(feature = "clipboard")]
|
||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||
@@ -209,6 +243,7 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
||||
ModuleConfig::Clock(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Focused(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Label(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Launcher(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "music")]
|
||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||
@@ -217,6 +252,8 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "tray")]
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "upower")]
|
||||
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "workspaces")]
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
}
|
||||
@@ -224,217 +261,3 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a module and sets it up.
|
||||
/// This setup includes widget/popup content and event channels.
|
||||
fn create_module<TModule, TWidget, TSend, TRec>(
|
||||
module: TModule,
|
||||
id: usize,
|
||||
info: &ModuleInfo,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<TWidget>
|
||||
where
|
||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||
TWidget: IsA<Widget>,
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
|
||||
let channel = BridgeChannel::<ModuleUpdateEvent<TSend>>::new();
|
||||
let (ui_tx, ui_rx) = mpsc::channel::<TRec>(16);
|
||||
|
||||
module.spawn_controller(info, channel.create_sender(), ui_rx)?;
|
||||
|
||||
let context = WidgetContext {
|
||||
id,
|
||||
widget_rx: w_rx,
|
||||
popup_rx: p_rx,
|
||||
tx: channel.create_sender(),
|
||||
controller_tx: ui_tx,
|
||||
};
|
||||
|
||||
let name = TModule::name();
|
||||
|
||||
let module_parts = module.into_widget(context, info)?;
|
||||
module_parts.widget.set_widget_name(name);
|
||||
|
||||
let mut has_popup = false;
|
||||
if let Some(popup_content) = module_parts.popup {
|
||||
register_popup_content(popup, id, popup_content);
|
||||
has_popup = true;
|
||||
}
|
||||
|
||||
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
|
||||
|
||||
Ok(module_parts.widget)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Sets up the bridge channel receiver
|
||||
/// to pick up events from the controller, widget or popup.
|
||||
///
|
||||
/// Handles opening/closing popups
|
||||
/// and communicating update messages between controllers and widgets/popups.
|
||||
fn setup_receiver<TSend>(
|
||||
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
|
||||
w_tx: glib::Sender<TSend>,
|
||||
p_tx: glib::Sender<TSend>,
|
||||
popup: Arc<RwLock<Popup>>,
|
||||
name: &'static str,
|
||||
id: usize,
|
||||
has_popup: bool,
|
||||
) where
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
// some rare cases can cause the popup to incorrectly calculate its size on first open.
|
||||
// we can fix that by just force re-rendering it on its first open.
|
||||
let mut has_popup_opened = false;
|
||||
|
||||
channel.recv(move |ev| {
|
||||
match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
if has_popup {
|
||||
send!(p_tx, update.clone());
|
||||
}
|
||||
|
||||
send!(w_tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(geometry) => {
|
||||
debug!("Toggling popup for {} [#{}]", name, id);
|
||||
let popup = read_lock!(popup);
|
||||
if popup.is_visible() {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
|
||||
if !has_popup_opened {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
popup.hide();
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
|
||||
if !has_popup_opened {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
/// Takes a widget and adds it into a new `gtk::EventBox`.
|
||||
/// The event box container is returned.
|
||||
fn wrap_widget<W: IsA<Widget>>(widget: &W) -> EventBox {
|
||||
let container = EventBox::new();
|
||||
container.add_events(EventMask::SCROLL_MASK);
|
||||
container.add(widget);
|
||||
container
|
||||
}
|
||||
|
||||
/// Configures the module's container according to the common config options.
|
||||
fn setup_module_common_options(container: EventBox, common: CommonConfig) {
|
||||
common.show_if.map_or_else(
|
||||
|| {
|
||||
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(|(_, success)| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
});
|
||||
rx.attach(None, move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
} else {
|
||||
container.hide();
|
||||
};
|
||||
Continue(true)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let left_click_script = common.on_click_left.map(Script::new_polling);
|
||||
let middle_click_script = common.on_click_middle.map(Script::new_polling);
|
||||
let right_click_script = common.on_click_right.map(Script::new_polling);
|
||||
|
||||
container.connect_button_press_event(move |_, event| {
|
||||
let script = match event.button() {
|
||||
1 => left_click_script.as_ref(),
|
||||
2 => middle_click_script.as_ref(),
|
||||
3 => right_click_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-click script: {}", event.button());
|
||||
|
||||
match await_sync(async { script.get_output().await }) {
|
||||
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = common.on_scroll_up.map(Script::new_polling);
|
||||
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
|
||||
|
||||
container.connect_scroll_event(move |_, event| {
|
||||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-scroll script: {}", event.direction());
|
||||
|
||||
match await_sync(async { script.get_output().await }) {
|
||||
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
if let Some(tooltip) = common.tooltip {
|
||||
DynamicString::new(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 +1,12 @@
|
||||
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;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClipboardEvent {
|
||||
@@ -26,20 +26,33 @@ pub struct ClipboardClient {
|
||||
|
||||
impl ClipboardClient {
|
||||
fn new() -> Self {
|
||||
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
|
||||
trace!("Initializing clipboard client");
|
||||
|
||||
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
|
||||
let senders = arc_mut!(Vec::<(EventSender, usize)>::new());
|
||||
|
||||
let cache = arc_mut!(ClipboardCache::new());
|
||||
|
||||
{
|
||||
let senders = senders.clone();
|
||||
let cache = cache.clone();
|
||||
|
||||
spawn(async move {
|
||||
let mut rx = {
|
||||
let wl = wayland::get_client().await;
|
||||
let (mut rx, item) = {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_clipboard()
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
|
||||
lock!(cache).insert(item, senders.len());
|
||||
}
|
||||
|
||||
while let Ok(item) = rx.recv().await {
|
||||
debug!("Received clipboard item (ID: {})", item.id);
|
||||
|
||||
@@ -59,8 +72,7 @@ impl ClipboardClient {
|
||||
let iter = senders.iter();
|
||||
for (tx, sender_cache_size) in iter {
|
||||
if cache_size == *sender_cache_size {
|
||||
let mut cache = lock!(cache);
|
||||
let removed_id = cache
|
||||
let removed_id = lock!(cache)
|
||||
.remove_ref_first()
|
||||
.expect("Clipboard cache unexpectedly empty");
|
||||
try_send!(tx, ClipboardEvent::Remove(removed_id));
|
||||
@@ -83,18 +95,11 @@ impl ClipboardClient {
|
||||
Self { senders, cache }
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
||||
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
|
||||
let wl = wayland::get_client().await;
|
||||
wl.roundtrip();
|
||||
|
||||
{
|
||||
let mut cache = lock!(self.cache);
|
||||
|
||||
if let Some(item) = wl.get_clipboard() {
|
||||
cache.insert_or_inc_ref(item);
|
||||
}
|
||||
let cache = lock!(self.cache);
|
||||
|
||||
let iter = cache.iter();
|
||||
for (_, (item, _)) in iter {
|
||||
@@ -102,15 +107,12 @@ impl ClipboardClient {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut senders = lock!(self.senders);
|
||||
senders.push((tx, cache_size));
|
||||
}
|
||||
lock!(self.senders).push((tx, cache_size));
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub async fn copy(&self, id: usize) {
|
||||
pub fn copy(&self, id: usize) {
|
||||
debug!("Copying item with id {id}");
|
||||
|
||||
let item = {
|
||||
@@ -119,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);
|
||||
}
|
||||
|
||||
@@ -131,8 +134,7 @@ impl ClipboardClient {
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: usize) {
|
||||
let mut cache = lock!(self.cache);
|
||||
cache.remove(id);
|
||||
lock!(self.cache).remove(id);
|
||||
|
||||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
@@ -172,13 +174,6 @@ impl ClipboardCache {
|
||||
.map(|(item, _)| item)
|
||||
}
|
||||
|
||||
/// Inserts an entry with `ref_count` initial references,
|
||||
/// or increments the `ref_count` by 1 if it already exists.
|
||||
fn insert_or_inc_ref(&mut self, item: Arc<ClipboardItem>) {
|
||||
let mut item = self.cache.entry(item.id).or_insert((item, 0));
|
||||
item.1 += 1;
|
||||
}
|
||||
|
||||
/// Removes the entry with key `id`.
|
||||
/// This ignores references.
|
||||
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -6,4 +6,6 @@ pub mod compositor;
|
||||
pub mod music;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod system_tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
pub mod wayland;
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
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};
|
||||
@@ -8,7 +8,8 @@ use std::collections::HashSet;
|
||||
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};
|
||||
|
||||
@@ -18,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();
|
||||
|
||||
@@ -83,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,
|
||||
@@ -94,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()?;
|
||||
@@ -137,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()?;
|
||||
@@ -147,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,
|
||||
@@ -158,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,
|
||||
};
|
||||
|
||||
@@ -180,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 {
|
||||
@@ -222,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();
|
||||
|
||||
@@ -230,6 +276,14 @@ impl MusicClient for Client {
|
||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else {
|
||||
let status = Status {
|
||||
playlist_position: 0,
|
||||
playlist_length: 0,
|
||||
state: PlayerState::Stopped,
|
||||
volume_percent: None,
|
||||
};
|
||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||
}
|
||||
|
||||
rx
|
||||
@@ -246,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)
|
||||
@@ -259,7 +322,7 @@ impl From<Metadata> for Track {
|
||||
.and_then(mpris::MetadataValue::as_str_array)
|
||||
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
||||
track: value.track_number().map(|track| track as u64),
|
||||
cover_path: value.art_url().map(|s| s.to_string()),
|
||||
cover_path: value.art_url().map(string::ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,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,16 +1,17 @@
|
||||
use crate::{lock, send};
|
||||
use crate::unique_id::get_unique_usize;
|
||||
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::error;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
|
||||
|
||||
@@ -23,14 +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("ironbar").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();
|
||||
@@ -38,6 +41,8 @@ impl TrayEventReceiver {
|
||||
|
||||
spawn(async move {
|
||||
while let Ok(message) = host.recv().await {
|
||||
trace!("Received message: {message:?} ");
|
||||
|
||||
send!(b_tx, message.clone());
|
||||
let mut tray = lock!(tray);
|
||||
match message {
|
||||
@@ -46,9 +51,11 @@ impl TrayEventReceiver {
|
||||
item,
|
||||
menu,
|
||||
} => {
|
||||
debug!("Adding item with address '{address}'");
|
||||
tray.insert(address, (item, menu));
|
||||
}
|
||||
NotifierItemMessage::Remove { address } => {
|
||||
debug!("Removing item with address '{address}'");
|
||||
tray.remove(&address);
|
||||
}
|
||||
}
|
||||
@@ -99,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),
|
||||
|
||||
40
src/clients/upower.rs
Normal file
40
src/clients/upower.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use async_once::AsyncOnce;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use upower_dbus::UPowerProxy;
|
||||
use zbus::fdo::PropertiesProxy;
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||
let dbus = Box::pin(zbus::Connection::system())
|
||||
.await
|
||||
.expect("failed to create connection to system bus");
|
||||
|
||||
let device_proxy = UPowerProxy::new(&dbus)
|
||||
.await
|
||||
.expect("failed to create upower proxy");
|
||||
|
||||
let display_device = device_proxy
|
||||
.get_display_device()
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
|
||||
|
||||
let path = display_device.path().to_owned();
|
||||
|
||||
let proxy = PropertiesProxy::builder(&dbus)
|
||||
.destination("org.freedesktop.UPower")
|
||||
.expect("failed to set proxy destination address")
|
||||
.path(path)
|
||||
.expect("failed to set proxy path")
|
||||
.cache_properties(zbus::CacheProperties::No)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to build proxy");
|
||||
|
||||
Arc::new(proxy)
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
|
||||
DISPLAY_PROXY.get().await
|
||||
}
|
||||
@@ -1,79 +1,93 @@
|
||||
use super::wlr_foreign_toplevel::{
|
||||
handle::{ToplevelEvent, ToplevelInfo},
|
||||
manager::listen_for_toplevels,
|
||||
};
|
||||
use super::{DData, Env, ToplevelHandler};
|
||||
use crate::{error as err, send};
|
||||
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::{cached_broadcast, send};
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Report;
|
||||
use indexmap::IndexMap;
|
||||
use smithay_client_toolkit::environment::Environment;
|
||||
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
use smithay_client_toolkit::WaylandSource;
|
||||
use smithay_client_toolkit::registry::RegistryState;
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
use std::sync::mpsc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error};
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::globals::registry_queue_init;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{ConnectError, Display, EventQueue};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
||||
};
|
||||
use wayland_client::{Connection, WaylandSource};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
use super::{ClipboardItem};
|
||||
use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler};
|
||||
use crate::{read_lock, write_lock};
|
||||
use tokio::spawn;
|
||||
use super::ClipboardItem;
|
||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use std::sync::Arc;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
/// Sends a request for all the seats.
|
||||
/// 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.
|
||||
Toplevels,
|
||||
/// Sends a request for the current clipboard item.
|
||||
/// This is then sent on the `clipboard_init` channel.
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard,
|
||||
/// Copies the value to the clipboard
|
||||
#[cfg(feature = "clipboard")]
|
||||
CopyToClipboard(Arc<ClipboardItem>),
|
||||
/// Forces a dispatch, flushing any currently queued events
|
||||
Refresh,
|
||||
Roundtrip,
|
||||
}
|
||||
|
||||
pub struct WaylandClient {
|
||||
pub outputs: Vec<OutputInfo>,
|
||||
pub seats: Vec<WlSeat>,
|
||||
|
||||
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||
// 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")]
|
||||
clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>,
|
||||
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
|
||||
// Internal channels
|
||||
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
|
||||
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
|
||||
|
||||
request_tx: Sender<Request>,
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(super) async fn new() -> Self {
|
||||
let (output_tx, output_rx) = oneshot::channel();
|
||||
let (seat_tx, seat_rx) = oneshot::channel();
|
||||
|
||||
pub(super) fn new() -> Self {
|
||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||
|
||||
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
||||
let toplevels2 = toplevels.clone();
|
||||
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 (seat_tx, seat_rx) = mpsc::channel();
|
||||
|
||||
let toplevel_tx2 = toplevel_tx.clone();
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32);
|
||||
let clipboard = Arc::new(RwLock::new(None));
|
||||
let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
|
||||
let clipboard_tx2 = clipboard_tx.clone();
|
||||
}
|
||||
}
|
||||
@@ -82,85 +96,97 @@ impl WaylandClient {
|
||||
|
||||
// `queue` is not `Send` so we need to handle everything inside the task
|
||||
spawn_blocking(move || {
|
||||
let toplevels = toplevels2;
|
||||
let toplevel_tx = toplevel_tx2;
|
||||
#[cfg(feature = "clipboard")]
|
||||
let clipboard_tx = clipboard_tx2;
|
||||
|
||||
let (env, _display, queue) =
|
||||
Self::new_environment().expect("Failed to connect to Wayland compositor");
|
||||
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");
|
||||
|
||||
let qh = queue.handle();
|
||||
let mut event_loop =
|
||||
EventLoop::<DData>::try_new().expect("Failed to create new event loop");
|
||||
EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
|
||||
|
||||
WaylandSource::new(queue)
|
||||
.quick_insert(event_loop.handle())
|
||||
.expect("Failed to create Wayland source from queue")
|
||||
.insert(event_loop.handle())
|
||||
.expect("Failed to insert Wayland event queue into event loop");
|
||||
|
||||
let outputs = Self::get_outputs(&env);
|
||||
send!(output_tx, outputs);
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
let seats = env.get_all_seats();
|
||||
// Initialize the registry handling
|
||||
// so other parts of Smithay's client toolkit may bind globals.
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
|
||||
let output_delegate = OutputState::new(&globals, &qh);
|
||||
let seat_delegate = SeatState::new(&globals, &qh);
|
||||
|
||||
// TODO: Actually handle seats properly
|
||||
#[cfg(feature = "clipboard")]
|
||||
let default_seat = seats[0].detach();
|
||||
let data_control_device_manager_delegate =
|
||||
DataControlDeviceManagerState::bind(&globals, &qh)
|
||||
.expect("data device manager is not available");
|
||||
|
||||
send!(
|
||||
seat_tx,
|
||||
seats
|
||||
.into_iter()
|
||||
.map(|seat| seat.detach())
|
||||
.collect::<Vec<WlSeat>>()
|
||||
);
|
||||
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
|
||||
.expect("foreign toplevel manager is not available");
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
.insert_source(ev_rx, move |event, _metadata, ddata| {
|
||||
// let env = &ddata.env;
|
||||
let mut env = Environment {
|
||||
registry_state,
|
||||
output_state: output_delegate,
|
||||
seat_state: seat_delegate,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_device_manager_state: data_control_device_manager_delegate,
|
||||
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
|
||||
seats: vec![],
|
||||
handles: HashMap::new(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: crate::arc_mut!(None),
|
||||
output_tx,
|
||||
toplevel_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_devices: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
selection_offers: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
copy_paste_sources: vec![],
|
||||
loop_handle: event_loop.handle(),
|
||||
};
|
||||
|
||||
loop_handle
|
||||
.insert_source(ev_rx, move |event, _metadata, env| {
|
||||
trace!("{event:?}");
|
||||
match event {
|
||||
Event::Msg(Request::Refresh) => debug!("Received refresh event"),
|
||||
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
|
||||
Event::Msg(Request::Seats) => {
|
||||
trace!("Receive get seats request");
|
||||
send!(seat_tx, env.seats.clone());
|
||||
}
|
||||
Event::Msg(Request::Toplevels) => {
|
||||
trace!("Receive get toplevels request");
|
||||
send!(toplevel_init_tx, env.handles.clone());
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::Clipboard) => {
|
||||
trace!("Receive get clipboard requests");
|
||||
let clipboard = lock!(env.clipboard).clone();
|
||||
send!(clipboard_init_tx, clipboard);
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::CopyToClipboard(value)) => {
|
||||
super::wlr_data_control::copy_to_clipboard(
|
||||
&ddata.env,
|
||||
&default_seat,
|
||||
&value,
|
||||
)
|
||||
.expect("Failed to copy to clipboard");
|
||||
env.copy_to_clipboard(value, &qh);
|
||||
}
|
||||
Event::Closed => panic!("Channel unexpectedly closed"),
|
||||
}
|
||||
})
|
||||
.expect("Failed to insert channel into event queue");
|
||||
|
||||
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
|
||||
|
||||
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
|
||||
super::wlr_foreign_toplevel::update_toplevels(
|
||||
&toplevels,
|
||||
handle,
|
||||
event,
|
||||
&toplevel_tx,
|
||||
);
|
||||
});
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
let clipboard_tx = clipboard_tx2;
|
||||
let handle = event_loop.handle();
|
||||
|
||||
let _offer_listener = listen_to_devices(&env, move |_seat, event, ddata| {
|
||||
debug!("Received clipboard event");
|
||||
super::wlr_data_control::receive_offer(event, &handle, clipboard_tx.clone(), ddata);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = DData {
|
||||
env,
|
||||
offer_tokens: HashMap::new(),
|
||||
};
|
||||
|
||||
loop {
|
||||
if let Err(err) = event_loop.dispatch(None, &mut data) {
|
||||
trace!("Dispatching event loop");
|
||||
if let Err(err) = event_loop.dispatch(None, &mut env) {
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||
@@ -169,119 +195,82 @@ impl WaylandClient {
|
||||
}
|
||||
});
|
||||
|
||||
// keep track of current clipboard item
|
||||
#[cfg(feature = "clipboard")]
|
||||
{
|
||||
let clipboard = clipboard.clone();
|
||||
spawn(async move {
|
||||
while let Ok(item) = clipboard_rx.recv().await {
|
||||
let mut clipboard = write_lock!(clipboard);
|
||||
clipboard.replace(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||
|
||||
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||
|
||||
Self {
|
||||
outputs,
|
||||
seats,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard,
|
||||
toplevels,
|
||||
output_channel,
|
||||
toplevel_tx,
|
||||
_toplevel_rx: toplevel_rx,
|
||||
toplevel_init_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx,
|
||||
seat_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
_clipboard_rx: clipboard_rx,
|
||||
request_tx: ev_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
|
||||
self.toplevel_tx.subscribe()
|
||||
pub fn subscribe_toplevels(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<ToplevelEvent>,
|
||||
HashMap<usize, ToplevelHandle>,
|
||||
) {
|
||||
let rx = self.toplevel_tx.subscribe();
|
||||
|
||||
let receiver = &self.toplevel_init_rx;
|
||||
send!(self.request_tx, Request::Toplevels);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn subscribe_clipboard(&self) -> broadcast::Receiver<Arc<ClipboardItem>> {
|
||||
self.clipboard_tx.subscribe()
|
||||
pub fn subscribe_clipboard(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
Option<Arc<ClipboardItem>>,
|
||||
) {
|
||||
let rx = self.clipboard_tx.subscribe();
|
||||
|
||||
let receiver = &self.clipboard_init_rx;
|
||||
send!(self.request_tx, Request::Clipboard);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(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) {
|
||||
send!(self.request_tx, Request::Refresh);
|
||||
trace!("Sending roundtrip request");
|
||||
send!(self.request_tx, Request::Roundtrip);
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> {
|
||||
let clipboard = read_lock!(self.clipboard);
|
||||
clipboard.as_ref().cloned()
|
||||
/// 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> {
|
||||
trace!("Sending get seats request");
|
||||
|
||||
send!(self.request_tx, Request::Seats);
|
||||
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
||||
send!(self.request_tx, Request::CopyToClipboard(item));
|
||||
}
|
||||
|
||||
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
|
||||
let outputs = env.get_all_outputs();
|
||||
|
||||
outputs
|
||||
.iter()
|
||||
.filter_map(|output| with_output_info(output, Clone::clone))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn new_environment() -> Result<(Environment<Env>, Display, EventQueue), ConnectError> {
|
||||
Display::connect_to_env().and_then(|display| {
|
||||
let mut queue = display.create_event_queue();
|
||||
let ret = {
|
||||
let mut sctk_seats = smithay_client_toolkit::seat::SeatHandler::new();
|
||||
let sctk_data_device_manager =
|
||||
smithay_client_toolkit::data_device::DataDeviceHandler::init(&mut sctk_seats);
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
let data_control_device = DataControlDeviceHandler::init(&mut sctk_seats);
|
||||
|
||||
let sctk_primary_selection_manager =
|
||||
smithay_client_toolkit::primary_selection::PrimarySelectionHandler::init(
|
||||
&mut sctk_seats,
|
||||
);
|
||||
|
||||
let display = ::smithay_client_toolkit::reexports::client::Proxy::clone(&display);
|
||||
let env = Environment::new(
|
||||
&display.attach(queue.token()),
|
||||
&mut queue,
|
||||
Env {
|
||||
sctk_compositor: smithay_client_toolkit::environment::SimpleGlobal::new(),
|
||||
sctk_subcompositor: smithay_client_toolkit::environment::SimpleGlobal::new(
|
||||
),
|
||||
sctk_shm: smithay_client_toolkit::shm::ShmHandler::new(),
|
||||
sctk_outputs: smithay_client_toolkit::output::OutputHandler::new(),
|
||||
sctk_seats,
|
||||
sctk_data_device_manager,
|
||||
sctk_primary_selection_manager,
|
||||
toplevel: ToplevelHandler::init(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_device,
|
||||
},
|
||||
);
|
||||
|
||||
if let Ok(env) = env.as_ref() {
|
||||
let _psm = env.get_primary_selection_manager();
|
||||
}
|
||||
|
||||
env
|
||||
};
|
||||
match ret {
|
||||
Ok(env) => Ok((env, display, queue)),
|
||||
Err(_e) => display.protocol_error().map_or_else(
|
||||
|| Err(ConnectError::NoCompositorListening),
|
||||
|perr| {
|
||||
panic!("[SCTK] A protocol error occured during initial setup: {perr}");
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
101
src/clients/wayland/macros.rs
Normal file
101
src/clients/wayland/macros.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
/// 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>
|
||||
|
||||
// --- Data Control Device --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_offer {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_source {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Foreign Toplevel --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_handle {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,75 +1,114 @@
|
||||
mod client;
|
||||
|
||||
mod macros;
|
||||
mod wl_output;
|
||||
mod wl_seat;
|
||||
mod wlr_foreign_toplevel;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use async_once::AsyncOnce;
|
||||
use lazy_static::lazy_static;
|
||||
use std::fmt::Debug;
|
||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use crate::{arc_mut, cached_broadcast, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||
use cfg_if::cfg_if;
|
||||
use smithay_client_toolkit::default_environment;
|
||||
use smithay_client_toolkit::environment::Environment;
|
||||
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||
use wayland_client::{Attached, Interface};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
pub use wlr_foreign_toplevel::handle::{ToplevelChange, ToplevelEvent, ToplevelInfo};
|
||||
use wlr_foreign_toplevel::manager::{ToplevelHandler};
|
||||
use lazy_static::lazy_static;
|
||||
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;
|
||||
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;
|
||||
|
||||
pub use client::WaylandClient;
|
||||
pub use self::client::WaylandClient;
|
||||
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
mod wlr_data_control;
|
||||
|
||||
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
|
||||
use wlr_data_control::manager::DataControlDeviceHandler;
|
||||
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
|
||||
use self::wlr_data_control::device::DataControlDevice;
|
||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use self::wlr_data_control::source::CopyPasteSource;
|
||||
use self::wlr_data_control::SelectionOfferItem;
|
||||
|
||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||
|
||||
pub struct DataControlDeviceEntry {
|
||||
seat: WlSeat,
|
||||
device: DataControlDevice,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility for lazy-loading globals.
|
||||
/// Taken from `smithay_client_toolkit` where it's not exposed
|
||||
#[derive(Debug)]
|
||||
enum LazyGlobal<I: Interface> {
|
||||
Unknown,
|
||||
Seen { id: u32, version: u32 },
|
||||
Bound(Attached<I>),
|
||||
pub struct Environment {
|
||||
pub registry_state: RegistryState,
|
||||
pub output_state: OutputState,
|
||||
pub seat_state: SeatState,
|
||||
pub foreign_toplevel_manager_state: ToplevelManagerState,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_device_manager_state: DataControlDeviceManagerState,
|
||||
pub loop_handle: LoopHandle<'static, Self>,
|
||||
|
||||
pub seats: Vec<WlSeat>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_devices: Vec<DataControlDeviceEntry>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub selection_offers: Vec<SelectionOfferItem>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub copy_paste_sources: Vec<CopyPasteSource>,
|
||||
|
||||
pub handles: HashMap<usize, ToplevelHandle>,
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
pub struct DData {
|
||||
env: Environment<Env>,
|
||||
offer_tokens: HashMap<u128, RegistrationToken>,
|
||||
}
|
||||
// Now we need to say we are delegating the responsibility of output related events for our application data
|
||||
// type to the requisite delegate.
|
||||
delegate_output!(Environment);
|
||||
delegate_seat!(Environment);
|
||||
|
||||
delegate_foreign_toplevel_manager!(Environment);
|
||||
delegate_foreign_toplevel_handle!(Environment);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
default_environment!(Env,
|
||||
fields = [
|
||||
toplevel: ToplevelHandler,
|
||||
data_control_device: DataControlDeviceHandler
|
||||
],
|
||||
singles = [
|
||||
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||
ZwlrDataControlManagerV1 => data_control_device
|
||||
],
|
||||
);
|
||||
} else {
|
||||
default_environment!(Env,
|
||||
fields = [
|
||||
toplevel: ToplevelHandler,
|
||||
],
|
||||
singles = [
|
||||
ZwlrForeignToplevelManagerV1 => toplevel,
|
||||
],
|
||||
);
|
||||
delegate_data_control_device_manager!(Environment);
|
||||
delegate_data_control_device!(Environment);
|
||||
delegate_data_control_source!(Environment);
|
||||
delegate_data_control_offer!(Environment);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: AsyncOnce<WaylandClient> =
|
||||
AsyncOnce::new(async { WaylandClient::new().await });
|
||||
// In order for our delegate to know of the existence of globals, we need to implement registry
|
||||
// handling for the program. This trait will forward events to the RegistryHandler trait
|
||||
// implementations.
|
||||
delegate_registry!(Environment);
|
||||
|
||||
// In order for delegate_registry to work, our application data type needs to provide a way for the
|
||||
// implementation to access the registry state.
|
||||
//
|
||||
// We also need to indicate which delegates will get told about globals being created. We specify
|
||||
// the types of the delegates inside the array.
|
||||
impl ProvidesRegistryState for Environment {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![OutputState, SeatState];
|
||||
}
|
||||
|
||||
pub async fn get_client() -> &'static WaylandClient {
|
||||
CLIENT.get().await
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new());
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
72
src/clients/wayland/wl_output.rs
Normal file
72
src/clients/wayland/wl_output.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl Environment {
|
||||
pub fn output_info(&mut self) -> Vec<OutputInfo> {
|
||||
self.output_state
|
||||
.outputs()
|
||||
.filter_map(|output| self.output_state.info(&output))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
|
||||
// output and to provide an instance of the output state to the delegate when dispatching events.
|
||||
impl OutputHandler for Environment {
|
||||
// First we need to provide a way to access the delegate.
|
||||
//
|
||||
// This is needed because delegate implementations for handling events use the application data type in
|
||||
// their function signatures. This allows the implementation to access an instance of the type.
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
// Then there exist these functions that indicate the lifecycle of an output.
|
||||
// These will be called as appropriate by the delegate implementation.
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
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(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_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,
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
63
src/clients/wayland/wl_seat.rs
Normal file
63
src/clients/wayland/wl_seat.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::Environment;
|
||||
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_seat;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl SeatHandler for Environment {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received new seat");
|
||||
self.seats.push(seat);
|
||||
}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received new capability");
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
if !self
|
||||
.data_control_devices
|
||||
.iter_mut()
|
||||
.any(|entry| entry.seat == seat)
|
||||
{
|
||||
debug!("Adding new data control device");
|
||||
// create the data device here for this seat
|
||||
let data_control_device_manager = &self.data_control_device_manager_state;
|
||||
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
|
||||
self.data_control_devices
|
||||
.push(super::DataControlDeviceEntry {
|
||||
seat: seat.clone(),
|
||||
device: data_control_device,
|
||||
});
|
||||
}
|
||||
|
||||
if !self.seats.iter().any(|s| s == &seat) {
|
||||
self.seats.push(seat);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received capability removal");
|
||||
// Not applicable
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received seat removal");
|
||||
self.seats.retain(|s| s != &seat);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,166 @@
|
||||
use super::offer::DataControlOffer;
|
||||
use super::source::DataControlSource;
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use super::offer::{
|
||||
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
|
||||
};
|
||||
use crate::error::ERR_WAYLAND_DATA;
|
||||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Attached, DispatchData, Main};
|
||||
use wayland_protocols::wlr::unstable::data_control::v1::client::{
|
||||
use tracing::warn;
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
offer: Option<Arc<DataControlOffer>>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) {
|
||||
self.offer.replace(Arc::new(DataControlOffer::new(offer)));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>);
|
||||
|
||||
fn data_control_device_implem<F>(
|
||||
event: Event,
|
||||
inner: &mut Inner,
|
||||
implem: &mut F,
|
||||
ddata: DispatchData,
|
||||
) where
|
||||
F: FnMut(DataControlDeviceEvent, DispatchData),
|
||||
{
|
||||
match event {
|
||||
Event::DataOffer { id } => {
|
||||
inner.new_offer(&id);
|
||||
}
|
||||
Event::Selection { id: Some(offer) } => {
|
||||
let inner_offer = inner
|
||||
.offer
|
||||
.clone()
|
||||
.expect("Offer should exist at this stage");
|
||||
if offer == inner_offer.offer {
|
||||
implem(DataControlDeviceEvent(inner_offer), ddata);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DataControlDevice {
|
||||
device: ZwlrDataControlDeviceV1,
|
||||
_inner: Arc<Mutex<Inner>>,
|
||||
pub device: ZwlrDataControlDeviceV1,
|
||||
}
|
||||
|
||||
impl DataControlDevice {
|
||||
pub fn init_for_seat<F>(
|
||||
manager: &Attached<ZwlrDataControlManagerV1>,
|
||||
seat: &WlSeat,
|
||||
mut callback: F,
|
||||
) -> Self
|
||||
where
|
||||
F: FnMut(DataControlDeviceEvent, DispatchData) + 'static,
|
||||
{
|
||||
let inner = Arc::new(Mutex::new(Inner { offer: None }));
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceInner {
|
||||
/// the active selection offer and its data
|
||||
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
|
||||
/// the active undetermined offers and their data
|
||||
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
|
||||
}
|
||||
|
||||
let device = manager.get_data_device(seat);
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceData {
|
||||
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
|
||||
}
|
||||
|
||||
{
|
||||
let inner = inner.clone();
|
||||
device.quick_assign(move |_handle, event, ddata| {
|
||||
let mut inner = lock!(inner);
|
||||
data_control_device_implem(event, &mut inner, &mut callback, ddata);
|
||||
});
|
||||
}
|
||||
pub trait DataControlDeviceDataExt: Send + Sync {
|
||||
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
|
||||
|
||||
Self {
|
||||
device: device.detach(),
|
||||
_inner: inner,
|
||||
}
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData;
|
||||
|
||||
fn selection_mime_types(&self) -> Vec<String> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.map(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.mime_types()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn set_selection(&self, source: &Option<DataControlSource>) {
|
||||
self.device
|
||||
.set_selection(source.as_ref().map(|s| &s.source));
|
||||
/// Get the active selection offer if it exists.
|
||||
fn selection_offer(&self) -> Option<SelectionOffer> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.and_then(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.as_selection_offer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDevice {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self.device.data().expect(ERR_WAYLAND_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDeviceData {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataDevice` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
|
||||
pub trait DataControlDeviceHandler: Sized {
|
||||
/// Advertises a new selection.
|
||||
fn selection(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U>
|
||||
+ Dispatch<ZwlrDataControlOfferV1, V>
|
||||
+ DataControlDeviceHandler
|
||||
+ DataControlOfferHandler
|
||||
+ 'static,
|
||||
U: DataControlDeviceDataExt,
|
||||
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrDataControlDeviceV1, [
|
||||
0 => (ZwlrDataControlOfferV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
data_device: &ZwlrDataControlDeviceV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_device_data();
|
||||
let inner = lock!(data.inner);
|
||||
|
||||
match event {
|
||||
Event::DataOffer { id } => {
|
||||
// XXX Drop done here to prevent Mutex deadlocks.S
|
||||
|
||||
lock!(inner.undetermined_offers).push(id.clone());
|
||||
let data = id
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.init_undetermined_offer(&id);
|
||||
|
||||
// Append the data offer to our list of offers.
|
||||
drop(inner);
|
||||
}
|
||||
Event::Selection { id } => {
|
||||
let mut selection_offer = lock!(inner.selection_offer);
|
||||
|
||||
if let Some(offer) = id {
|
||||
let mut undetermined = lock!(inner.undetermined_offers);
|
||||
if let Some(i) = undetermined.iter().position(|o| o == &offer) {
|
||||
undetermined.remove(i);
|
||||
}
|
||||
drop(undetermined);
|
||||
|
||||
let data = offer
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.to_selection_offer();
|
||||
// XXX Drop done here to prevent Mutex deadlocks.
|
||||
*selection_offer = Some(offer.clone());
|
||||
drop(selection_offer);
|
||||
drop(inner);
|
||||
state.selection(
|
||||
conn,
|
||||
qh,
|
||||
DataControlDevice {
|
||||
device: data_device.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
*selection_offer = None;
|
||||
}
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +1,132 @@
|
||||
use super::device::{DataControlDevice, DataControlDeviceEvent};
|
||||
use super::source::DataControlSource;
|
||||
use smithay_client_toolkit::data_device::WritePipe;
|
||||
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||
use smithay_client_toolkit::seat::{SeatHandling, SeatListener};
|
||||
use smithay_client_toolkit::MissingGlobal;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::{self, Rc};
|
||||
use tracing::warn;
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
|
||||
use super::offer::DataControlOfferData;
|
||||
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::debug;
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Attached, DispatchData};
|
||||
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
|
||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
enum DataControlDeviceHandlerInner {
|
||||
Ready {
|
||||
manager: Attached<ZwlrDataControlManagerV1>,
|
||||
devices: Vec<(WlSeat, DataControlDevice)>,
|
||||
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||
},
|
||||
Pending {
|
||||
seats: Vec<WlSeat>,
|
||||
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||
},
|
||||
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
|
||||
manager: ZwlrDataControlManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandlerInner {
|
||||
fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
|
||||
let (seats, status_listeners) = if let Self::Pending {
|
||||
seats,
|
||||
status_listeners,
|
||||
} = self
|
||||
{
|
||||
(std::mem::take(seats), status_listeners.clone())
|
||||
} else {
|
||||
warn!("Ignoring second zwlr_data_control_manager_v1");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut devices = Vec::new();
|
||||
|
||||
for seat in seats {
|
||||
let my_seat = seat.clone();
|
||||
let status_listeners = status_listeners.clone();
|
||||
let device =
|
||||
DataControlDevice::init_for_seat(&manager, &seat, move |event, dispatch_data| {
|
||||
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
|
||||
});
|
||||
devices.push((seat.clone(), device));
|
||||
}
|
||||
|
||||
*self = Self::Ready {
|
||||
impl DataControlDeviceManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let manager = globals.bind(qh, 1..=2, GlobalData)?;
|
||||
debug!("Bound to ZwlDataControlManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
devices,
|
||||
status_listeners,
|
||||
};
|
||||
}
|
||||
|
||||
fn get_manager(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
|
||||
match self {
|
||||
Self::Ready { manager, .. } => Some(manager.clone()),
|
||||
Self::Pending { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, seat: &WlSeat) {
|
||||
match self {
|
||||
Self::Ready {
|
||||
manager,
|
||||
devices,
|
||||
status_listeners,
|
||||
} => {
|
||||
if devices.iter().any(|(s, _)| s == seat) {
|
||||
// the seat already exists, nothing to do
|
||||
return;
|
||||
}
|
||||
let my_seat = seat.clone();
|
||||
let status_listeners = status_listeners.clone();
|
||||
let device =
|
||||
DataControlDevice::init_for_seat(manager, seat, move |event, dispatch_data| {
|
||||
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
|
||||
});
|
||||
devices.push((seat.clone(), device));
|
||||
}
|
||||
Self::Pending { seats, .. } => {
|
||||
seats.push(seat.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, seat: &WlSeat) {
|
||||
match self {
|
||||
Self::Ready { devices, .. } => devices.retain(|(s, _)| s != seat),
|
||||
Self::Pending { seats, .. } => seats.retain(|s| s != seat),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||
where
|
||||
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||
{
|
||||
match self {
|
||||
Self::Ready { manager, .. } => {
|
||||
let source = DataControlSource::new(manager, mime_types, callback);
|
||||
Some(source)
|
||||
}
|
||||
Self::Pending { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||
where
|
||||
F: FnOnce(&DataControlDevice),
|
||||
{
|
||||
match self {
|
||||
Self::Ready { devices, .. } => {
|
||||
let device = devices
|
||||
.iter()
|
||||
.find_map(|(s, device)| if s == seat { Some(device) } else { None });
|
||||
|
||||
device.map_or(Err(MissingGlobal), |device| {
|
||||
f(device);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
Self::Pending { .. } => Err(MissingGlobal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DataControlDeviceHandler {
|
||||
inner: Rc<RefCell<DataControlDeviceHandlerInner>>,
|
||||
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
|
||||
_seat_listener: SeatListener,
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandler {
|
||||
pub fn init<S>(seat_handler: &mut S) -> Self
|
||||
where
|
||||
S: SeatHandling,
|
||||
{
|
||||
let status_listeners = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let inner = Rc::new(RefCell::new(DataControlDeviceHandlerInner::Pending {
|
||||
seats: Vec::new(),
|
||||
status_listeners: status_listeners.clone(),
|
||||
}));
|
||||
|
||||
let seat_inner = inner.clone();
|
||||
let seat_listener = seat_handler.listen(move |seat, seat_data, _| {
|
||||
if seat_data.defunct {
|
||||
seat_inner.borrow_mut().remove_seat(&seat);
|
||||
} else {
|
||||
seat_inner.borrow_mut().new_seat(&seat);
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
inner,
|
||||
_seat_listener: seat_listener,
|
||||
status_listeners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
|
||||
fn created(
|
||||
&mut self,
|
||||
registry: Attached<WlRegistry>,
|
||||
id: u32,
|
||||
version: u32,
|
||||
_ddata: DispatchData,
|
||||
) {
|
||||
// data control manager is supported until version 2
|
||||
let version = std::cmp::min(version, 2);
|
||||
|
||||
let manager = registry.bind::<ZwlrDataControlManagerV1>(version, id);
|
||||
self.inner.borrow_mut().init_manager((*manager).clone());
|
||||
}
|
||||
|
||||
fn get(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
|
||||
RefCell::borrow(&self.inner).get_manager()
|
||||
}
|
||||
}
|
||||
|
||||
type DataControlDeviceStatusCallback =
|
||||
dyn FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
|
||||
|
||||
/// Notifies the callbacks of an event on the data device
|
||||
fn notify_status_listeners(
|
||||
seat: &WlSeat,
|
||||
event: &DataControlDeviceEvent,
|
||||
mut ddata: DispatchData,
|
||||
listeners: &RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>,
|
||||
) {
|
||||
listeners.borrow_mut().retain(|lst| {
|
||||
rc::Weak::upgrade(lst).map_or(false, |cb| {
|
||||
(cb.borrow_mut())(seat.clone(), event.clone(), ddata.reborrow());
|
||||
true
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub struct DataControlDeviceStatusListener {
|
||||
_cb: Rc<RefCell<DataControlDeviceStatusCallback>>,
|
||||
}
|
||||
|
||||
pub trait DataControlDeviceHandling {
|
||||
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||
where
|
||||
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
|
||||
|
||||
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||
where
|
||||
F: FnOnce(&DataControlDevice);
|
||||
|
||||
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||
where
|
||||
F: FnMut(String, WritePipe, DispatchData) + 'static;
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandling for DataControlDeviceHandler {
|
||||
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||
where
|
||||
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||
{
|
||||
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
|
||||
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
|
||||
DataControlDeviceStatusListener { _cb: rc }
|
||||
}
|
||||
|
||||
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||
/// creates a data source for copy paste
|
||||
pub fn create_copy_paste_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> CopyPasteSource
|
||||
where
|
||||
F: FnOnce(&DataControlDevice),
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
RefCell::borrow(&self.inner).with_device(seat, f)
|
||||
CopyPasteSource {
|
||||
inner: self.create_data_control_source(qh, mime_types),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||
/// creates a data source
|
||||
fn create_data_control_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
RefCell::borrow(&self.inner).create_source(mime_types, callback)
|
||||
let source =
|
||||
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
|
||||
|
||||
for mime in mime_types {
|
||||
source.offer(mime.to_string());
|
||||
}
|
||||
|
||||
source
|
||||
}
|
||||
|
||||
/// create a new data source for a given seat with some user data
|
||||
pub fn create_data_control_source_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
data: U,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
|
||||
U: DataControlSourceDataExt + 'static,
|
||||
{
|
||||
self.manager.create_data_source(qh, data)
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat
|
||||
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
|
||||
{
|
||||
DataControlDevice {
|
||||
device: self.get_data_control_device_with_data(
|
||||
qh,
|
||||
seat,
|
||||
DataControlDeviceData::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat with some user data
|
||||
pub fn get_data_control_device_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
seat: &WlSeat,
|
||||
data: U,
|
||||
) -> ZwlrDataControlDeviceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
|
||||
U: DataControlDeviceDataExt + 'static,
|
||||
{
|
||||
self.manager.get_data_device(seat, qh, data)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_to_devices<E, F>(env: &Environment<E>, f: F) -> DataControlDeviceStatusListener
|
||||
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState
|
||||
where
|
||||
E: DataControlDeviceHandling,
|
||||
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>,
|
||||
{
|
||||
env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
|
||||
fn event(
|
||||
_state: &mut D,
|
||||
_proxy: &ZwlrDataControlManagerV1,
|
||||
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
|
||||
_data: &GlobalData,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,34 @@ pub mod manager;
|
||||
pub mod offer;
|
||||
pub mod source;
|
||||
|
||||
use super::Env;
|
||||
use crate::clients::wayland::DData;
|
||||
use crate::send;
|
||||
use color_eyre::Report;
|
||||
use device::{DataControlDevice, DataControlDeviceEvent};
|
||||
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||
use self::source::DataControlSourceHandler;
|
||||
use crate::clients::wayland::Environment;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{lock, send};
|
||||
use device::DataControlDevice;
|
||||
use glib::Bytes;
|
||||
use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener};
|
||||
use smithay_client_toolkit::data_device::WritePipe;
|
||||
use smithay_client_toolkit::environment::Environment;
|
||||
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
||||
use smithay_client_toolkit::MissingGlobal;
|
||||
use source::DataControlSource;
|
||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
||||
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;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
use std::sync::Arc;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tokio::sync::broadcast;
|
||||
use std::{fs, io};
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::DispatchData;
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
||||
|
||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||
|
||||
fn get_id() -> usize {
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
pub struct SelectionOfferItem {
|
||||
offer: SelectionOffer,
|
||||
token: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
@@ -47,77 +46,27 @@ impl PartialEq<Self> for ClipboardItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardValue {
|
||||
Text(String),
|
||||
Image(Bytes),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandling for Env {
|
||||
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||
where
|
||||
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.data_control_device.listen(f)
|
||||
}
|
||||
|
||||
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
|
||||
where
|
||||
F: FnOnce(&DataControlDevice),
|
||||
{
|
||||
self.data_control_device.with_data_control_device(seat, f)
|
||||
}
|
||||
|
||||
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
|
||||
where
|
||||
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||
{
|
||||
self.data_control_device.create_source(mime_types, callback)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard<E>(
|
||||
env: &Environment<E>,
|
||||
seat: &WlSeat,
|
||||
item: &ClipboardItem,
|
||||
) -> Result<(), MissingGlobal>
|
||||
where
|
||||
E: DataControlDeviceHandling,
|
||||
{
|
||||
debug!("Copying item with id {} [{}]", item.id, item.mime_type);
|
||||
trace!("Copying: {item:?}");
|
||||
|
||||
let item = item.clone();
|
||||
|
||||
env.with_inner(|env| {
|
||||
let mime_types = vec![INTERNAL_MIME_TYPE.to_string(), item.mime_type];
|
||||
let source = env.create_source(mime_types, move |mime_type, mut pipe, _ddata| {
|
||||
debug!(
|
||||
"Triggering source callback for item with id {} [{}]",
|
||||
item.id, mime_type
|
||||
);
|
||||
|
||||
// FIXME: Not working for large (buffered) values in xwayland
|
||||
let bytes = match &item.value {
|
||||
ClipboardValue::Text(text) => text.as_bytes(),
|
||||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||
ClipboardValue::Other => panic!(
|
||||
"{:?}",
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Attempted to copy unsupported mime type",
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
if let Err(err) = pipe.write_all(bytes) {
|
||||
error!("{err:?}");
|
||||
impl Debug for ClipboardValue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Text(text) => text.clone(),
|
||||
Self::Image(bytes) => {
|
||||
format!("[{} Bytes]", bytes.len())
|
||||
}
|
||||
Self::Other => "[Unknown]".to_string(),
|
||||
}
|
||||
});
|
||||
|
||||
env.with_data_control_device(seat, |device| device.set_selection(&source))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -133,126 +82,294 @@ enum MimeTypeCategory {
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
fn parse(mime_types: &[String]) -> Option<Self> {
|
||||
mime_types
|
||||
.iter()
|
||||
.map(|s| s.to_lowercase())
|
||||
.find_map(|mime_type| match mime_type.as_str() {
|
||||
"text"
|
||||
| "string"
|
||||
| "utf8_string"
|
||||
| "text/plain"
|
||||
| "text/plain;charset=utf-8"
|
||||
| "text/plain;charset=iso-8859-1"
|
||||
| "text/plain;charset=us-ascii"
|
||||
| "text/plain;charset=unicode" => Some(Self {
|
||||
value: mime_type,
|
||||
category: MimeTypeCategory::Text,
|
||||
}),
|
||||
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
||||
| "image/x-bmp" | "image/icon" => Some(Self {
|
||||
value: mime_type,
|
||||
category: MimeTypeCategory::Image,
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
fn parse(mime_type: &str) -> Option<Self> {
|
||||
match mime_type.to_lowercase().as_str() {
|
||||
"text"
|
||||
| "string"
|
||||
| "utf8_string"
|
||||
| "text/plain"
|
||||
| "text/plain;charset=utf-8"
|
||||
| "text/plain;charset=iso-8859-1"
|
||||
| "text/plain;charset=us-ascii"
|
||||
| "text/plain;charset=unicode" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Text,
|
||||
}),
|
||||
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
||||
| "image/x-bmp" | "image/icon" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Image,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
|
||||
mime_types.iter().find_map(|mime| Self::parse(mime))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_offer(
|
||||
event: DataControlDeviceEvent,
|
||||
handle: &LoopHandle<DData>,
|
||||
tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
mut ddata: DispatchData,
|
||||
) {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Could not get epoch, system time is probably very wrong")
|
||||
.as_nanos();
|
||||
impl Environment {
|
||||
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
|
||||
debug!("Copying item to clipboard: {item:?}");
|
||||
|
||||
let offer = event.0;
|
||||
// TODO: Proper device tracking
|
||||
let device = self.data_control_devices.first();
|
||||
if let Some(device) = device {
|
||||
let source = self
|
||||
.data_control_device_manager_state
|
||||
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
|
||||
|
||||
let ddata = ddata
|
||||
.get::<DData>()
|
||||
.expect("Expected dispatch data to exist");
|
||||
source.set_selection(&device.device);
|
||||
self.copy_paste_sources.push(source);
|
||||
|
||||
let handle2 = handle.clone();
|
||||
lock!(self.clipboard).replace(item);
|
||||
}
|
||||
}
|
||||
|
||||
let res = offer.with_mime_types(|mime_types| {
|
||||
debug!("Offer mime types: {mime_types:?}");
|
||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||
let value = match mime_type.category {
|
||||
MimeTypeCategory::Text => {
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)?;
|
||||
|
||||
ClipboardValue::Text(txt)
|
||||
}
|
||||
MimeTypeCategory::Image => {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)?;
|
||||
let bytes = Bytes::from(&bytes);
|
||||
|
||||
ClipboardValue::Image(bytes)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ClipboardItem {
|
||||
id: get_unique_usize(),
|
||||
value,
|
||||
mime_type: mime_type.value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandler for Environment {
|
||||
fn selection(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
) {
|
||||
debug!("Handler received selection event");
|
||||
|
||||
let mime_types = data_device.selection_mime_types();
|
||||
|
||||
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
||||
debug!("Skipping value provided by bar");
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
let mime_type = MimeType::parse(mime_types);
|
||||
debug!("Detected mime type: {mime_type:?}");
|
||||
if let Some(offer) = data_device.selection_offer() {
|
||||
self.selection_offers
|
||||
.push(SelectionOfferItem { offer, token: None });
|
||||
|
||||
match mime_type {
|
||||
Some(mime_type) => {
|
||||
debug!("[{timestamp}] Sending clipboard read request ({mime_type:?})");
|
||||
let read_pipe = offer.receive(mime_type.value.clone())?;
|
||||
let source = handle.insert_source(read_pipe, move |(), file, ddata| {
|
||||
debug!(
|
||||
"[{timestamp}] Reading clipboard contents ({:?})",
|
||||
&mime_type.category
|
||||
);
|
||||
match read_file(&mime_type, file) {
|
||||
Ok(item) => {
|
||||
send!(tx, Arc::new(item));
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
let cur_offer = self
|
||||
.selection_offers
|
||||
.last_mut()
|
||||
.expect("Failed to get current offer");
|
||||
|
||||
if let Some(src) = ddata.offer_tokens.remove(×tamp) {
|
||||
handle2.remove(src);
|
||||
}
|
||||
})?;
|
||||
|
||||
ddata.offer_tokens.insert(timestamp, source);
|
||||
}
|
||||
None => {
|
||||
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
||||
lock!(self.clipboard).take();
|
||||
// send an event so the clipboard module is aware it's changed
|
||||
send!(
|
||||
tx,
|
||||
self.clipboard_tx,
|
||||
Arc::new(ClipboardItem {
|
||||
id: usize::MAX,
|
||||
mime_type: String::new(),
|
||||
value: ClipboardValue::Other
|
||||
})
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
||||
let offer_clone = cur_offer.offer.clone();
|
||||
|
||||
let tx = self.clipboard_tx.clone();
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
let token = self
|
||||
.loop_handle
|
||||
.insert_source(read_pipe, move |_, file, state| {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
|
||||
match Self::read_file(&mime_type, file) {
|
||||
Ok(item) => {
|
||||
let item = Arc::new(item);
|
||||
lock!(clipboard).replace(item.clone());
|
||||
send!(tx, item);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
});
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
cur_offer.token.replace(token);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||
let value = match mime_type.category {
|
||||
MimeTypeCategory::Text => {
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)?;
|
||||
impl DataControlOfferHandler for Environment {
|
||||
fn offer(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_offer: &mut DataControlDeviceOffer,
|
||||
_mime_type: String,
|
||||
) {
|
||||
trace!("Handler received offer");
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardValue::Text(txt)
|
||||
}
|
||||
MimeTypeCategory::Image => {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)?;
|
||||
let bytes = Bytes::from(&bytes);
|
||||
impl DataControlSourceHandler for Environment {
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
) {
|
||||
debug!("Accepted mime type: {mime:?}");
|
||||
}
|
||||
|
||||
ClipboardValue::Image(bytes)
|
||||
fn send_request(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
debug!("Handler received source send request event ({mime})");
|
||||
|
||||
if let Some(item) = lock!(self.clipboard).clone() {
|
||||
let fd = OwnedFd::from(write_pipe);
|
||||
if self
|
||||
.copy_paste_sources
|
||||
.iter_mut()
|
||||
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
|
||||
{
|
||||
trace!("Source found, writing to file");
|
||||
|
||||
let mut bytes = match &item.value {
|
||||
ClipboardValue::Text(text) => text.as_bytes(),
|
||||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||
ClipboardValue::Other => panic!(
|
||||
"{:?}",
|
||||
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",)
|
||||
),
|
||||
};
|
||||
|
||||
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
|
||||
.expect("Failed to increase pipe size");
|
||||
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
|
||||
|
||||
trace!("Num bytes: {}", bytes.len());
|
||||
|
||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
|
||||
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_fd
|
||||
.wait(&mut events, 100)
|
||||
.expect("Failed to wait to epoll");
|
||||
|
||||
match file.write(chunk) {
|
||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to find source");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
) {
|
||||
debug!("Handler received source cancelled event");
|
||||
|
||||
self.copy_paste_sources
|
||||
.iter()
|
||||
.position(|s| s.inner() == source)
|
||||
.map(|pos| self.copy_paste_sources.remove(pos));
|
||||
source.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to increase the fd pipe size to the requested number of bytes.
|
||||
/// The kernel will automatically round this up to the nearest page size.
|
||||
/// If the requested size is larger than the kernel max (normally 1MB),
|
||||
/// it will be clamped at this.
|
||||
///
|
||||
/// Returns the new size if succeeded
|
||||
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||
// clamp size at kernel max
|
||||
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
||||
.expect("Failed to find pipe-max-size virtual kernel file")
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.expect("Failed to parse pipe-max-size contents");
|
||||
|
||||
let size = min(size, max_pipe_size);
|
||||
|
||||
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
|
||||
|
||||
trace!("Current pipe size: {curr_size}");
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
Ok(ClipboardItem {
|
||||
id: get_id(),
|
||||
value,
|
||||
mime_type: mime_type.value.clone(),
|
||||
})
|
||||
Ok(new_size)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,183 @@
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::unistd::{close, pipe2};
|
||||
use smithay_client_toolkit::data_device::ReadPipe;
|
||||
use std::io;
|
||||
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
|
||||
use smithay_client_toolkit::data_device_manager::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::warn;
|
||||
use wayland_client::Main;
|
||||
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Inner {
|
||||
mime_types: Vec<String>,
|
||||
pub struct UndeterminedOffer {
|
||||
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>,
|
||||
}
|
||||
|
||||
impl PartialEq for UndeterminedOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataControlOffer {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
pub(crate) offer: ZwlrDataControlOfferV1,
|
||||
pub struct SelectionOffer {
|
||||
pub data_offer: ZwlrDataControlOfferV1,
|
||||
}
|
||||
|
||||
impl DataControlOffer {
|
||||
pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> Self {
|
||||
let inner = Arc::new(Mutex::new(Inner {
|
||||
mime_types: Vec::new(),
|
||||
}));
|
||||
|
||||
{
|
||||
let inner = inner.clone();
|
||||
|
||||
offer.quick_assign(move |_, event, _| {
|
||||
let mut inner = lock!(inner);
|
||||
if let Event::Offer { mime_type } = event {
|
||||
inner.mime_types.push(mime_type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
offer: offer.detach(),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mime_types<F, T>(&self, f: F) -> T
|
||||
where
|
||||
F: FnOnce(&[String]) -> T,
|
||||
{
|
||||
let inner = lock!(self.inner);
|
||||
f(&inner.mime_types)
|
||||
}
|
||||
|
||||
pub fn receive(&self, mime_type: String) -> io::Result<ReadPipe> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
|
||||
self.offer.receive(mime_type, writefd);
|
||||
|
||||
if let Err(err) = close(writefd) {
|
||||
warn!("Failed to close write pipe: {}", err);
|
||||
}
|
||||
|
||||
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||
impl PartialEq for SelectionOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DataControlOffer {
|
||||
fn drop(&mut self) {
|
||||
self.offer.destroy();
|
||||
impl SelectionOffer {
|
||||
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
|
||||
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DataControlDeviceOffer {
|
||||
Selection(SelectionOffer),
|
||||
Undetermined(UndeterminedOffer),
|
||||
}
|
||||
|
||||
impl Default for DataControlDeviceOffer {
|
||||
fn default() -> Self {
|
||||
Self::Undetermined(UndeterminedOffer { data_offer: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlOfferData {
|
||||
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceOfferInner {
|
||||
pub(crate) offer: DataControlDeviceOffer,
|
||||
pub(crate) mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl DataControlOfferData {
|
||||
pub(crate) fn push_mime_type(&self, mime_type: String) {
|
||||
lock!(self.inner).mime_types.push(mime_type);
|
||||
}
|
||||
|
||||
pub(crate) fn to_selection_offer(&self) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
|
||||
data_offer: o.data_offer.clone().expect("Missing current data offer"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {
|
||||
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
|
||||
data_offer: Some(offer.clone()),
|
||||
});
|
||||
}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
o.data_offer = Some(offer.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataControlOfferDataExt {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData;
|
||||
fn mime_types(&self) -> Vec<String>;
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer>;
|
||||
}
|
||||
|
||||
impl DataControlOfferDataExt for DataControlOfferData {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData {
|
||||
self
|
||||
}
|
||||
|
||||
fn mime_types(&self) -> Vec<String> {
|
||||
lock!(self.inner).mime_types.clone()
|
||||
}
|
||||
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer> {
|
||||
match &lock!(self.inner).offer {
|
||||
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
|
||||
DataControlDeviceOffer::Undetermined(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataOffer` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
|
||||
pub trait DataControlOfferHandler: Sized {
|
||||
// Called for each mime type the data offer advertises.
|
||||
fn offer(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
offer: &mut DataControlDeviceOffer,
|
||||
mime_type: String,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
|
||||
U: DataControlOfferDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
_offer: &ZwlrDataControlOfferV1,
|
||||
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_offer_data();
|
||||
|
||||
if let Event::Offer { mime_type } = event {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to receive the data of a given mime type.
|
||||
///
|
||||
/// You can do this several times, as a reaction to motion of
|
||||
/// the dnd cursor, or to inspect the data in order to choose your
|
||||
/// response.
|
||||
///
|
||||
/// Note that you should *not* read the contents right away in a
|
||||
/// blocking way, as you may deadlock your application doing so.
|
||||
/// At least make sure you flush your events to the server before
|
||||
/// doing so.
|
||||
///
|
||||
/// Fails if too many file descriptors were already open and a pipe
|
||||
/// could not be created.
|
||||
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
|
||||
offer.receive(mime_type, writefd);
|
||||
|
||||
if let Err(err) = close(writefd) {
|
||||
warn!("Failed to close write pipe: {}", err);
|
||||
}
|
||||
|
||||
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||
}
|
||||
|
||||
@@ -1,54 +1,101 @@
|
||||
use smithay_client_toolkit::data_device::WritePipe;
|
||||
use std::os::fd::FromRawFd;
|
||||
use wayland_client::{Attached, DispatchData};
|
||||
use wayland_protocols::wlr::unstable::data_control::v1::client::{
|
||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||
zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1},
|
||||
use super::device::DataControlDevice;
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
|
||||
Event, ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
fn data_control_source_impl<F>(
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
event: Event,
|
||||
implem: &mut F,
|
||||
ddata: DispatchData,
|
||||
) where
|
||||
F: FnMut(String, WritePipe, DispatchData),
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlSourceData {}
|
||||
|
||||
pub trait DataControlSourceDataExt: Send + Sync {
|
||||
fn data_source_data(&self) -> &DataControlSourceData;
|
||||
}
|
||||
|
||||
impl DataControlSourceDataExt for DataControlSourceData {
|
||||
fn data_source_data(&self) -> &DataControlSourceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataSource` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
|
||||
pub trait DataControlSourceHandler: Sized {
|
||||
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
);
|
||||
|
||||
/// The client has requested the data for this source to be sent.
|
||||
/// Send the data, then close the fd.
|
||||
fn send_request(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
fd: WritePipe,
|
||||
);
|
||||
|
||||
/// The data source is no longer valid
|
||||
/// Cleanup & destroy this resource
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
|
||||
U: DataControlSourceDataExt,
|
||||
{
|
||||
match event {
|
||||
Event::Send { mime_type, fd } => {
|
||||
let pipe = unsafe { FromRawFd::from_raw_fd(fd) };
|
||||
implem(mime_type, pipe, ddata);
|
||||
}
|
||||
Event::Cancelled => source.destroy(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DataControlSource {
|
||||
pub(crate) source: ZwlrDataControlSourceV1,
|
||||
}
|
||||
|
||||
impl DataControlSource {
|
||||
pub fn new<F>(
|
||||
manager: &Attached<ZwlrDataControlManagerV1>,
|
||||
mime_types: Vec<String>,
|
||||
mut callback: F,
|
||||
) -> Self
|
||||
where
|
||||
F: FnMut(String, WritePipe, DispatchData) + 'static,
|
||||
{
|
||||
let source = manager.create_data_source();
|
||||
|
||||
source.quick_assign(move |source, evt, ddata| {
|
||||
data_control_source_impl(&source, evt, &mut callback, ddata);
|
||||
});
|
||||
|
||||
for mime_type in mime_types {
|
||||
source.offer(mime_type);
|
||||
}
|
||||
|
||||
Self {
|
||||
source: source.detach(),
|
||||
fn event(
|
||||
state: &mut D,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
|
||||
_data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Send { mime_type, fd } => {
|
||||
state.send_request(conn, qh, source, mime_type, fd.into());
|
||||
}
|
||||
Event::Cancelled => {
|
||||
state.cancelled(conn, qh, source);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CopyPasteSource {
|
||||
pub(crate) inner: ZwlrDataControlSourceV1,
|
||||
}
|
||||
|
||||
impl CopyPasteSource {
|
||||
/// Set the selection of the provided data device as a response to the event with with provided serial.
|
||||
pub fn set_selection(&self, device: &DataControlDevice) {
|
||||
device.device.set_selection(Some(&self.inner));
|
||||
}
|
||||
|
||||
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CopyPasteSource {
|
||||
fn drop(&mut self) {
|
||||
self.inner.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,152 +1,178 @@
|
||||
use super::manager::ToplevelManagerState;
|
||||
use crate::lock;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::trace;
|
||||
use wayland_client::{DispatchData, Main};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1};
|
||||
use crate::write_lock;
|
||||
use wayland_client::protocol::wl_output::WlOutput;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
|
||||
Event, ZwlrForeignToplevelHandleV1,
|
||||
};
|
||||
|
||||
const STATE_ACTIVE: u32 = 2;
|
||||
const STATE_FULLSCREEN: u32 = 3;
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
fn get_id() -> usize {
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelHandle {
|
||||
pub handle: ZwlrForeignToplevelHandleV1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
impl PartialEq for ToplevelHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.handle == other.handle
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelHandle {
|
||||
pub fn info(&self) -> Option<ToplevelInfo> {
|
||||
trace!("Retrieving handle info");
|
||||
|
||||
let data = self.handle.data::<ToplevelHandleData>()?;
|
||||
data.info()
|
||||
}
|
||||
|
||||
pub fn focus(&self, seat: &WlSeat) {
|
||||
trace!("Activating handle");
|
||||
self.handle.activate(seat);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleData {
|
||||
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
|
||||
}
|
||||
|
||||
impl ToplevelHandleData {
|
||||
fn info(&self) -> Option<ToplevelInfo> {
|
||||
lock!(self.inner).current_info.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleDataInner {
|
||||
initial_done: bool,
|
||||
output: Option<WlOutput>,
|
||||
|
||||
current_info: Option<ToplevelInfo>,
|
||||
pending_info: ToplevelInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelInfo {
|
||||
pub id: usize,
|
||||
pub app_id: String,
|
||||
pub title: String,
|
||||
pub active: bool,
|
||||
pub fullscreen: bool,
|
||||
|
||||
ready: bool,
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl ToplevelInfo {
|
||||
fn new() -> Self {
|
||||
let id = get_id();
|
||||
impl Default for ToplevelInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id,
|
||||
..Default::default()
|
||||
id: get_unique_usize(),
|
||||
app_id: String::new(),
|
||||
title: String::new(),
|
||||
fullscreen: false,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toplevel;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelEvent {
|
||||
pub toplevel: ToplevelInfo,
|
||||
pub change: ToplevelChange,
|
||||
pub trait ToplevelHandleDataExt {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ToplevelChange {
|
||||
New,
|
||||
Close,
|
||||
Title(String),
|
||||
Focus(bool),
|
||||
Fullscreen(bool),
|
||||
impl ToplevelHandleDataExt for ToplevelHandleData {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn toplevel_implem<F>(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData)
|
||||
pub trait ToplevelHandleHandler: Sized {
|
||||
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
|
||||
where
|
||||
F: FnMut(ToplevelEvent, DispatchData),
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
|
||||
U: ToplevelHandleDataExt,
|
||||
{
|
||||
trace!("event: {event:?} (info: {info:?})");
|
||||
fn event(
|
||||
state: &mut D,
|
||||
handle: &ZwlrForeignToplevelHandleV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
const STATE_ACTIVE: u32 = 2;
|
||||
const STATE_FULLSCREEN: u32 = 3;
|
||||
|
||||
let change = match event {
|
||||
Event::AppId { app_id } => {
|
||||
info.app_id = app_id;
|
||||
None
|
||||
}
|
||||
Event::Title { title } => {
|
||||
info.title = title.clone();
|
||||
let data = data.toplevel_handle_data();
|
||||
|
||||
if info.ready {
|
||||
Some(ToplevelChange::Title(title))
|
||||
} else {
|
||||
None
|
||||
trace!("Processing handle event: {event:?}");
|
||||
|
||||
match event {
|
||||
Event::Title { title } => {
|
||||
lock!(data.inner).pending_info.title = title;
|
||||
}
|
||||
}
|
||||
Event::State { state } => {
|
||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||
// the u32 then represents a value in the `State` enum.
|
||||
assert_eq!(state.len() % 4, 0);
|
||||
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
|
||||
Event::State { state } => {
|
||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||
// the u32 then represents a value in the `State` enum.
|
||||
assert_eq!(state.len() % 4, 0);
|
||||
let state = (0..state.len() / 4)
|
||||
.map(|i| {
|
||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||
.try_into()
|
||||
.expect("Received invalid state length");
|
||||
u32::from_le_bytes(slice)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let state = (0..state.len() / 4)
|
||||
.map(|i| {
|
||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||
.try_into()
|
||||
.expect("Received invalid state length");
|
||||
u32::from_le_bytes(slice)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let new_active = state.contains(&STATE_ACTIVE);
|
||||
let new_fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||
|
||||
let change = if info.ready && new_active != info.active {
|
||||
Some(ToplevelChange::Focus(new_active))
|
||||
} else if info.ready && new_fullscreen != info.fullscreen {
|
||||
Some(ToplevelChange::Fullscreen(new_fullscreen))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
info.active = new_active;
|
||||
info.fullscreen = new_fullscreen;
|
||||
|
||||
change
|
||||
}
|
||||
Event::Closed => {
|
||||
if info.ready {
|
||||
Some(ToplevelChange::Close)
|
||||
} else {
|
||||
None
|
||||
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
|
||||
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
Event::OutputEnter { output: _ }
|
||||
| Event::OutputLeave { output: _ }
|
||||
| Event::Parent { parent: _ } => None,
|
||||
Event::Done => {
|
||||
if info.ready || info.app_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
info.ready = true;
|
||||
Some(ToplevelChange::New)
|
||||
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
|
||||
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
|
||||
Event::Closed => state.remove_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
),
|
||||
Event::Done => {
|
||||
{
|
||||
let pending_info = lock!(data.inner).pending_info.clone();
|
||||
lock!(data.inner).current_info = Some(pending_info);
|
||||
}
|
||||
|
||||
if lock!(data.inner).initial_done {
|
||||
state.update_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
lock!(data.inner).initial_done = true;
|
||||
state.new_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(change) = change {
|
||||
let event = ToplevelEvent {
|
||||
change,
|
||||
toplevel: info.clone(),
|
||||
};
|
||||
|
||||
implem(event, ddata);
|
||||
}
|
||||
}
|
||||
|
||||
impl Toplevel {
|
||||
pub fn init<F>(handle: &Main<ZwlrForeignToplevelHandleV1>, mut callback: F) -> Self
|
||||
where
|
||||
F: FnMut(ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
let inner = Arc::new(RwLock::new(ToplevelInfo::new()));
|
||||
|
||||
handle.quick_assign(move |_handle, event, ddata| {
|
||||
let mut inner = write_lock!(inner);
|
||||
toplevel_implem(event, &mut inner, &mut callback, ddata);
|
||||
});
|
||||
|
||||
Self
|
||||
trace!("Event processed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +1,86 @@
|
||||
use super::handle::{Toplevel, ToplevelEvent};
|
||||
use crate::wayland::LazyGlobal;
|
||||
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::{self, Rc};
|
||||
use tracing::warn;
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use wayland_client::{Attached, DispatchData};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
|
||||
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
|
||||
struct ToplevelHandlerInner {
|
||||
manager: LazyGlobal<ZwlrForeignToplevelManagerV1>,
|
||||
registry: Option<Attached<WlRegistry>>,
|
||||
toplevels: Vec<Toplevel>,
|
||||
pub struct ToplevelManagerState<V = ToplevelHandleData> {
|
||||
manager: ZwlrForeignToplevelManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl ToplevelHandlerInner {
|
||||
const fn new() -> Self {
|
||||
let toplevels = vec![];
|
||||
|
||||
Self {
|
||||
registry: None,
|
||||
manager: LazyGlobal::Unknown,
|
||||
toplevels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToplevelHandler {
|
||||
inner: Rc<RefCell<ToplevelHandlerInner>>,
|
||||
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>>,
|
||||
}
|
||||
|
||||
impl ToplevelHandler {
|
||||
pub fn init() -> Self {
|
||||
let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new()));
|
||||
|
||||
Self {
|
||||
inner,
|
||||
status_listeners: Rc::new(RefCell::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalHandler<ZwlrForeignToplevelManagerV1> for ToplevelHandler {
|
||||
fn created(
|
||||
&mut self,
|
||||
registry: Attached<WlRegistry>,
|
||||
id: u32,
|
||||
version: u32,
|
||||
_ddata: DispatchData,
|
||||
) {
|
||||
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||
if inner.registry.is_none() {
|
||||
inner.registry = Some(registry);
|
||||
}
|
||||
if matches!(inner.manager, LazyGlobal::Unknown) {
|
||||
inner.manager = LazyGlobal::Seen { id, version }
|
||||
} else {
|
||||
warn!(
|
||||
"Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self) -> Option<Attached<ZwlrForeignToplevelManagerV1>> {
|
||||
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||
|
||||
match inner.manager {
|
||||
LazyGlobal::Bound(ref mgr) => Some(mgr.clone()),
|
||||
LazyGlobal::Unknown => None,
|
||||
LazyGlobal::Seen { id, version } => {
|
||||
let registry = inner.registry.as_ref().expect("Failed to get registry");
|
||||
// current max protocol version = 3
|
||||
let version = std::cmp::min(version, 3);
|
||||
let manager = registry.bind::<ZwlrForeignToplevelManagerV1>(version, id);
|
||||
|
||||
{
|
||||
let inner = self.inner.clone();
|
||||
let status_listeners = self.status_listeners.clone();
|
||||
|
||||
manager.quick_assign(move |_, event, _ddata| {
|
||||
let mut inner = RefCell::borrow_mut(&inner);
|
||||
let status_listeners = status_listeners.clone();
|
||||
|
||||
match event {
|
||||
zwlr_foreign_toplevel_manager_v1::Event::Toplevel {
|
||||
toplevel: handle,
|
||||
} => {
|
||||
let toplevel =
|
||||
Toplevel::init(&handle.clone(), move |event, ddata| {
|
||||
notify_status_listeners(
|
||||
&handle,
|
||||
&event,
|
||||
ddata,
|
||||
&status_listeners,
|
||||
);
|
||||
});
|
||||
|
||||
inner.toplevels.push(toplevel);
|
||||
}
|
||||
zwlr_foreign_toplevel_manager_v1::Event::Finished => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inner.manager = LazyGlobal::Bound((*manager).clone());
|
||||
Some((*manager).clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ToplevelStatusCallback =
|
||||
dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||
|
||||
/// Notifies the callbacks of an event on the toplevel
|
||||
fn notify_status_listeners(
|
||||
toplevel: &ZwlrForeignToplevelHandleV1,
|
||||
event: &ToplevelEvent,
|
||||
mut ddata: DispatchData,
|
||||
listeners: &RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>,
|
||||
) {
|
||||
listeners.borrow_mut().retain(|lst| {
|
||||
rc::Weak::upgrade(lst).map_or(false, |cb| {
|
||||
(cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow());
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub struct ToplevelStatusListener {
|
||||
_cb: Rc<RefCell<ToplevelStatusCallback>>,
|
||||
}
|
||||
|
||||
pub trait ToplevelHandling {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
impl ToplevelManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||
}
|
||||
|
||||
impl ToplevelHandling for ToplevelHandler {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
|
||||
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
|
||||
ToplevelStatusListener { _cb: rc }
|
||||
let manager = globals.bind(qh, 1..=3, GlobalData)?;
|
||||
debug!("Bound to ZwlForeignToplevelManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
E: ToplevelHandling,
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
env.with_inner(move |inner| ToplevelHandling::listen(inner, f))
|
||||
pub trait ToplevelManagerHandler: Sized {
|
||||
/// Advertises a new toplevel.
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
manager: ToplevelManagerState,
|
||||
);
|
||||
}
|
||||
|
||||
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
|
||||
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
|
||||
+ ToplevelManagerHandler
|
||||
+ ToplevelHandleHandler
|
||||
+ 'static,
|
||||
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
|
||||
0 => (ZwlrForeignToplevelHandleV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
toplevel_manager: &ZwlrForeignToplevelManagerV1,
|
||||
event: Event,
|
||||
_data: &GlobalData,
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Toplevel { toplevel: _ } => {
|
||||
state.toplevel(
|
||||
conn,
|
||||
qhandle,
|
||||
ToplevelManagerState {
|
||||
manager: toplevel_manager.clone(),
|
||||
_phantom: PhantomData,
|
||||
},
|
||||
);
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,84 @@
|
||||
use std::sync::RwLock;
|
||||
use indexmap::IndexMap;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::trace;
|
||||
use super::Env;
|
||||
use handle::{ToplevelEvent, ToplevelChange, ToplevelInfo};
|
||||
use manager::{ToplevelHandling, ToplevelStatusListener};
|
||||
use wayland_client::DispatchData;
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use crate::{send, write_lock};
|
||||
|
||||
pub mod handle;
|
||||
pub mod manager;
|
||||
|
||||
impl ToplevelHandling for Env {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.toplevel.listen(f)
|
||||
use self::handle::ToplevelHandleHandler;
|
||||
use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
|
||||
use crate::clients::wayland::Environment;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
use crate::send;
|
||||
pub use handle::{ToplevelHandle, ToplevelInfo};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelEvent {
|
||||
New(ToplevelHandle),
|
||||
Update(ToplevelHandle),
|
||||
Remove(ToplevelHandle),
|
||||
}
|
||||
|
||||
impl ToplevelManagerHandler for Environment {
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_manager: ToplevelManagerState,
|
||||
) {
|
||||
debug!("Manager received new handle");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_toplevels(
|
||||
toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>,
|
||||
handle: ZwlrForeignToplevelHandleV1,
|
||||
event: ToplevelEvent,
|
||||
tx: &Sender<ToplevelEvent>,
|
||||
) {
|
||||
trace!("Received toplevel event: {:?}", event);
|
||||
impl ToplevelHandleHandler for Environment {
|
||||
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
|
||||
trace!("Handler received new handle");
|
||||
|
||||
if event.change == ToplevelChange::Close {
|
||||
write_lock!(toplevels).remove(&event.toplevel.id);
|
||||
} else {
|
||||
write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Adding new handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::New(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send!(tx, event);
|
||||
fn update_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
trace!("Handler received handle update");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Updating handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
debug!("Handler received handle close");
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
self.handles.remove(&info.id);
|
||||
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
src/config/common.rs
Normal file
151
src/config/common.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
/// Common configuration options
|
||||
/// which can be set on every module.
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
pub struct CommonConfig {
|
||||
pub class: Option<String>,
|
||||
pub name: Option<String>,
|
||||
|
||||
pub show_if: Option<DynamicBool>,
|
||||
pub transition_type: Option<TransitionType>,
|
||||
pub transition_duration: Option<u32>,
|
||||
|
||||
pub on_click_left: Option<ScriptInput>,
|
||||
pub on_click_right: Option<ScriptInput>,
|
||||
pub on_click_middle: Option<ScriptInput>,
|
||||
pub on_scroll_up: Option<ScriptInput>,
|
||||
pub on_scroll_down: Option<ScriptInput>,
|
||||
pub on_mouse_enter: Option<ScriptInput>,
|
||||
pub on_mouse_exit: Option<ScriptInput>,
|
||||
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TransitionType {
|
||||
None,
|
||||
Crossfade,
|
||||
SlideStart,
|
||||
SlideEnd,
|
||||
}
|
||||
|
||||
impl TransitionType {
|
||||
pub const fn to_revealer_transition_type(
|
||||
&self,
|
||||
orientation: Orientation,
|
||||
) -> RevealerTransitionType {
|
||||
match (self, orientation) {
|
||||
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft,
|
||||
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
||||
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight,
|
||||
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
||||
(Self::Crossfade, _) => RevealerTransitionType::Crossfade,
|
||||
_ => RevealerTransitionType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonConfig {
|
||||
/// Configures the module's container according to the common config options.
|
||||
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.install_show_if(container, revealer);
|
||||
|
||||
let left_click_script = self.on_click_left.map(Script::new_polling);
|
||||
let middle_click_script = self.on_click_middle.map(Script::new_polling);
|
||||
let right_click_script = self.on_click_right.map(Script::new_polling);
|
||||
|
||||
container.connect_button_press_event(move |_, event| {
|
||||
let script = match event.button() {
|
||||
1 => left_click_script.as_ref(),
|
||||
2 => middle_click_script.as_ref(),
|
||||
3 => right_click_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-click script: {}", event.button());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
||||
|
||||
container.connect_scroll_event(move |_, event| {
|
||||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-scroll script: {}", event.direction());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
macro_rules! install_oneshot {
|
||||
($option:expr, $method:ident) => {
|
||||
$option.map(Script::new_polling).map(|script| {
|
||||
container.$method(move |_, _| {
|
||||
script.run_as_oneshot(None);
|
||||
Inhibit(false)
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
install_oneshot!(self.on_mouse_enter, connect_enter_notify_event);
|
||||
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
let container = container.clone();
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.show_if.take().map_or_else(
|
||||
|| {
|
||||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
let container = container.clone();
|
||||
|
||||
{
|
||||
let revealer = revealer.clone();
|
||||
let container = container.clone();
|
||||
|
||||
show_if.subscribe(move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
revealer.connect_child_revealed_notify(move |revealer| {
|
||||
if !revealer.reveals_child() {
|
||||
container.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod common;
|
||||
mod r#impl;
|
||||
mod truncate;
|
||||
|
||||
@@ -7,6 +8,7 @@ use crate::modules::clipboard::ClipboardModule;
|
||||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::label::LabelModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
#[cfg(feature = "music")]
|
||||
use crate::modules::music::MusicModule;
|
||||
@@ -15,36 +17,27 @@ use crate::modules::script::ScriptModule;
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
#[cfg(feature = "tray")]
|
||||
use crate::modules::tray::TrayModule;
|
||||
#[cfg(feature = "upower")]
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use crate::script::ScriptInput;
|
||||
use cfg_if::cfg_if;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use self::common::{CommonConfig, TransitionType};
|
||||
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CommonConfig {
|
||||
pub show_if: Option<ScriptInput>,
|
||||
|
||||
pub on_click_left: Option<ScriptInput>,
|
||||
pub on_click_right: Option<ScriptInput>,
|
||||
pub on_click_middle: Option<ScriptInput>,
|
||||
pub on_scroll_up: Option<ScriptInput>,
|
||||
pub on_scroll_down: Option<ScriptInput>,
|
||||
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
|
||||
#[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>),
|
||||
Custom(Box<CustomModule>),
|
||||
Focused(Box<FocusedModule>),
|
||||
Label(Box<LabelModule>),
|
||||
Launcher(Box<LauncherModule>),
|
||||
#[cfg(feature = "music")]
|
||||
Music(Box<MusicModule>),
|
||||
@@ -53,6 +46,8 @@ pub enum ModuleConfig {
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
#[cfg(feature = "tray")]
|
||||
Tray(Box<TrayModule>),
|
||||
#[cfg(feature = "upower")]
|
||||
Upower(Box<UpowerModule>),
|
||||
#[cfg(feature = "workspaces")]
|
||||
Workspaces(Box<WorkspacesModule>),
|
||||
}
|
||||
@@ -100,10 +95,15 @@ pub struct Config {
|
||||
pub height: i32,
|
||||
#[serde(default)]
|
||||
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>>,
|
||||
@@ -111,10 +111,44 @@ 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
|
||||
}
|
||||
|
||||
const fn default_popup_gap() -> i32 {
|
||||
5
|
||||
}
|
||||
|
||||
pub const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -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,56 +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();
|
||||
|
||||
for dir in dirs {
|
||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||
|
||||
let entry = walker.find(|entry| match entry {
|
||||
Ok(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
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(Ok(entry)) = entry {
|
||||
let path = entry.path().to_owned();
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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();
|
||||
/// 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 mut map = HashMap::new();
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
}
|
||||
|
||||
for line in lines.flatten() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
/// 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<_>>();
|
||||
|
||||
Ok(map)
|
||||
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))
|
||||
})
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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,125 +0,0 @@
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{lock, send};
|
||||
use gtk::prelude::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DynamicStringSegment {
|
||||
Static(String),
|
||||
Dynamic(Script),
|
||||
}
|
||||
|
||||
pub struct DynamicString;
|
||||
|
||||
impl DynamicString {
|
||||
pub fn new<F>(input: &str, f: F) -> Self
|
||||
where
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let mut segments = vec![];
|
||||
|
||||
let mut chars = input.chars().collect::<Vec<_>>();
|
||||
while !chars.is_empty() {
|
||||
let char = &chars[..=1];
|
||||
|
||||
let (token, skip) = if let ['{', '{'] = char {
|
||||
const SKIP_BRACKETS: usize = 4;
|
||||
|
||||
let str = chars
|
||||
.iter()
|
||||
.skip(2)
|
||||
.enumerate()
|
||||
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
|
||||
.map(|(_, c)| c)
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(
|
||||
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
||||
len + SKIP_BRACKETS,
|
||||
)
|
||||
} else {
|
||||
let str = chars
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
|
||||
.map(|(_, c)| c)
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(DynamicStringSegment::Static(str), len)
|
||||
};
|
||||
|
||||
assert_ne!(skip, 0);
|
||||
|
||||
segments.push(token);
|
||||
chars.drain(..skip);
|
||||
}
|
||||
|
||||
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(|(out, _)| {
|
||||
if let OutputStream::Stdout(out) = out {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
let _ = 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
|
||||
}
|
||||
}
|
||||
|
||||
#[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,12 +2,13 @@
|
||||
pub enum ExitCode {
|
||||
GtkDisplay = 1,
|
||||
CreateBars = 2,
|
||||
Config = 3,
|
||||
}
|
||||
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
||||
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
|
||||
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
|
||||
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
|
||||
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
|
||||
|
||||
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/gtk_helpers.rs
Normal file
77
src/gtk_helpers.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use glib::IsA;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Orientation, Widget};
|
||||
|
||||
/// 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,7 +1,7 @@
|
||||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
@@ -9,17 +9,17 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
image.set_widget_name("image");
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, size)
|
||||
.and_then(|provider| provider.load_into_image(image.clone()))
|
||||
match ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Ok(_) => {
|
||||
Some(_) => {
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
None => {
|
||||
button.set_label(input);
|
||||
}
|
||||
}
|
||||
@@ -36,18 +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();
|
||||
image.set_widget_name("image");
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
||||
.and_then(|provider| provider.load_into_image(image))
|
||||
{
|
||||
error!("{err:?}");
|
||||
}
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
label.set_widget_name("label");
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::desktop_file::get_desktop_icon_name;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::warn;
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
@@ -38,60 +41,98 @@ 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) -> Result<Self> {
|
||||
let location = Self::get_location(input, theme, size)?;
|
||||
Ok(Self { location, 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) -> Result<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));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
|
||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
}),
|
||||
Some(input_type) if input_type == "file" => Ok(ImageLocation::Local(PathBuf::from(
|
||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
||||
input_name[2..].to_string(),
|
||||
))),
|
||||
#[cfg(feature = "http")]
|
||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||
Ok(ImageLocation::Remote(input.parse()?))
|
||||
input.parse().ok().map(ImageLocation::Remote)
|
||||
}
|
||||
None if input.starts_with("steam_app_") => Ok(ImageLocation::Steam(
|
||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Ok(ImageLocation::Icon {
|
||||
Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
})
|
||||
}
|
||||
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")),
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
Some(input_type) => {
|
||||
warn!(
|
||||
"{:?}",
|
||||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
fallback!()
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
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}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
None => get_desktop_icon_name(input_name).map_or_else(
|
||||
|| Err(Report::msg(format!("Unknown image type: '{input}'"))),
|
||||
|input| Self::get_location(&input, theme, size),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,17 +156,24 @@ impl<'a> ImageProvider<'a> {
|
||||
let size = self.size;
|
||||
rx.attach(None, move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
size,
|
||||
size,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
);
|
||||
|
||||
match pixbuf {
|
||||
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
||||
{
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Continue(false)
|
||||
@@ -141,26 +189,46 @@ impl<'a> ImageProvider<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||
let scale = image.scale_factor();
|
||||
|
||||
let pixbuf = match &self.location {
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
|
||||
ImageLocation::Local(path) => self.get_from_file(path),
|
||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
||||
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
||||
#[cfg(feature = "http")]
|
||||
_ => unreachable!(), // handled above
|
||||
}?;
|
||||
|
||||
image.set_pixbuf(Some(&pixbuf));
|
||||
Self::create_and_load_surface(&pixbuf, image, scale)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image, scale: i32) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr =
|
||||
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme) -> Result<Pixbuf> {
|
||||
let pixbuf = match theme.lookup_icon(name, self.size, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||
let pixbuf =
|
||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
@@ -169,14 +237,15 @@ impl<'a> ImageProvider<'a> {
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
|
||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||
let scaled_size = self.size * scale;
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||
Ok(pixbuf)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file,
|
||||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
|
||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
@@ -187,13 +256,29 @@ impl<'a> ImageProvider<'a> {
|
||||
},
|
||||
)?;
|
||||
|
||||
self.get_from_file(&path)
|
||||
self.get_from_file(&path, scale)
|
||||
}
|
||||
|
||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||
#[cfg(feature = "http")]
|
||||
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
|
||||
let bytes = reqwest::get(url).await?.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
let res = reqwest::get(url).await?;
|
||||
|
||||
let status = res.status();
|
||||
if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ fn install_tracing() -> Result<WorkerGuard> {
|
||||
const DEFAULT_LOG: &str = "info";
|
||||
const DEFAULT_FILE_LOG: &str = "warn";
|
||||
|
||||
let fmt_layer = fmt::layer().with_target(true);
|
||||
let fmt_layer = fmt::layer().with_target(true).with_line_number(true);
|
||||
let filter_layer =
|
||||
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
||||
|
||||
|
||||
@@ -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,22 +46,23 @@ macro_rules! try_send {
|
||||
/// Locks a `Mutex`.
|
||||
/// Panics if the `Mutex` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut val = lock!(my_mutex);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! lock {
|
||||
($mutex:expr) => {
|
||||
($mutex:expr) => {{
|
||||
tracing::trace!("Locking {}", std::stringify!($mutex));
|
||||
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
|
||||
};
|
||||
}};
|
||||
}
|
||||
|
||||
/// Gets a read lock on a `RwLock`.
|
||||
/// Panics if the `RwLock` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = read_lock!(my_rwlock);
|
||||
@@ -76,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);
|
||||
@@ -87,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))
|
||||
};
|
||||
}
|
||||
|
||||
335
src/main.rs
335
src/main.rs
@@ -1,84 +1,124 @@
|
||||
#![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;
|
||||
mod popup;
|
||||
mod script;
|
||||
mod style;
|
||||
|
||||
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::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
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;
|
||||
mod unique_id;
|
||||
|
||||
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
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| {
|
||||
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 = match env::var("IRONBAR_CONFIG") {
|
||||
Ok(path) => ConfigLoader::load(path),
|
||||
Err(_) => ConfigLoader::new("ironbar").find_and_load(),
|
||||
};
|
||||
|
||||
let config = match config_res {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::Config as i32)
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Loaded config file");
|
||||
|
||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::CreateBars as i32);
|
||||
if running.get() {
|
||||
info!("Ironbar already running, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("Created bars");
|
||||
running.set(true);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
let ipc = ipc::Ipc::new(global_state2.clone());
|
||||
ipc.start(app);
|
||||
}
|
||||
}
|
||||
|
||||
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
||||
|| {
|
||||
@@ -97,66 +137,163 @@ async fn main() -> Result<()> {
|
||||
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());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates each of the bars across each of the (configured) outputs.
|
||||
fn create_bars(
|
||||
app: &Application,
|
||||
display: &Display,
|
||||
wl: &WaylandClient,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
let outputs = wl.outputs.as_slice();
|
||||
/// 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();
|
||||
}
|
||||
|
||||
debug!("Received {} outputs from Wayland", outputs.len());
|
||||
debug!("Outputs: {:?}", outputs);
|
||||
let config = load_config();
|
||||
|
||||
let num_monitors = display.n_monitors();
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
let outputs = wl.get_outputs();
|
||||
|
||||
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_name = &output.name;
|
||||
|
||||
config.monitors.as_ref().map_or_else(
|
||||
|| {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())
|
||||
},
|
||||
|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())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
},
|
||||
)?;
|
||||
let display = get_display();
|
||||
for output in outputs.iter() {
|
||||
create_bars_for_monitor(app, &display, output, config.clone(), global_state)?;
|
||||
}
|
||||
|
||||
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(());
|
||||
};
|
||||
|
||||
let monitor = match get_monitor(&monitor_name, display) {
|
||||
Ok(monitor) => monitor,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let Some(monitor_config) = config.get_monitor_config(&monitor_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
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(
|
||||
|| {
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
error!("{:?}", report);
|
||||
exit(ExitCode::GtkDisplay as i32)
|
||||
},
|
||||
|display| display,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
monitor.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))
|
||||
}
|
||||
|
||||
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};
|
||||
@@ -21,6 +22,9 @@ pub struct ClipboardModule {
|
||||
#[serde(default = "default_icon")]
|
||||
icon: String,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
#[serde(default = "default_max_items")]
|
||||
max_items: usize,
|
||||
|
||||
@@ -35,6 +39,10 @@ fn default_icon() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
const fn default_max_items() -> usize {
|
||||
10
|
||||
}
|
||||
@@ -73,7 +81,7 @@ impl Module<Button> for ClipboardModule {
|
||||
spawn(async move {
|
||||
let mut rx = {
|
||||
let client = clipboard::get_client();
|
||||
client.subscribe(max_items).await
|
||||
client.subscribe(max_items)
|
||||
};
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
@@ -104,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),
|
||||
}
|
||||
}
|
||||
@@ -117,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;
|
||||
|
||||
let button = new_icon_button(&self.icon, info.icon_theme, 32);
|
||||
) -> 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::button_pos(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(
|
||||
@@ -147,11 +156,7 @@ impl Module<Button> for ClipboardModule {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(10)
|
||||
.name("popup-clipboard")
|
||||
.build();
|
||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||
|
||||
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
||||
container.add(&entries);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::config::CommonConfig;
|
||||
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::*;
|
||||
@@ -12,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.
|
||||
@@ -22,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 = ();
|
||||
@@ -59,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::button_pos(button, orientation))
|
||||
ModuleUpdateEvent::TogglePopup(button.popup_id())
|
||||
);
|
||||
});
|
||||
|
||||
let format = self.format.clone();
|
||||
{
|
||||
context.widget_rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(&format));
|
||||
label.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
context.widget_rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
label.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup,
|
||||
})
|
||||
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(
|
||||
@@ -96,29 +133,25 @@ impl Module<Button> for ClockModule {
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.name("popup-clock")
|
||||
.build();
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
let clock = Label::builder()
|
||||
.name("calendar-clock")
|
||||
.halign(Align::Center)
|
||||
.build();
|
||||
let format = "%H:%M:%S";
|
||||
let clock = Label::builder().halign(Align::Center).build();
|
||||
clock.add_class("calendar-clock");
|
||||
|
||||
container.add(&clock);
|
||||
|
||||
let calendar = Calendar::builder().name("calendar").build();
|
||||
let calendar = Calendar::new();
|
||||
calendar.add_class("calendar");
|
||||
container.add(&calendar);
|
||||
|
||||
{
|
||||
rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(format));
|
||||
clock.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
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_localized(&format, locale));
|
||||
clock.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
container.show_all();
|
||||
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::{ButtonGeometry, Popup};
|
||||
use crate::script::Script;
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CustomModule {
|
||||
/// Container class name
|
||||
class: Option<String>,
|
||||
/// Widgets to add to the bar container
|
||||
bar: Vec<Widget>,
|
||||
/// Widgets to add to the popup container
|
||||
popup: Option<Vec<Widget>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
/// Attempts to parse an `Orientation` from `String`
|
||||
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||
match orientation.to_lowercase().as_str() {
|
||||
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget attributes
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Widget {
|
||||
/// Type of GTK widget to add
|
||||
#[serde(rename = "type")]
|
||||
widget_type: WidgetType,
|
||||
widgets: Option<Vec<Widget>>,
|
||||
label: Option<String>,
|
||||
name: Option<String>,
|
||||
class: Option<String>,
|
||||
on_click: Option<String>,
|
||||
orientation: Option<String>,
|
||||
src: Option<String>,
|
||||
size: Option<i32>,
|
||||
}
|
||||
|
||||
/// Supported GTK widget types
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WidgetType {
|
||||
Box,
|
||||
Label,
|
||||
Button,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(
|
||||
self,
|
||||
parent: >k::Box,
|
||||
tx: Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &IconTheme,
|
||||
) {
|
||||
match self.widget_type {
|
||||
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)),
|
||||
WidgetType::Label => parent.add(&self.into_label()),
|
||||
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
||||
WidgetType::Image => parent.add(&self.into_image(icon_theme)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Box` from this widget
|
||||
fn into_box(
|
||||
self,
|
||||
tx: &Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &IconTheme,
|
||||
) -> gtk::Box {
|
||||
let mut builder = gtk::Box::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
builder = builder
|
||||
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
|
||||
}
|
||||
|
||||
let container = builder.build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
for widget in widgets {
|
||||
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
|
||||
}
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Label` from this widget
|
||||
fn into_label(self) -> Label {
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(name);
|
||||
}
|
||||
|
||||
let label = builder.build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
label.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
let text = self.label.map_or_else(String::new, |text| text);
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
DynamicString::new(&text, move |string| {
|
||||
label.set_label(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Button` from this widget
|
||||
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
|
||||
let mut builder = Button::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(name);
|
||||
}
|
||||
|
||||
let button = builder.build();
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
label.set_markup(&text);
|
||||
button.add(&label);
|
||||
}
|
||||
|
||||
if let Some(class) = self.class {
|
||||
button.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(exec) = self.on_click {
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
geometry: Popup::button_pos(button, bar_orientation),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
fn into_image(self, icon_theme: &IconTheme) -> gtk::Image {
|
||||
let mut builder = gtk::Image::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
let gtk_image = builder.build();
|
||||
|
||||
if let Some(src) = self.src {
|
||||
let size = self.size.unwrap_or(32);
|
||||
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
|
||||
.and_then(|image| image.load_into_image(gtk_image.clone()))
|
||||
{
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(class) = self.class {
|
||||
gtk_image.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
gtk_image
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
geometry: ButtonGeometry,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
type SendMessage = ();
|
||||
type ReceiveMessage = ExecEvent;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if event.cmd.starts_with('!') {
|
||||
let script = Script::from(&event.cmd[1..]);
|
||||
|
||||
debug!("executing command: '{}'", script.cmd);
|
||||
// TODO: Migrate to use script.run
|
||||
if let Err(err) = script.get_output().await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
|
||||
} else if event.cmd == "popup:open" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
} else {
|
||||
error!("Received invalid command: '{}'", event.cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let container = gtk::Box::builder().orientation(orientation).build();
|
||||
|
||||
if let Some(ref class) = self.class {
|
||||
container.style_context().add_class(class);
|
||||
}
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
widget.add_to(
|
||||
&container,
|
||||
context.controller_tx.clone(),
|
||||
orientation,
|
||||
info.icon_theme,
|
||||
);
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder().name("popup-custom").build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container
|
||||
.style_context()
|
||||
.add_class(format!("popup-{class}").as_str());
|
||||
}
|
||||
|
||||
if let Some(popup) = self.popup {
|
||||
for widget in popup {
|
||||
widget.add_to(
|
||||
&container,
|
||||
tx.clone(),
|
||||
Orientation::Horizontal,
|
||||
info.icon_theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
36
src/modules/custom/box.rs
Normal file
36
src/modules/custom/box.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::modules::custom::WidgetConfig;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Orientation;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct BoxWidget {
|
||||
name: Option<String>,
|
||||
class: Option<String>,
|
||||
orientation: Option<String>,
|
||||
widgets: Option<Vec<WidgetConfig>>,
|
||||
}
|
||||
|
||||
impl CustomWidget for BoxWidget {
|
||||
type Widget = gtk::Box;
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let container = build!(self, Self::Widget);
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
container.set_orientation(
|
||||
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
for widget in widgets {
|
||||
widget.widget.add_to(&container, &context, widget.common);
|
||||
}
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
}
|
||||
54
src/modules/custom/button.rs
Normal file
54
src/modules/custom/button.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
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>,
|
||||
class: Option<String>,
|
||||
label: Option<String>,
|
||||
on_click: Option<String>,
|
||||
}
|
||||
|
||||
impl CustomWidget for ButtonWidget {
|
||||
type Widget = Button;
|
||||
|
||||
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);
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(exec) = self.on_click {
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
args: None,
|
||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
44
src/modules/custom/image.rs
Normal file
44
src/modules/custom/image.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
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>,
|
||||
class: Option<String>,
|
||||
src: String,
|
||||
#[serde(default = "default_size")]
|
||||
size: i32,
|
||||
}
|
||||
|
||||
const fn default_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
impl CustomWidget for ImageWidget {
|
||||
type Widget = Image;
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let gtk_image = build!(self, Self::Widget);
|
||||
|
||||
{
|
||||
let gtk_image = gtk_image.clone();
|
||||
let icon_theme = context.icon_theme.clone();
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
gtk_image
|
||||
}
|
||||
}
|
||||
35
src/modules/custom/label.rs
Normal file
35
src/modules/custom/label.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
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>,
|
||||
class: Option<String>,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl CustomWidget for LabelWidget {
|
||||
type Widget = Label;
|
||||
|
||||
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
|
||||
let label = build!(self, Self::Widget);
|
||||
|
||||
label.set_use_markup(true);
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
dynamic_string(&self.label, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
label
|
||||
}
|
||||
}
|
||||
255
src/modules/custom/mod.rs
Normal file
255
src/modules/custom/mod.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
mod r#box;
|
||||
mod button;
|
||||
mod image;
|
||||
mod label;
|
||||
mod progress;
|
||||
mod slider;
|
||||
|
||||
use self::image::ImageWidget;
|
||||
use self::label::LabelWidget;
|
||||
use self::r#box::BoxWidget;
|
||||
use self::slider::SliderWidget;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::custom::button::ButtonWidget;
|
||||
use crate::modules::custom::progress::ProgressWidget;
|
||||
use crate::modules::{
|
||||
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::script::Script;
|
||||
use crate::send_async;
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
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};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CustomModule {
|
||||
/// Widgets to add to the bar container
|
||||
bar: Vec<WidgetConfig>,
|
||||
/// Widgets to add to the popup container
|
||||
popup: Option<Vec<WidgetConfig>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WidgetConfig {
|
||||
#[serde(flatten)]
|
||||
widget: Widget,
|
||||
#[serde(flatten)]
|
||||
common: CommonConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Widget {
|
||||
Box(BoxWidget),
|
||||
Label(LabelWidget),
|
||||
Button(ButtonWidget),
|
||||
Image(ImageWidget),
|
||||
Slider(SliderWidget),
|
||||
Progress(ProgressWidget),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CustomWidgetContext<'a> {
|
||||
tx: &'a Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &'a IconTheme,
|
||||
popup_buttons: Rc<RefCell<Vec<Button>>>,
|
||||
}
|
||||
|
||||
trait CustomWidget {
|
||||
type Widget;
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget;
|
||||
}
|
||||
|
||||
/// Creates a new widget of type `ty`,
|
||||
/// setting its name and class based on
|
||||
/// the values available on `self`.
|
||||
#[macro_export]
|
||||
macro_rules! build {
|
||||
($self:ident, $ty:ty) => {{
|
||||
let mut builder = <$ty>::builder();
|
||||
|
||||
if let Some(name) = &$self.name {
|
||||
builder = builder.name(name);
|
||||
}
|
||||
|
||||
let widget = builder.build();
|
||||
|
||||
if let Some(class) = &$self.class {
|
||||
widget.style_context().add_class(class);
|
||||
}
|
||||
|
||||
widget
|
||||
}};
|
||||
}
|
||||
|
||||
/// Sets the widget length,
|
||||
/// using either a width or height request
|
||||
/// based on the bar's orientation.
|
||||
pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orientation) {
|
||||
match bar_orientation {
|
||||
Orientation::Horizontal => widget.set_width_request(length),
|
||||
Orientation::Vertical => widget.set_height_request(length),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
/// Attempts to parse an `Orientation` from `String`.
|
||||
/// Will accept `horizontal`, `vertical`, `h` or `v`.
|
||||
/// Ignores case.
|
||||
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||
match orientation.to_lowercase().as_str() {
|
||||
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(self, parent: >k::Box, context: &CustomWidgetContext, common: CommonConfig) {
|
||||
macro_rules! create {
|
||||
($widget:expr) => {
|
||||
wrap_widget(
|
||||
&$widget.into_widget(context.clone()),
|
||||
common,
|
||||
context.bar_orientation,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let event_box = match self {
|
||||
Self::Box(widget) => create!(widget),
|
||||
Self::Label(widget) => create!(widget),
|
||||
Self::Button(widget) => create!(widget),
|
||||
Self::Image(widget) => create!(widget),
|
||||
Self::Slider(widget) => create!(widget),
|
||||
Self::Progress(widget) => create!(widget),
|
||||
};
|
||||
|
||||
parent.add(&event_box);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
args: Option<Vec<String>>,
|
||||
id: usize,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
type SendMessage = ();
|
||||
type ReceiveMessage = ExecEvent;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if event.cmd.starts_with('!') {
|
||||
let script = Script::from(&event.cmd[1..]);
|
||||
|
||||
debug!("executing command: '{}'", script.cmd);
|
||||
|
||||
let args = event.args.unwrap_or_default();
|
||||
|
||||
if let Err(err) = script.get_output(Some(&args)).await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
|
||||
} else if event.cmd == "popup:open" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
} else {
|
||||
error!("Received invalid command: '{}'", event.cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> 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);
|
||||
});
|
||||
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, context.popup_rx, info)
|
||||
.into_popup_parts_owned(popup_buttons.take());
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
if let Some(popup) = self.popup {
|
||||
let custom_context = CustomWidgetContext {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
82
src/modules/custom/progress.rs
Normal file
82
src/modules/custom/progress.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
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>,
|
||||
class: Option<String>,
|
||||
orientation: Option<String>,
|
||||
label: Option<String>,
|
||||
value: Option<ScriptInput>,
|
||||
#[serde(default = "default_max")]
|
||||
max: f64,
|
||||
length: Option<i32>,
|
||||
}
|
||||
|
||||
const fn default_max() -> f64 {
|
||||
100.0
|
||||
}
|
||||
|
||||
impl CustomWidget for ProgressWidget {
|
||||
type Widget = ProgressBar;
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let progress = build!(self, Self::Widget);
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
progress.set_orientation(
|
||||
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(length) = self.length {
|
||||
set_length(&progress, length, context.bar_orientation);
|
||||
}
|
||||
|
||||
if let Some(value) = self.value {
|
||||
let script = Script::from(value);
|
||||
let progress = progress.clone();
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
||||
Ok(value) => send!(tx, value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
rx.attach(None, move |value| {
|
||||
progress.set_fraction(value / self.max);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let progress = progress.clone();
|
||||
progress.set_show_text(true);
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
progress.set_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
progress
|
||||
}
|
||||
}
|
||||
130
src/modules/custom/slider.rs
Normal file
130
src/modules/custom/slider.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::Scale;
|
||||
use serde::Deserialize;
|
||||
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>,
|
||||
class: Option<String>,
|
||||
orientation: Option<String>,
|
||||
value: Option<ScriptInput>,
|
||||
on_change: Option<String>,
|
||||
#[serde(default = "default_min")]
|
||||
min: f64,
|
||||
#[serde(default = "default_max")]
|
||||
max: f64,
|
||||
step: Option<f64>,
|
||||
length: Option<i32>,
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_label: bool,
|
||||
}
|
||||
|
||||
const fn default_min() -> f64 {
|
||||
0.0
|
||||
}
|
||||
|
||||
const fn default_max() -> f64 {
|
||||
100.0
|
||||
}
|
||||
|
||||
impl CustomWidget for SliderWidget {
|
||||
type Widget = Scale;
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let scale = build!(self, Self::Widget);
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
scale.set_orientation(
|
||||
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(length) = self.length {
|
||||
set_length(&scale, length, context.bar_orientation);
|
||||
}
|
||||
|
||||
scale.set_range(self.min, self.max);
|
||||
scale.set_draw_value(self.show_label);
|
||||
|
||||
if let Some(on_change) = self.on_change {
|
||||
let min = self.min;
|
||||
let max = self.max;
|
||||
let step = self.step;
|
||||
let tx = context.tx.clone();
|
||||
|
||||
// GTK will spam the same value over and over
|
||||
let prev_value = Cell::new(scale.value());
|
||||
|
||||
scale.connect_scroll_event(move |scale, event| {
|
||||
let value = scale.value();
|
||||
let delta = event.delta().1.neg();
|
||||
|
||||
let delta = match (step, delta.is_sign_positive()) {
|
||||
(Some(step), true) => step,
|
||||
(Some(step), false) => -step,
|
||||
(None, _) => delta,
|
||||
};
|
||||
|
||||
scale.set_value(value + delta);
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
scale.connect_change_value(move |_, _, val| {
|
||||
// GTK will send values outside min/max range
|
||||
let val = val.clamp(min, max);
|
||||
|
||||
if val != prev_value.get() {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: on_change.clone(),
|
||||
args: Some(vec![val.to_string()]),
|
||||
id: usize::MAX // ignored
|
||||
}
|
||||
);
|
||||
|
||||
prev_value.set(val);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(value) = self.value {
|
||||
let script = Script::from(value);
|
||||
let scale = scale.clone();
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse() {
|
||||
Ok(value) => send!(tx, value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
rx.attach(None, move |value| {
|
||||
scale.set_value(value);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
scale
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::clients::wayland::{self, ToplevelChange};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{await_sync, read_lock, send_async};
|
||||
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::*;
|
||||
@@ -10,7 +11,7 @@ use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::error;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct FocusedModule {
|
||||
@@ -31,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
|
||||
}
|
||||
@@ -49,38 +62,37 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let focused = await_sync(async {
|
||||
let wl = wayland::get_client().await;
|
||||
let toplevels = read_lock!(wl.toplevels);
|
||||
|
||||
toplevels
|
||||
.iter()
|
||||
.find(|(_, (top, _))| top.active)
|
||||
.map(|(_, (top, _))| top.clone())
|
||||
});
|
||||
|
||||
if let Some(top) = focused {
|
||||
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
|
||||
}
|
||||
|
||||
spawn(async move {
|
||||
let mut wlrx = {
|
||||
let wl = wayland::get_client().await;
|
||||
let (mut wlrx, handles) = {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_toplevels()
|
||||
};
|
||||
|
||||
while let Ok(event) = wlrx.recv().await {
|
||||
let update = match event.change {
|
||||
ToplevelChange::Focus(focus) => focus,
|
||||
ToplevelChange::Title(_) => event.toplevel.active,
|
||||
_ => false,
|
||||
};
|
||||
let focused = handles.values().find_map(|handle| {
|
||||
handle
|
||||
.info()
|
||||
.and_then(|info| if info.focused { Some(info) } else { None })
|
||||
});
|
||||
|
||||
if update {
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update((event.toplevel.title, event.toplevel.app_id))
|
||||
);
|
||||
if let Some(focused) = focused {
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update((focused.title.clone(), focused.app_id))
|
||||
);
|
||||
};
|
||||
|
||||
while let Ok(event) = wlrx.recv().await {
|
||||
if let ToplevelEvent::Update(handle) = event {
|
||||
let info = handle.info().unwrap_or_default();
|
||||
|
||||
if info.focused {
|
||||
debug!("Changing focus");
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update((info.title.clone(), info.app_id.clone()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -92,29 +104,35 @@ 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::builder().name("icon").build();
|
||||
let label = Label::builder().name("label").build();
|
||||
let icon = gtk::Image::new();
|
||||
if self.show_icon {
|
||||
icon.add_class("icon");
|
||||
container.add(&icon);
|
||||
}
|
||||
|
||||
let label = Label::new(None);
|
||||
label.add_class("label");
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
}
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
|
||||
{
|
||||
let icon_theme = icon_theme.clone();
|
||||
context.widget_rx.attach(None, move |(name, id)| {
|
||||
if self.show_icon {
|
||||
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||
.and_then(|image| image.load_into_image(icon.clone()))
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
error!("{err:?}");
|
||||
Some(Ok(_)) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +144,7 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleWidget {
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
72
src/modules/label.rs
Normal file
72
src/modules/label.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use crate::config::CommonConfig;
|
||||
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;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LabelModule {
|
||||
label: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
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 = ();
|
||||
|
||||
fn name() -> &'static str {
|
||||
"label"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
dynamic_string(&self.label, move |string| {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> 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_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
use super::open_state::OpenState;
|
||||
use crate::clients::wayland::ToplevelInfo;
|
||||
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;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::error;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Item {
|
||||
@@ -34,24 +37,30 @@ impl Item {
|
||||
}
|
||||
|
||||
/// Merges the provided node into this launcher item
|
||||
pub fn merge_toplevel(&mut self, node: ToplevelInfo) -> Window {
|
||||
let id = node.id;
|
||||
pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> {
|
||||
let info = handle
|
||||
.info()
|
||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
||||
|
||||
let id = info.id;
|
||||
|
||||
if self.windows.is_empty() {
|
||||
self.name = node.title.clone();
|
||||
self.name = info.title;
|
||||
}
|
||||
|
||||
let window: Window = node.into();
|
||||
let window = Window::try_from(handle)?;
|
||||
self.windows.insert(id, window.clone());
|
||||
|
||||
self.recalculate_open_state();
|
||||
|
||||
window
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
pub fn unmerge_toplevel(&mut self, node: &ToplevelInfo) {
|
||||
self.windows.remove(&node.id);
|
||||
self.recalculate_open_state();
|
||||
pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) {
|
||||
if let Some(info) = handle.info() {
|
||||
self.windows.remove(&info.id);
|
||||
self.recalculate_open_state();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_window_name(&mut self, window_id: usize, name: String) {
|
||||
@@ -87,22 +96,29 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToplevelInfo> for Item {
|
||||
fn from(toplevel: ToplevelInfo) -> Self {
|
||||
let open_state = OpenState::from_toplevel(&toplevel);
|
||||
let name = toplevel.title.clone();
|
||||
let app_id = toplevel.app_id.clone();
|
||||
impl TryFrom<ToplevelHandle> for Item {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> {
|
||||
let info = handle
|
||||
.info()
|
||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
||||
|
||||
let name = info.title.clone();
|
||||
let app_id = info.app_id.clone();
|
||||
let open_state = OpenState::from(&info);
|
||||
|
||||
let mut windows = IndexMap::new();
|
||||
windows.insert(toplevel.id, toplevel.into());
|
||||
let window = Window::try_from(handle)?;
|
||||
windows.insert(info.id, window);
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
app_id,
|
||||
favorite: false,
|
||||
open_state,
|
||||
windows,
|
||||
name,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,17 +127,30 @@ pub struct Window {
|
||||
pub id: usize,
|
||||
pub name: String,
|
||||
pub open_state: OpenState,
|
||||
handle: ToplevelHandle,
|
||||
}
|
||||
|
||||
impl From<ToplevelInfo> for Window {
|
||||
fn from(node: ToplevelInfo) -> Self {
|
||||
let open_state = OpenState::from_toplevel(&node);
|
||||
impl TryFrom<ToplevelHandle> for Window {
|
||||
type Error = Report;
|
||||
|
||||
Self {
|
||||
id: node.id,
|
||||
name: node.title,
|
||||
fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> {
|
||||
let info = handle
|
||||
.info()
|
||||
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
|
||||
let open_state = OpenState::from(&info);
|
||||
|
||||
Ok(Self {
|
||||
id: info.id,
|
||||
name: info.title,
|
||||
open_state,
|
||||
}
|
||||
handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn focus(&self, seat: &WlSeat) {
|
||||
self.handle.focus(seat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,37 +165,41 @@ pub struct ItemButton {
|
||||
pub menu_state: Rc<RwLock<MenuState>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct AppearanceOptions {
|
||||
pub show_names: bool,
|
||||
pub show_icons: bool,
|
||||
pub icon_size: i32,
|
||||
}
|
||||
|
||||
impl ItemButton {
|
||||
pub fn new(
|
||||
item: &Item,
|
||||
show_names: bool,
|
||||
show_icons: bool,
|
||||
orientation: Orientation,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
bar_position: BarPosition,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
) -> Self {
|
||||
let mut button = Button::builder();
|
||||
|
||||
if show_names {
|
||||
if appearance.show_names {
|
||||
button = button.label(&item.name);
|
||||
}
|
||||
|
||||
let button = button.build();
|
||||
|
||||
if show_icons {
|
||||
if appearance.show_icons {
|
||||
let gtk_image = gtk::Image::new();
|
||||
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32);
|
||||
match image {
|
||||
Ok(image) => {
|
||||
button.set_image(Some(>k_image));
|
||||
button.set_always_show_image(true);
|
||||
let image =
|
||||
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);
|
||||
|
||||
if let Err(err) = image.load_into_image(gtk_image) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
if let Err(err) = image.load_into_image(gtk_image) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,7 +250,9 @@ impl ItemButton {
|
||||
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::OpenPopup(Popup::button_pos(button, orientation,))
|
||||
ModuleUpdateEvent::OpenPopupAt(
|
||||
button.geometry(bar_position.get_orientation())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
@@ -227,12 +262,37 @@ 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 {
|
||||
button,
|
||||
persistent: item.favorite,
|
||||
show_names,
|
||||
show_names: appearance.show_names,
|
||||
menu_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ mod open_state;
|
||||
|
||||
use self::item::{Item, ItemButton, Window};
|
||||
use self::open_state::OpenState;
|
||||
use crate::clients::wayland::{self, ToplevelChange};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{lock, read_lock, try_send, write_lock};
|
||||
use crate::modules::launcher::item::AppearanceOptions;
|
||||
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::*;
|
||||
@@ -15,9 +18,8 @@ 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;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
@@ -33,10 +35,17 @@ pub struct LauncherModule {
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_icons: bool,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LauncherUpdate {
|
||||
/// Adds item
|
||||
@@ -101,72 +110,68 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
.collect::<IndexMap<_, _>>()
|
||||
});
|
||||
|
||||
let items = Arc::new(Mutex::new(items));
|
||||
let items = arc_mut!(items);
|
||||
|
||||
{
|
||||
let items = Arc::clone(&items);
|
||||
let tx = tx.clone();
|
||||
spawn(async move {
|
||||
let wl = wayland::get_client().await;
|
||||
let open_windows = read_lock!(wl.toplevels);
|
||||
let items2 = Arc::clone(&items);
|
||||
let tx2 = tx.clone();
|
||||
spawn(async move {
|
||||
let items = items2;
|
||||
let tx = tx2;
|
||||
|
||||
let open_windows = open_windows.clone();
|
||||
for (_, (window, _)) in open_windows {
|
||||
let mut items = lock!(items);
|
||||
let item = items.get_mut(&window.app_id);
|
||||
match item {
|
||||
Some(item) => {
|
||||
item.merge_toplevel(window);
|
||||
}
|
||||
None => {
|
||||
items.insert(window.app_id.clone(), window.into());
|
||||
}
|
||||
let (mut wlrx, handles) = {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_toplevels()
|
||||
};
|
||||
|
||||
for handle in handles.values() {
|
||||
let Some(info) = handle.info() else { continue };
|
||||
|
||||
let mut items = lock!(items);
|
||||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
Some(item) => {
|
||||
item.merge_toplevel(handle.clone())?;
|
||||
}
|
||||
None => {
|
||||
items.insert(info.app_id.clone(), Item::try_from(handle.clone())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let items = lock!(items);
|
||||
let items = items.iter();
|
||||
for (_, item) in items {
|
||||
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||
item.clone(),
|
||||
)))?;
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(LauncherUpdate::AddItem(item.clone()))
|
||||
);
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
}
|
||||
|
||||
let items2 = Arc::clone(&items);
|
||||
spawn(async move {
|
||||
let items = items2;
|
||||
|
||||
let mut wlrx = {
|
||||
let wl = wayland::get_client().await;
|
||||
wl.subscribe_toplevels()
|
||||
};
|
||||
}
|
||||
|
||||
let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
|
||||
|
||||
while let Ok(event) = wlrx.recv().await {
|
||||
trace!("event: {:?}", event);
|
||||
|
||||
let window = event.toplevel;
|
||||
let app_id = window.app_id.clone();
|
||||
match event {
|
||||
ToplevelEvent::New(handle) => {
|
||||
let Some(info) = handle.info() else { continue };
|
||||
|
||||
match event.change {
|
||||
ToplevelChange::New => {
|
||||
let new_item = {
|
||||
let mut items = lock!(items);
|
||||
let item = items.get_mut(&app_id);
|
||||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
None => {
|
||||
let item: Item = window.into();
|
||||
items.insert(app_id.clone(), item.clone());
|
||||
let item: Item = handle.try_into()?;
|
||||
|
||||
items.insert(info.app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
}
|
||||
Some(item) => {
|
||||
let window = item.merge_toplevel(window);
|
||||
let window = item.merge_toplevel(handle)?;
|
||||
ItemOrWindow::Window(window)
|
||||
}
|
||||
}
|
||||
@@ -177,20 +182,40 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
send_update(LauncherUpdate::AddItem(item)).await
|
||||
}
|
||||
ItemOrWindow::Window(window) => {
|
||||
send_update(LauncherUpdate::AddWindow(app_id, window)).await
|
||||
send_update(LauncherUpdate::AddWindow(info.app_id.clone(), window))
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
}
|
||||
ToplevelChange::Close => {
|
||||
ToplevelEvent::Update(handle) => {
|
||||
let Some(info) = handle.info() else { continue };
|
||||
|
||||
if let Some(item) = lock!(items).get_mut(&info.app_id) {
|
||||
item.set_window_focused(info.id, info.focused);
|
||||
item.set_window_name(info.id, info.title.clone());
|
||||
}
|
||||
|
||||
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused))
|
||||
.await?;
|
||||
send_update(LauncherUpdate::Title(
|
||||
info.app_id.clone(),
|
||||
info.id,
|
||||
info.title.clone(),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
ToplevelEvent::Remove(handle) => {
|
||||
let Some(info) = handle.info() else { continue };
|
||||
|
||||
let remove_item = {
|
||||
let mut items = lock!(items);
|
||||
let item = items.get_mut(&app_id);
|
||||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
Some(item) => {
|
||||
item.unmerge_toplevel(&window);
|
||||
item.unmerge_toplevel(&handle);
|
||||
|
||||
if item.windows.is_empty() {
|
||||
items.remove(&app_id);
|
||||
items.remove(&info.app_id);
|
||||
Some(ItemOrWindowId::Item)
|
||||
} else {
|
||||
Some(ItemOrWindowId::Window)
|
||||
@@ -202,56 +227,28 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
match remove_item {
|
||||
Some(ItemOrWindowId::Item) => {
|
||||
send_update(LauncherUpdate::RemoveItem(app_id)).await?;
|
||||
send_update(LauncherUpdate::RemoveItem(info.app_id.clone()))
|
||||
.await?;
|
||||
}
|
||||
Some(ItemOrWindowId::Window) => {
|
||||
send_update(LauncherUpdate::RemoveWindow(app_id, window.id))
|
||||
.await?;
|
||||
send_update(LauncherUpdate::RemoveWindow(
|
||||
info.app_id.clone(),
|
||||
info.id,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
ToplevelChange::Focus(focused) => {
|
||||
let mut update_title = false;
|
||||
|
||||
if focused {
|
||||
if let Some(item) = lock!(items).get_mut(&app_id) {
|
||||
item.set_window_focused(window.id, true);
|
||||
|
||||
// might be switching focus between windows of same app
|
||||
if item.windows.len() > 1 {
|
||||
item.set_window_name(window.id, window.title.clone());
|
||||
update_title = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_update(LauncherUpdate::Focus(app_id.clone(), focused)).await?;
|
||||
|
||||
if update_title {
|
||||
send_update(LauncherUpdate::Title(app_id, window.id, window.title))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
ToplevelChange::Title(title) => {
|
||||
if let Some(item) = lock!(items).get_mut(&app_id) {
|
||||
item.set_window_name(window.id, title.clone());
|
||||
}
|
||||
|
||||
send_update(LauncherUpdate::Title(app_id, window.id, title)).await?;
|
||||
}
|
||||
ToplevelChange::Fullscreen(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<LauncherUpdate>>>(())
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
// listen to ui events
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
trace!("{:?}", event);
|
||||
|
||||
if let ItemEvent::OpenItem(app_id) = event {
|
||||
find_desktop_file(&app_id).map_or_else(
|
||||
|| error!("Could not find desktop file for {}", app_id),
|
||||
@@ -275,27 +272,39 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let wl = wayland::get_client().await;
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
|
||||
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!(),
|
||||
};
|
||||
|
||||
if let Some(id) = id {
|
||||
let toplevels = read_lock!(wl.toplevels);
|
||||
let seat = wl.seats.first().expect("Failed to get Wayland seat");
|
||||
if let Some((_top, handle)) = toplevels.get(&id) {
|
||||
handle.activate(seat);
|
||||
};
|
||||
if let Some(window) =
|
||||
items.iter().find_map(|(_, item)| item.windows.get(&id))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -307,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);
|
||||
@@ -318,9 +327,14 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
let controller_tx = context.controller_tx.clone();
|
||||
|
||||
let appearance_options = AppearanceOptions {
|
||||
show_names: self.show_names,
|
||||
show_icons: self.show_icons,
|
||||
icon_size: self.icon_size,
|
||||
};
|
||||
|
||||
let show_names = self.show_names;
|
||||
let show_icons = self.show_icons;
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let bar_position = info.bar_position;
|
||||
|
||||
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||
|
||||
@@ -334,10 +348,9 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
} else {
|
||||
let button = ItemButton::new(
|
||||
&item,
|
||||
show_names,
|
||||
show_icons,
|
||||
orientation,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
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,
|
||||
})
|
||||
@@ -413,10 +434,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
) -> Option<gtk::Box> {
|
||||
const MAX_WIDTH: i32 = 250;
|
||||
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.name("popup-launcher")
|
||||
.build();
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
// we need some content to force the container to have a size
|
||||
let placeholder = Button::with_label("PLACEHOLDER");
|
||||
@@ -444,12 +462,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
{
|
||||
let tx = controller_tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
button.connect_clicked(move |_| {
|
||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||
|
||||
if let Some(win) = button.window() {
|
||||
win.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -473,12 +487,8 @@ 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));
|
||||
|
||||
if let Some(win) = button.window() {
|
||||
win.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -536,6 +546,9 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
/// This is a hacky number derived from
|
||||
/// "what fits inside the 250px popup"
|
||||
/// and probably won't hold up with wide fonts.
|
||||
///
|
||||
/// TODO: Migrate this to truncate system
|
||||
///
|
||||
fn clamp(str: &str) -> String {
|
||||
const MAX_CHARS: usize = 24;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user