Compare commits
100 Commits
v0.12.0
...
flatpak_ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e9bdea83 | ||
|
|
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 | ||
|
|
31a57ae637 |
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"
|
||||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -22,6 +22,9 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
name: Cache dependencies
|
||||||
|
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|||||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -4,6 +4,105 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
## [v0.11.0] - 2023-04-01
|
||||||
### :boom: BREAKING CHANGES
|
### :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))*:
|
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||||
@@ -272,3 +371,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.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.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
|
||||||
@@ -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.
|
- Fix any `cargo clippy` warnings, using at least the default configuration.
|
||||||
- Make sure your code is formatted using `cargo fmt`.
|
- Make sure your code is formatted using `cargo fmt`.
|
||||||
- Keep any documentation up to date.
|
- 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:
|
- For PRs:
|
||||||
|
|||||||
982
Cargo.lock
generated
982
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
@@ -1,12 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ironbar"
|
name = "ironbar"
|
||||||
version = "0.12.0"
|
version = "0.13.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||||
|
repository = "https://github.com/jakestanger/ironbar"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
|
"cli",
|
||||||
|
"ipc",
|
||||||
"http",
|
"http",
|
||||||
"config+all",
|
"config+all",
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@@ -17,10 +20,19 @@ default = [
|
|||||||
"upower",
|
"upower",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
http = ["dep:reqwest"]
|
|
||||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
|
||||||
|
|
||||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
|
cli = ["dep:clap", "ipc"]
|
||||||
|
ipc = ["dep:serde_json"]
|
||||||
|
|
||||||
|
http = ["dep:reqwest"]
|
||||||
|
|
||||||
|
"config+all" = [
|
||||||
|
"config+json",
|
||||||
|
"config+yaml",
|
||||||
|
"config+toml",
|
||||||
|
"config+corn",
|
||||||
|
"config+ron",
|
||||||
|
]
|
||||||
"config+json" = ["universal-config/json"]
|
"config+json" = ["universal-config/json"]
|
||||||
"config+yaml" = ["universal-config/yaml"]
|
"config+yaml" = ["universal-config/yaml"]
|
||||||
"config+toml" = ["universal-config/toml"]
|
"config+toml" = ["universal-config/toml"]
|
||||||
@@ -40,6 +52,8 @@ sys_info = ["sysinfo", "regex"]
|
|||||||
|
|
||||||
tray = ["stray"]
|
tray = ["stray"]
|
||||||
|
|
||||||
|
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||||
|
|
||||||
workspaces = ["futures-util"]
|
workspaces = ["futures-util"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||||
@@ -49,44 +63,61 @@ workspaces = ["futures-util"]
|
|||||||
# core
|
# core
|
||||||
gtk = "0.17.0"
|
gtk = "0.17.0"
|
||||||
gtk-layer-shell = "0.6.0"
|
gtk-layer-shell = "0.6.0"
|
||||||
glib = "0.17.5"
|
glib = "0.17.10"
|
||||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
tokio = { version = "1.28.2", features = [
|
||||||
|
"macros",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"time",
|
||||||
|
"process",
|
||||||
|
"sync",
|
||||||
|
"io-util",
|
||||||
|
"net",
|
||||||
|
] }
|
||||||
tracing = "0.1.37"
|
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-error = "0.2.0"
|
||||||
tracing-appender = "0.2.2"
|
tracing-appender = "0.2.2"
|
||||||
strip-ansi-escapes = "0.1.1"
|
strip-ansi-escapes = "0.1.1"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
serde = { version = "1.0.141", features = ["derive"] }
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
indexmap = "1.9.1"
|
indexmap = "2.0.0"
|
||||||
dirs = "5.0.0"
|
dirs = "5.0.1"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
notify = { version = "5.0.0", default-features = false }
|
notify = { version = "6.0.1", default-features = false }
|
||||||
wayland-client = "0.30.0"
|
wayland-client = "0.30.2"
|
||||||
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
|
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
|
||||||
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
||||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] }
|
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
|
||||||
|
"calloop",
|
||||||
|
] }
|
||||||
universal-config = { version = "0.4.0", default_features = false }
|
universal-config = { version = "0.4.0", default_features = false }
|
||||||
|
ctrlc = "3.4.0"
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
|
# cli
|
||||||
|
clap = { version = "4.2.7", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
|
# ipc
|
||||||
|
serde_json = { version = "1.0.96", optional = true }
|
||||||
|
|
||||||
# http
|
# http
|
||||||
reqwest = { version = "0.11.14", optional = true }
|
reqwest = { version = "0.11.18", optional = true }
|
||||||
|
|
||||||
# clipboard
|
# clipboard
|
||||||
nix = { version = "0.26.2", optional = true, features = ["event"] }
|
nix = { version = "0.26.2", optional = true, features = ["event"] }
|
||||||
|
|
||||||
# clock
|
# clock
|
||||||
chrono = { version = "0.4.19", optional = true }
|
chrono = { version = "0.4.26", optional = true }
|
||||||
|
|
||||||
# music
|
# music
|
||||||
mpd_client = { version = "1.0.0", optional = true }
|
mpd_client = { version = "1.0.0", optional = true }
|
||||||
mpris = { version = "2.0.0", optional = true }
|
mpris = { version = "2.0.1", optional = true }
|
||||||
|
|
||||||
# sys_info
|
# sys_info
|
||||||
sysinfo = { version = "0.28.4", optional = true }
|
sysinfo = { version = "0.29.2", optional = true }
|
||||||
|
|
||||||
# tray
|
# tray
|
||||||
stray = { version = "0.1.3", optional = true }
|
stray = { version = "0.1.3", optional = true }
|
||||||
@@ -94,12 +125,17 @@ stray = { version = "0.1.3", optional = true }
|
|||||||
# upower
|
# upower
|
||||||
upower_dbus = { version = "0.3.2", optional = true }
|
upower_dbus = { version = "0.3.2", optional = true }
|
||||||
futures-lite = { version = "1.12.0", optional = true }
|
futures-lite = { version = "1.12.0", optional = true }
|
||||||
zbus = { version = "3.11.0", optional = true }
|
zbus = { version = "3.13.1", optional = true }
|
||||||
|
|
||||||
# workspaces
|
# workspaces
|
||||||
swayipc-async = { version = "2.0.1", optional = true }
|
swayipc-async = { version = "2.0.1", optional = true }
|
||||||
hyprland = { version = "0.3.1", optional = true }
|
hyprland = { version = "=0.3.1", optional = true }
|
||||||
futures-util = { version = "0.3.21", optional = true }
|
futures-util = { version = "0.3.21", optional = true }
|
||||||
|
|
||||||
# shared
|
# shared
|
||||||
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
|
regex = { version = "1.8.4", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
], optional = true } # music, sys_info
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
stray = { git = "https://github.com/jakestanger/stray", branch = "fix/connection-errors" }
|
||||||
|
|||||||
127
README.md
127
README.md
@@ -1,49 +1,86 @@
|
|||||||
# Ironbar
|
<h1 align="center" >--- Ironbar ---</h1>
|
||||||
|
|
||||||
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
|
<div align="center">
|
||||||
It uses GTK3 and gtk-layer-shell.
|
<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
|
## Features
|
||||||
|
|
||||||
- First-class support for Sway and Hyprland, but should (mostly) work on any wlroots compositor.
|
- First-class support for Sway and Hyprland
|
||||||
- Fully themeable with CSS and hot-loaded styles.
|
- Fully themeable with hot-loaded CSS
|
||||||
- Support for multiple configuration languages.
|
- Popups to show rich content
|
||||||
- Popups used by widgets to show rich content and controls on click.
|
- Ability to create custom widgets, run scripts and embed dynamic content
|
||||||
- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
|
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
||||||
- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
|
- Support for multiple config languages
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
|
|
||||||
|
[crate](https://crates.io/crates/ironbar)
|
||||||
|
|
||||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install ironbar
|
cargo install ironbar
|
||||||
```
|
```
|
||||||
|
|
||||||
[crate](https://crates.io/crates/ironbar)
|
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
|
|
||||||
|
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yay -S ironbar-git
|
yay -S ironbar-git
|
||||||
```
|
```
|
||||||
|
|
||||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
|
||||||
|
|
||||||
### Nix Flake
|
### Nix Flake
|
||||||
|
|
||||||
A flake is included with the repo which can be used with home-manager.
|
A flake is included with the repo which can be used with Home Manager.
|
||||||
|
|
||||||
#### Example
|
<details>
|
||||||
|
<summary>Example usage</summary>
|
||||||
Here is an example nix flake that uses Ironbar.
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
@@ -80,13 +117,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`
|
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
|
||||||
in case you don't want to compile Ironbar.
|
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
|
[repo](https://github.com/jakestanger/ironbar)
|
||||||
|
|
||||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -100,53 +138,36 @@ install target/release/ironbar ~/.local/bin/ironbar
|
|||||||
By default, all features are enabled.
|
By default, all features are enabled.
|
||||||
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
||||||
|
|
||||||
[repo](https://github.com/jakestanger/ironbar)
|
|
||||||
|
|
||||||
## Running
|
## 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`.
|
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||||
|
|
||||||
File output can be found at `~/.local/share/ironbar/error.log`.
|
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
|
Ironbar is an **alpha** project.
|
||||||
and options for scaling complexity: you can use a single config across all monitors,
|
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
|
||||||
or configure different/multiple bars per monitor.
|
|
||||||
|
|
||||||
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
|
## 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
|
## Acknowledgements
|
||||||
|
|
||||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
- [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
|
- [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
|
- [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
|
||||||
@@ -58,6 +58,8 @@ cargo build --release --no-default-features \
|
|||||||
|---------------------|-----------------------------------------------------------------------------------|
|
|---------------------|-----------------------------------------------------------------------------------|
|
||||||
| **Core** | |
|
| **Core** | |
|
||||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
| 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+all | Enables support for all configuration languages. |
|
||||||
| config+json | Enables configuration support for JSON. |
|
| config+json | Enables configuration support for JSON. |
|
||||||
| config+yaml | Enables configuration support for YAML. |
|
| config+yaml | Enables configuration support for YAML. |
|
||||||
|
|||||||
@@ -267,20 +267,21 @@ 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:
|
The following table lists each of the top-level bar config options:
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
|
||||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
| `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. |
|
| `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. |
|
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
| `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.top` | `integer` | `0` | The margin on the top of the bar |
|
||||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom 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.left` | `integer` | `0` | The margin on the left of the bar |
|
||||||
| `margin.right` | `integer` | `0` | The margin on the right 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. |
|
| `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. |
|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||||
|
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||||
|
|
||||||
### 3.2 Module-level options
|
### 3.2 Module-level options
|
||||||
|
|
||||||
@@ -306,9 +307,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her
|
|||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||||
| `transition_duration` | `Integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||||
|
|
||||||
#### Appearance
|
#### Appearance
|
||||||
|
|
||||||
|
|||||||
134
docs/Controlling Ironbar.md
Normal file
134
docs/Controlling Ironbar.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
- [Compiling from source](compiling)
|
- [Compiling from source](compiling)
|
||||||
- [Configuration guide](configuration-guide)
|
- [Configuration guide](configuration-guide)
|
||||||
- [Scripts](scripts)
|
|
||||||
- [Images](images)
|
- [Images](images)
|
||||||
- [Styling guide](styling-guide)
|
- [Styling guide](styling-guide)
|
||||||
|
|
||||||
|
# Dynamic content
|
||||||
|
|
||||||
|
- [Controlling Ironbar](controlling-ironbar)
|
||||||
|
- [Dynamic values](dynamic-values)
|
||||||
|
- [Scripts](scripts)
|
||||||
|
- [Ironvars](ironvars)
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
- [Config](config)
|
- [Config](config)
|
||||||
|
|||||||
@@ -9,17 +9,15 @@ Supports plain text and images.
|
|||||||
|
|
||||||
> Type: `clipboard`
|
> Type: `clipboard`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `icon` | `string/image` | `` | Icon to show on the widget button. |
|
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `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. |
|
| `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` | `'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.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.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. |
|
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
See [here](images) for information on images.
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -86,6 +84,9 @@ end:
|
|||||||
|--------------------------------------|------------------------------------------------------|
|
|--------------------------------------|------------------------------------------------------|
|
||||||
| `.clipboard` | Clipboard widget. |
|
| `.clipboard` | Clipboard widget. |
|
||||||
| `.clipboard .btn` | Clipboard widget button. |
|
| `.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` | Clipboard popup box. |
|
||||||
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||||
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
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.
|
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
|
## Configuration
|
||||||
@@ -18,11 +21,11 @@ You can think of these like HTML elements and their attributes.
|
|||||||
Every widget has the following options available; `type` is mandatory.
|
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.
|
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 |
|
| Name | Type | Default | Description |
|
||||||
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
|
||||||
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
|
||||||
| `name` | `string` | `null` | Widget name. |
|
| `name` | `string` | `null` | Widget name. |
|
||||||
| `class` | `string` | `null` | Widget class name. |
|
| `class` | `string` | `null` | Widget class name. |
|
||||||
|
|
||||||
#### Box
|
#### Box
|
||||||
|
|
||||||
@@ -30,20 +33,20 @@ A container to place nested widgets inside.
|
|||||||
|
|
||||||
> Type: `box`
|
> Type: `box`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
|
||||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||||
|
|
||||||
#### Label
|
#### Label
|
||||||
|
|
||||||
A text label. Pango markup and embedded scripts are supported.
|
A text label. Pango markup is supported.
|
||||||
|
|
||||||
> Type `label`
|
> Type `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|----------|--------------|---------------------------------------------------------------------|
|
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
|
|
||||||
#### Button
|
#### Button
|
||||||
|
|
||||||
@@ -51,10 +54,10 @@ A clickable button, which can run a command when clicked.
|
|||||||
|
|
||||||
> Type `button`
|
> Type `button`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|------------|--------------------|--------------|---------------------------------------------------------------------|
|
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||||
|
|
||||||
#### Image
|
#### Image
|
||||||
|
|
||||||
@@ -62,10 +65,10 @@ An image or icon from disk or http.
|
|||||||
|
|
||||||
> Type `image`
|
> Type `image`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|--------|-----------|---------|---------------------------------------------------------------------------------------------|
|
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
|
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||||
|
|
||||||
#### Slider
|
#### Slider
|
||||||
|
|
||||||
@@ -76,18 +79,16 @@ A draggable slider.
|
|||||||
Note that `on_change` will provide the **floating point** value as an argument.
|
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.
|
If your input program requires an integer, you will need to round it.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
||||||
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
| `min` | `float` | `0` | Minimum slider value. |
|
||||||
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
| `max` | `float` | `100` | Maximum slider value. |
|
||||||
| `min` | `float` | `0` | Minimum 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. |
|
||||||
| `max` | `float` | `100` | Maximum slider value. |
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
||||||
| `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,
|
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.
|
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
||||||
@@ -115,14 +116,12 @@ A progress bar.
|
|||||||
|
|
||||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||||
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
| `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,
|
The example below shows progress for the current playing song in MPD,
|
||||||
and displays the elapsed/length timestamps as a label above:
|
and displays the elapsed/length timestamps as a label above:
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ Displays the title and/or icon of the currently focused window.
|
|||||||
|
|
||||||
> Type: `focused`
|
> Type: `focused`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
||||||
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
||||||
| `icon_size` | `integer` | `32` | Size of icon in pixels |
|
| `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` | `'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.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.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. |
|
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
|
Displays custom text, with markup support.
|
||||||
|
|
||||||
|
If you only intend to run a single script, prefer the [script](script) module.
|
||||||
|
For more advanced use-cases, use [custom](custom).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
> Type: `label`
|
> Type: `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|----------|---------|-----------------------------------------|
|
|---------|-------------------------------------------------|---------|------------------------|
|
||||||
| `label` | `string` | `null` | Text, optionally with embedded scripts. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
|||||||
|
|
||||||
> Type: `music`
|
> Type: `music`
|
||||||
|
|
||||||
| | Type | Default | Description |
|
| | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||||
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
| `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` | `'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.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.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. |
|
| `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.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||||
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||||
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||||
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
|
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||||
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
|
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||||
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
|
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||||
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
|
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||||
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist 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. |
|
| `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). |
|
| `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. |
|
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||||
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
| `host` | `string` | `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. |
|
| `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.
|
See [here](images) for information on images.
|
||||||
|
|
||||||
@@ -133,28 +133,40 @@ and will be replaced with values from the currently playing track:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-------------------------------------|------------------------------------------|
|
|---------------------------------------------|-------------------------------------------------------|
|
||||||
| `.music` | Tray widget button |
|
| `.music` | Tray widget button |
|
||||||
| `.music .contents` | Tray widget button contents box |
|
| `.music .contents` | Tray widget button contents box |
|
||||||
| `.popup-music` | Popup box |
|
| `.music .contents .icon` | Tray widget button icon (any type) |
|
||||||
| `.popup-music .album-art` | Album art image inside popup box |
|
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
|
||||||
| `.popup-music .title` | Track title container inside popup box |
|
| `.music .contents .image` | Tray widget button icon (image only) |
|
||||||
| `.popup-music .title .icon` | Track title icon label inside popup box |
|
| `.popup-music` | Popup box |
|
||||||
| `.popup-music .title .label` | Track title label inside popup box |
|
| `.popup-music .album-art` | Album art image inside popup box |
|
||||||
| `.popup-music .album` | Track album container inside popup box |
|
| `.popup-music .title` | Track title container inside popup box |
|
||||||
| `.popup-music .album .icon` | Track album icon label inside popup box |
|
| `.popup-music .title .icon-box` | Track title icon container inside popup box |
|
||||||
| `.popup-music .album .label` | Track album label inside popup box |
|
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
|
||||||
| `.popup-music .artist` | Track artist container inside popup box |
|
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
|
||||||
| `.popup-music .artist .icon` | Track artist icon label inside popup box |
|
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
|
||||||
| `.popup-music .artist .label` | Track artist label inside popup box |
|
| `.popup-music .title .label` | Track title label inside popup box |
|
||||||
| `.popup-music .controls` | Controls container inside popup box |
|
| `.popup-music .album` | Track album container inside popup box |
|
||||||
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
|
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
|
||||||
| `.popup-music .controls .btn-play` | Play button inside popup box |
|
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
|
||||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
|
||||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
|
||||||
| `.popup-music .volume` | Volume container inside popup box |
|
| `.popup-music .album .label` | Track album label inside popup box |
|
||||||
| `.popup-music .volume .slider` | Volume slider popup box |
|
| `.popup-music .artist` | Track artist container inside popup box |
|
||||||
| `.popup-music .volume .icon` | Volume icon label 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` | Volume slider popup box |
|
||||||
|
| `.popup-music .volume .icon` | Volume icon label inside popup box |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
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.
|
Executes a script and shows the result of `stdout` on a label.
|
||||||
Pango markup is supported.
|
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
|
## Configuration
|
||||||
|
|
||||||
> Type: `script`
|
> Type: `script`
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ The following tokens can be used in the `format` configuration option:
|
|||||||
| `{load_average:15}` | 15-minute load average. |
|
| `{load_average:15}` | 15-minute load average. |
|
||||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
| `{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
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ Displays system power information such as the battery percentage, and estimated
|
|||||||
|
|
||||||
> Type: `upower`
|
> Type: `upower`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|----------|----------|-----------------|---------------------------------------------------|
|
|-------------|-----------|-----------------|---------------------------------------------------|
|
||||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||||
|
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -70,13 +71,14 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|---------------------------------|-----------------------------|
|
|---------------------------------|--------------------------------|
|
||||||
| `.upower` | Upower widget container. |
|
| `.upower` | Upower widget container. |
|
||||||
| `.upower .icon` | Upower widget battery icon. |
|
| `.upower .button` | Upower widget button. |
|
||||||
| `.upower .button` | Upower widget button. |
|
| `.upower .button .contents` | Upower widget button contents. |
|
||||||
| `.upower .button .label` | Upower widget button label. |
|
| `.upower .button .icon` | Upower widget battery icon. |
|
||||||
| `.popup-upower` | Upower popup box. |
|
| `.upower .button .label` | Upower widget button label. |
|
||||||
| `.popup-upower .upower-details` | Label inside the popup. |
|
| `.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).
|
For more information on styling, please see the [styling guide](styling-guide).
|
||||||
@@ -8,12 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
|||||||
|
|
||||||
> Type: `workspaces`
|
> Type: `workspaces`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `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. |
|
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
@@ -89,10 +89,13 @@ end:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-----------------------------|--------------------------------------|
|
|--------------------------------|--------------------------------------|
|
||||||
| `.workspaces` | Workspaces widget box |
|
| `.workspaces` | Workspaces widget box |
|
||||||
| `.workspaces .item` | Workspace button |
|
| `.workspaces .item` | Workspace button |
|
||||||
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
||||||
|
| `.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).
|
For more information on styling, please see the [styling guide](styling-guide).
|
||||||
@@ -15,7 +15,7 @@ let {
|
|||||||
|
|
||||||
$launcher = {
|
$launcher = {
|
||||||
type = "launcher"
|
type = "launcher"
|
||||||
favorites = ["firefox" "discord" "Steam"]
|
favorites = ["firefox" "discord" "steam"]
|
||||||
show_names = false
|
show_names = false
|
||||||
show_icons = true
|
show_icons = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"favorites": [
|
"favorites": [
|
||||||
"firefox",
|
"firefox",
|
||||||
"discord",
|
"discord",
|
||||||
"Steam"
|
"steam"
|
||||||
],
|
],
|
||||||
"show_icons": true,
|
"show_icons": true,
|
||||||
"show_names": false,
|
"show_names": false,
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ type = 'launcher'
|
|||||||
favorites = [
|
favorites = [
|
||||||
'firefox',
|
'firefox',
|
||||||
'discord',
|
'discord',
|
||||||
'Steam',
|
'steam',
|
||||||
]
|
]
|
||||||
|
|
||||||
[[start]]
|
[[start]]
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ start:
|
|||||||
- favorites:
|
- favorites:
|
||||||
- firefox
|
- firefox
|
||||||
- discord
|
- discord
|
||||||
- Steam
|
- steam
|
||||||
show_icons: true
|
show_icons: true
|
||||||
show_names: false
|
show_names: false
|
||||||
type: launcher
|
type: launcher
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
box, menubar, button {
|
box, menubar, button {
|
||||||
background-color: @color_bg;
|
background-color: @color_bg;
|
||||||
|
background-image: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, label {
|
button, label {
|
||||||
|
|||||||
12
flake.lock
generated
12
flake.lock
generated
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1682786779,
|
"lastModified": 1686960236,
|
||||||
"narHash": "sha256-m7QFzPS/CE8hbkbIVK4UStihAQMtczr0vSpOgETOM1g=",
|
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b",
|
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1682821181,
|
"lastModified": 1686968542,
|
||||||
"narHash": "sha256-7MYRqO9Ge46sULbQwJbcH/IMDNBdxCGUO9w7bEOc3CI=",
|
"narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "1be440e9119e69b68151cd9c84876ff3063a2e45",
|
"rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ rustPlatform.buildRustPackage rec {
|
|||||||
then false
|
then false
|
||||||
else true;
|
else true;
|
||||||
buildFeatures = features;
|
buildFeatures = features;
|
||||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
|
cargoDeps = rustPlatform.importCargoLock {
|
||||||
|
lockFile = ../Cargo.lock;
|
||||||
|
};
|
||||||
cargoLock.lockFile = ../Cargo.lock;
|
cargoLock.lockFile = ../Cargo.lock;
|
||||||
|
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
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];
|
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 = [
|
propagatedBuildInputs = [
|
||||||
|
|||||||
19
src/bar.rs
19
src/bar.rs
@@ -3,6 +3,7 @@ use crate::modules::{
|
|||||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
||||||
};
|
};
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
|
use crate::unique_id::get_unique_usize;
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
@@ -163,19 +164,23 @@ fn load_modules(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// popup ignores module location so can bodge this for now
|
||||||
|
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
|
||||||
|
let popup = Arc::new(RwLock::new(popup));
|
||||||
|
|
||||||
if let Some(modules) = config.start {
|
if let Some(modules) = config.start {
|
||||||
let info = info!(ModuleLocation::Left);
|
let info = info!(ModuleLocation::Left);
|
||||||
add_modules(left, modules, &info, config.popup_gap)?;
|
add_modules(left, modules, &info, &popup)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(modules) = config.center {
|
if let Some(modules) = config.center {
|
||||||
let info = info!(ModuleLocation::Center);
|
let info = info!(ModuleLocation::Center);
|
||||||
add_modules(center, modules, &info, config.popup_gap)?;
|
add_modules(center, modules, &info, &popup)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(modules) = config.end {
|
if let Some(modules) = config.end {
|
||||||
let info = info!(ModuleLocation::Right);
|
let info = info!(ModuleLocation::Right);
|
||||||
add_modules(right, modules, &info, config.popup_gap)?;
|
add_modules(right, modules, &info, &popup)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -187,11 +192,8 @@ fn add_modules(
|
|||||||
content: >k::Box,
|
content: >k::Box,
|
||||||
modules: Vec<ModuleConfig>,
|
modules: Vec<ModuleConfig>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
popup_gap: i32,
|
popup: &Arc<RwLock<Popup>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let popup = Popup::new(info, popup_gap);
|
|
||||||
let popup = Arc::new(RwLock::new(popup));
|
|
||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
macro_rules! add_module {
|
macro_rules! add_module {
|
||||||
@@ -205,7 +207,8 @@ fn add_modules(
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, config) in modules.into_iter().enumerate() {
|
for config in modules {
|
||||||
|
let id = get_unique_usize();
|
||||||
match config {
|
match config {
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::send;
|
|||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// MPSC async -> sync channel.
|
/// MPSC async -> GTK sync channel.
|
||||||
/// The sender uses `tokio::sync::mpsc`
|
/// The sender uses `tokio::sync::mpsc`
|
||||||
/// while the receiver uses `glib::MainContext::channel`.
|
/// while the receiver uses `glib::MainContext::channel`.
|
||||||
///
|
///
|
||||||
|
|||||||
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,8 @@ impl ClipboardClient {
|
|||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let (mut rx, item) = {
|
let (mut rx, item) = {
|
||||||
let wl = wayland::get_client().await;
|
let wl = wayland::get_client();
|
||||||
|
let wl = lock!(wl);
|
||||||
wl.subscribe_clipboard()
|
wl.subscribe_clipboard()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ impl ClipboardClient {
|
|||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn copy(&self, id: usize) {
|
pub fn copy(&self, id: usize) {
|
||||||
debug!("Copying item with id {id}");
|
debug!("Copying item with id {id}");
|
||||||
|
|
||||||
let item = {
|
let item = {
|
||||||
@@ -120,7 +121,8 @@ impl ClipboardClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = item {
|
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);
|
wl.copy_to_clipboard(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,16 @@ impl MusicClient for Client {
|
|||||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let status = Status {
|
||||||
|
playlist_position: 0,
|
||||||
|
playlist_length: 0,
|
||||||
|
state: PlayerState::Stopped,
|
||||||
|
elapsed: None,
|
||||||
|
duration: None,
|
||||||
|
volume_percent: 0,
|
||||||
|
};
|
||||||
|
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
rx
|
rx
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::unique_id::get_unique_usize;
|
||||||
use crate::{lock, send};
|
use crate::{lock, send};
|
||||||
use async_once::AsyncOnce;
|
use async_once::AsyncOnce;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
@@ -24,11 +25,13 @@ pub struct TrayEventReceiver {
|
|||||||
|
|
||||||
impl TrayEventReceiver {
|
impl TrayEventReceiver {
|
||||||
async fn new() -> stray::error::Result<Self> {
|
async fn new() -> stray::error::Result<Self> {
|
||||||
|
let id = format!("ironbar-{}", get_unique_usize());
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(16);
|
let (tx, rx) = mpsc::channel(16);
|
||||||
let (b_tx, b_rx) = broadcast::channel(16);
|
let (b_tx, b_rx) = broadcast::channel(16);
|
||||||
|
|
||||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
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::new(Mutex::new(BTreeMap::new()));
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ lazy_static! {
|
|||||||
let value = loop {
|
let value = loop {
|
||||||
retries += 1;
|
retries += 1;
|
||||||
|
|
||||||
let tray = TrayEventReceiver::new().await;
|
let tray = Box::pin(TrayEventReceiver::new()).await;
|
||||||
|
|
||||||
match tray {
|
match tray {
|
||||||
Ok(tray) => break Some(tray),
|
Ok(tray) => break Some(tray),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use zbus::fdo::PropertiesProxy;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||||
let dbus = zbus::Connection::system()
|
let dbus = Box::pin(zbus::Connection::system())
|
||||||
.await
|
.await
|
||||||
.expect("failed to create connection to system bus");
|
.expect("failed to create connection to system bus");
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ pub struct WaylandClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WaylandClient {
|
impl WaylandClient {
|
||||||
pub(super) async fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||||
|
|
||||||
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// It is necessary to store macros in a separate file due to a compilation error.
|
/// It is necessary to store macros in a separate file due to a compilation error.
|
||||||
/// I believe this stems from the feature flags.
|
/// I believe this stems from the feature flags.
|
||||||
/// Related issue: https://github.com/rust-lang/rust/issues/81066
|
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
|
||||||
|
|
||||||
// --- Data Control Device --- \\
|
// --- Data Control Device --- \\
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ mod wlr_foreign_toplevel;
|
|||||||
|
|
||||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||||
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||||
use async_once::AsyncOnce;
|
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use smithay_client_toolkit::output::OutputState;
|
use smithay_client_toolkit::output::OutputState;
|
||||||
@@ -18,6 +17,7 @@ use smithay_client_toolkit::{
|
|||||||
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
|
|
||||||
@@ -33,7 +33,6 @@ cfg_if! {
|
|||||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||||
use self::wlr_data_control::source::CopyPasteSource;
|
use self::wlr_data_control::source::CopyPasteSource;
|
||||||
use self::wlr_data_control::SelectionOfferItem;
|
use self::wlr_data_control::SelectionOfferItem;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||||
|
|
||||||
@@ -106,10 +105,9 @@ impl ProvidesRegistryState for Environment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLIENT: AsyncOnce<WaylandClient> =
|
static ref CLIENT: Arc<Mutex<WaylandClient>> = Arc::new(Mutex::new(WaylandClient::new()));
|
||||||
AsyncOnce::new(async { WaylandClient::new().await });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_client() -> &'static WaylandClient {
|
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
||||||
CLIENT.get().await
|
CLIENT.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
|||||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||||
use self::source::DataControlSourceHandler;
|
use self::source::DataControlSourceHandler;
|
||||||
use crate::clients::wayland::Environment;
|
use crate::clients::wayland::Environment;
|
||||||
|
use crate::unique_id::get_unique_usize;
|
||||||
use crate::{lock, send};
|
use crate::{lock, send};
|
||||||
use device::DataControlDevice;
|
use device::DataControlDevice;
|
||||||
use glib::Bytes;
|
use glib::Bytes;
|
||||||
@@ -19,21 +20,14 @@ use std::fmt::{Debug, Formatter};
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{ErrorKind, Read, Write};
|
use std::io::{ErrorKind, Read, Write};
|
||||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
use wayland_client::{Connection, QueueHandle};
|
use wayland_client::{Connection, QueueHandle};
|
||||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
||||||
|
|
||||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
|
||||||
|
|
||||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||||
|
|
||||||
fn get_id() -> usize {
|
|
||||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SelectionOfferItem {
|
pub struct SelectionOfferItem {
|
||||||
offer: SelectionOffer,
|
offer: SelectionOffer,
|
||||||
token: Option<RegistrationToken>,
|
token: Option<RegistrationToken>,
|
||||||
@@ -151,7 +145,7 @@ impl Environment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(ClipboardItem {
|
Ok(ClipboardItem {
|
||||||
id: get_id(),
|
id: get_unique_usize(),
|
||||||
value,
|
value,
|
||||||
mime_type: mime_type.value.clone(),
|
mime_type: mime_type.value.clone(),
|
||||||
})
|
})
|
||||||
@@ -297,14 +291,14 @@ impl DataControlSourceHandler for Environment {
|
|||||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||||
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||||
|
|
||||||
let epoll_fd = epoll_create().unwrap();
|
let epoll_fd = epoll_create().expect("to get valid file descriptor");
|
||||||
epoll_ctl(
|
epoll_ctl(
|
||||||
epoll_fd,
|
epoll_fd,
|
||||||
EpollOp::EpollCtlAdd,
|
EpollOp::EpollCtlAdd,
|
||||||
fd.as_raw_fd(),
|
fd.as_raw_fd(),
|
||||||
&mut epoll_event,
|
&mut epoll_event,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("to send valid epoll operation");
|
||||||
|
|
||||||
while !bytes.is_empty() {
|
while !bytes.is_empty() {
|
||||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::manager::ToplevelManagerState;
|
use super::manager::ToplevelManagerState;
|
||||||
use crate::lock;
|
use crate::lock;
|
||||||
|
use crate::unique_id::get_unique_usize;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use wayland_client::protocol::wl_output::WlOutput;
|
use wayland_client::protocol::wl_output::WlOutput;
|
||||||
@@ -11,12 +11,6 @@ use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_h
|
|||||||
Event, ZwlrForeignToplevelHandleV1,
|
Event, ZwlrForeignToplevelHandleV1,
|
||||||
};
|
};
|
||||||
|
|
||||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
|
||||||
|
|
||||||
fn get_id() -> usize {
|
|
||||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ToplevelHandle {
|
pub struct ToplevelHandle {
|
||||||
pub handle: ZwlrForeignToplevelHandleV1,
|
pub handle: ZwlrForeignToplevelHandleV1,
|
||||||
@@ -74,7 +68,7 @@ pub struct ToplevelInfo {
|
|||||||
impl Default for ToplevelInfo {
|
impl Default for ToplevelInfo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: get_id(),
|
id: get_unique_usize(),
|
||||||
app_id: String::new(),
|
app_id: String::new(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
@@ -157,9 +151,8 @@ where
|
|||||||
lock!(data.inner).current_info = Some(pending_info);
|
lock!(data.inner).current_info = Some(pending_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lock!(data.inner).initial_done {
|
if lock!(data.inner).initial_done {
|
||||||
lock!(data.inner).initial_done = true;
|
state.update_handle(
|
||||||
state.new_handle(
|
|
||||||
conn,
|
conn,
|
||||||
qh,
|
qh,
|
||||||
ToplevelHandle {
|
ToplevelHandle {
|
||||||
@@ -167,7 +160,8 @@ where
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state.update_handle(
|
lock!(data.inner).initial_done = true;
|
||||||
|
state.new_handle(
|
||||||
conn,
|
conn,
|
||||||
qh,
|
qh,
|
||||||
ToplevelHandle {
|
ToplevelHandle {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||||
use crate::script::{Script, ScriptInput};
|
use crate::script::{Script, ScriptInput};
|
||||||
use crate::send;
|
|
||||||
use gtk::gdk::ScrollDirection;
|
use gtk::gdk::ScrollDirection;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::spawn;
|
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
/// Common configuration options
|
/// Common configuration options
|
||||||
@@ -15,7 +13,7 @@ pub struct CommonConfig {
|
|||||||
pub class: Option<String>,
|
pub class: Option<String>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
pub show_if: Option<ScriptInput>,
|
pub show_if: Option<DynamicBool>,
|
||||||
pub transition_type: Option<TransitionType>,
|
pub transition_type: Option<TransitionType>,
|
||||||
pub transition_duration: Option<u32>,
|
pub transition_duration: Option<u32>,
|
||||||
|
|
||||||
@@ -114,7 +112,7 @@ impl CommonConfig {
|
|||||||
|
|
||||||
if let Some(tooltip) = self.tooltip {
|
if let Some(tooltip) = self.tooltip {
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
DynamicString::new(&tooltip, move |string| {
|
dynamic_string(&tooltip, move |string| {
|
||||||
container.set_tooltip_text(Some(&string));
|
container.set_tooltip_text(Some(&string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
@@ -127,23 +125,13 @@ impl CommonConfig {
|
|||||||
container.show_all();
|
container.show_all();
|
||||||
},
|
},
|
||||||
|show_if| {
|
|show_if| {
|
||||||
let script = Script::new_polling(show_if);
|
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
script
|
|
||||||
.run(None, |_, success| {
|
|
||||||
send!(tx, success);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let revealer = revealer.clone();
|
let revealer = revealer.clone();
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
|
|
||||||
rx.attach(None, move |success| {
|
show_if.subscribe(move |success| {
|
||||||
if success {
|
if success {
|
||||||
container.show_all();
|
container.show_all();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ pub struct Config {
|
|||||||
/// GTK icon theme to use.
|
/// GTK icon theme to use.
|
||||||
pub icon_theme: Option<String>,
|
pub icon_theme: Option<String>,
|
||||||
|
|
||||||
|
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
||||||
|
|
||||||
pub start: Option<Vec<ModuleConfig>>,
|
pub start: Option<Vec<ModuleConfig>>,
|
||||||
pub center: Option<Vec<ModuleConfig>>,
|
pub center: Option<Vec<ModuleConfig>>,
|
||||||
pub end: Option<Vec<ModuleConfig>>,
|
pub end: Option<Vec<ModuleConfig>>,
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
use std::collections::HashMap;
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use walkdir::WalkDir;
|
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.
|
/// and exist on the filesystem.
|
||||||
fn find_application_dirs() -> Vec<PathBuf> {
|
fn find_application_dirs() -> Vec<PathBuf> {
|
||||||
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
let mut dirs = vec![
|
||||||
let user_dir = dirs::data_local_dir();
|
PathBuf::from("/usr/share/applications"), // system installed apps
|
||||||
|
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
|
||||||
|
];
|
||||||
|
|
||||||
|
let user_dir = dirs::data_local_dir(); // user installed apps
|
||||||
if let Some(mut user_dir) = user_dir {
|
if let Some(mut user_dir) = user_dir {
|
||||||
user_dir.push("applications");
|
user_dir.push("applications");
|
||||||
dirs.push(user_dir);
|
dirs.push(user_dir);
|
||||||
@@ -19,55 +38,126 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
|||||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to locate a `.desktop` file for an app id
|
/// Finds all the desktop files
|
||||||
/// (or app class).
|
fn find_desktop_files() -> Vec<PathBuf> {
|
||||||
///
|
|
||||||
/// A simple case-insensitive check is performed on filename == `app_id`.
|
|
||||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
|
||||||
let dirs = find_application_dirs();
|
let dirs = find_application_dirs();
|
||||||
|
dirs.into_iter()
|
||||||
for dir in dirs {
|
.flat_map(|dir| {
|
||||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
WalkDir::new(dir)
|
||||||
|
.max_depth(5)
|
||||||
let entry = walker.find(|entry| {
|
.into_iter()
|
||||||
entry.as_ref().map_or(false, |entry| {
|
.filter_map(Result::ok)
|
||||||
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
.map(DirEntry::into_path)
|
||||||
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||||
file_name == test_name
|
})
|
||||||
})
|
.collect()
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(Ok(entry)) = entry {
|
|
||||||
let path = entry.path().to_owned();
|
|
||||||
return Some(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a desktop file into a flat hashmap of keys/values.
|
/// Attempts to locate a `.desktop` file for an app id
|
||||||
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||||
let file = File::open(path)?;
|
// this is necessary to invalidate the cache
|
||||||
let lines = io::BufReader::new(file).lines();
|
let files = find_desktop_files();
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
if let Some(path) = find_desktop_file_by_filename(app_id, &files) {
|
||||||
|
return Some(path);
|
||||||
for line in lines.flatten() {
|
|
||||||
if let Some((key, value)) = line.split_once('=') {
|
|
||||||
map.insert(key.to_string(), value.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(map)
|
find_desktop_file_by_filedata(app_id, &files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the correct desktop file using a simple condition check
|
||||||
|
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||||
|
let app_id = app_id.to_lowercase();
|
||||||
|
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.find(|file| {
|
||||||
|
let file_name: String = file
|
||||||
|
.file_name()
|
||||||
|
.expect("file name doesn't end with ...")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
file_name.contains(&app_id)
|
||||||
|
|| app_id
|
||||||
|
.split(&['-', ' ', ':', '@', '.', '_'][..])
|
||||||
|
.any(|part| file_name.contains(part)) // this will attempt to find flatpak apps that are like this
|
||||||
|
// `com.company.app` or `com.app.something`
|
||||||
|
})
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
.find(|(_, desktop_file)| {
|
||||||
|
desktop_file
|
||||||
|
.values()
|
||||||
|
.flatten()
|
||||||
|
.any(|value| value.to_lowercase().contains(app_id))
|
||||||
|
})
|
||||||
|
.map(|(path, _)| path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||||
|
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||||
|
let Ok(file) = File::open(path) else {
|
||||||
|
warn!("Couldn't Open File: {}", path.display());
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lines = io::BufReader::new(file).lines();
|
||||||
|
|
||||||
|
let mut desktop_file: DesktopFile = DesktopFile::new();
|
||||||
|
|
||||||
|
let _ = lines.flatten().map(|line| {
|
||||||
|
line.split_once('=')
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, value)| {
|
||||||
|
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.
|
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||||
find_desktop_file(app_id).and_then(|file| {
|
let Some(path) = find_desktop_file(app_id) else { return None };
|
||||||
let map = parse_desktop_file(file);
|
|
||||||
map.map_or(None, |map| {
|
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||||
map.get("Icon").map(std::string::ToString::to_string)
|
|
||||||
})
|
let desktop_file = match desktop_files_cache.get(&path) {
|
||||||
})
|
Some(desktop_file) => desktop_file,
|
||||||
|
_ => desktop_files_cache
|
||||||
|
.entry(path.clone())
|
||||||
|
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||||
|
|
||||||
|
icons.next().map(std::string::ToString::to_string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
use crate::script::{OutputStream, Script};
|
|
||||||
use crate::{lock, send};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tokio::spawn;
|
|
||||||
|
|
||||||
/// A segment of a dynamic string,
|
|
||||||
/// containing either a static string
|
|
||||||
/// or a script.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DynamicStringSegment {
|
|
||||||
Static(String),
|
|
||||||
Dynamic(Script),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string with embedded scripts for dynamic content.
|
|
||||||
pub struct DynamicString;
|
|
||||||
|
|
||||||
impl DynamicString {
|
|
||||||
/// Creates a new dynamic string, based off the input template.
|
|
||||||
/// Runs `f` with the compiled string each time one of the scripts updates.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// DynamicString::new(&text, move |string| {
|
|
||||||
/// label.set_markup(&string);
|
|
||||||
/// Continue(true)
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
pub fn new<F>(input: &str, f: F) -> Self
|
|
||||||
where
|
|
||||||
F: FnMut(String) -> Continue + 'static,
|
|
||||||
{
|
|
||||||
let segments = Self::parse_input(input);
|
|
||||||
|
|
||||||
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
for (i, segment) in segments.into_iter().enumerate() {
|
|
||||||
match segment {
|
|
||||||
DynamicStringSegment::Static(str) => {
|
|
||||||
lock!(label_parts).push(str);
|
|
||||||
}
|
|
||||||
DynamicStringSegment::Dynamic(script) => {
|
|
||||||
let tx = tx.clone();
|
|
||||||
let label_parts = label_parts.clone();
|
|
||||||
|
|
||||||
// insert blank value to preserve segment order
|
|
||||||
lock!(label_parts).push(String::new());
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
script
|
|
||||||
.run(None, |out, _| {
|
|
||||||
if let OutputStream::Stdout(out) = out {
|
|
||||||
let mut label_parts = lock!(label_parts);
|
|
||||||
|
|
||||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
|
||||||
|
|
||||||
let string = label_parts.join("");
|
|
||||||
send!(tx, string);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize
|
|
||||||
{
|
|
||||||
let label_parts = lock!(label_parts).join("");
|
|
||||||
send!(tx, label_parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
rx.attach(None, f);
|
|
||||||
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the input string into static and dynamic segments
|
|
||||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
|
||||||
if !input.contains("{{") {
|
|
||||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut segments = vec![];
|
|
||||||
|
|
||||||
let mut chars = input.chars().collect::<Vec<_>>();
|
|
||||||
while !chars.is_empty() {
|
|
||||||
let char_pair = if chars.len() > 1 {
|
|
||||||
Some(&chars[..=1])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (token, skip) = if let Some(['{', '{']) = char_pair {
|
|
||||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
|
||||||
|
|
||||||
let str = chars
|
|
||||||
.windows(2)
|
|
||||||
.skip(2)
|
|
||||||
.take_while(|win| win != &['}', '}'])
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let len = str.len();
|
|
||||||
|
|
||||||
(
|
|
||||||
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
|
||||||
len + SKIP_BRACKETS,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let mut str = chars
|
|
||||||
.windows(2)
|
|
||||||
.take_while(|win| win != &['{', '{'])
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
|
||||||
if chars.len() == str.len() + 1 {
|
|
||||||
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
|
||||||
str.push(remaining_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = str.len();
|
|
||||||
|
|
||||||
(DynamicStringSegment::Static(str), len)
|
|
||||||
};
|
|
||||||
|
|
||||||
// quick runtime check to make sure the parser is working as expected
|
|
||||||
assert_ne!(skip, 0);
|
|
||||||
|
|
||||||
segments.push(token);
|
|
||||||
chars.drain(..skip);
|
|
||||||
}
|
|
||||||
|
|
||||||
segments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test() {
|
|
||||||
// TODO: see if we can run gtk tests in ci
|
|
||||||
if gtk::init().is_ok() {
|
|
||||||
let label = gtk::Label::new(None);
|
|
||||||
DynamicString::new(
|
|
||||||
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
|
|
||||||
move |string| {
|
|
||||||
label.set_label(&string);
|
|
||||||
Continue(true)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
78
src/dynamic_value/dynamic_bool.rs
Normal file
78
src/dynamic_value/dynamic_bool.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::script::Script;
|
||||||
|
use crate::send;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use glib::Continue;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum DynamicBool {
|
||||||
|
/// Either a script or variable, to be determined.
|
||||||
|
Unknown(String),
|
||||||
|
Script(Script),
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
Variable(Box<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicBool {
|
||||||
|
pub fn subscribe<F>(self, f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(bool) -> Continue + 'static,
|
||||||
|
{
|
||||||
|
let value = match self {
|
||||||
|
Self::Unknown(input) => {
|
||||||
|
if input.starts_with('#') {
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ipc")] {
|
||||||
|
Self::Variable(input.into())
|
||||||
|
} else {
|
||||||
|
Self::Unknown(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let script = Script::from(input.as_str());
|
||||||
|
Self::Script(script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
rx.attach(None, f);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
match value {
|
||||||
|
DynamicBool::Script(script) => {
|
||||||
|
script
|
||||||
|
.run(None, |_, success| {
|
||||||
|
send!(tx, success);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
DynamicBool::Variable(variable) => {
|
||||||
|
let variable_manager = get_variable_manager();
|
||||||
|
|
||||||
|
let variable_name = variable[1..].into(); // remove hash
|
||||||
|
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||||
|
|
||||||
|
while let Ok(value) = rx.recv().await {
|
||||||
|
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||||
|
send!(tx, has_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicBool::Unknown(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a string ironvar is 'truthy'
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
fn is_truthy(string: &str) -> bool {
|
||||||
|
!(string.is_empty() || string == "0" || string == "false")
|
||||||
|
}
|
||||||
321
src/dynamic_value/dynamic_string.rs
Normal file
321
src/dynamic_value/dynamic_string.rs
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::script::{OutputStream, Script};
|
||||||
|
use crate::{lock, send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
/// A segment of a dynamic string,
|
||||||
|
/// containing either a static string
|
||||||
|
/// or a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DynamicStringSegment {
|
||||||
|
Static(String),
|
||||||
|
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::new(Mutex::new(Vec::new()));
|
||||||
|
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.len() + 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.len() + 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>();
|
||||||
|
|
||||||
|
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||||
|
if chars.len() == str.len() + 1 {
|
||||||
|
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||||
|
str.push(remaining_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(DynamicStringSegment::Static(str), len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static() {
|
||||||
|
const INPUT: &str = "hello world";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_odd_char_count() {
|
||||||
|
const INPUT: &str = "hello";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script() {
|
||||||
|
const INPUT: &str = "{{echo hello}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable() {
|
||||||
|
const INPUT: &str = "#variable";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script() {
|
||||||
|
const INPUT: &str = "hello {{echo world}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 2);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_variable() {
|
||||||
|
const INPUT: &str = "hello #subject";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 2);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script_static() {
|
||||||
|
const INPUT: &str = "hello {{echo world}} foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_variable_static() {
|
||||||
|
const INPUT: &str = "hello #subject foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script_variable() {
|
||||||
|
const INPUT: &str = "hello {{echo world}} #foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 4);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_hash() {
|
||||||
|
const INPUT: &str = "number ###num";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
|
||||||
|
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script_with_hash() {
|
||||||
|
const INPUT: &str = "{{echo #hello}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/dynamic_value/mod.rs
Normal file
7
src/dynamic_value/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#![doc = include_str!("../../docs/Dynamic values.md")]
|
||||||
|
|
||||||
|
mod dynamic_bool;
|
||||||
|
mod dynamic_string;
|
||||||
|
|
||||||
|
pub use dynamic_bool::DynamicBool;
|
||||||
|
pub use dynamic_string::dynamic_string;
|
||||||
@@ -2,7 +2,6 @@ use super::ImageProvider;
|
|||||||
use crate::gtk_helpers::add_class;
|
use crate::gtk_helpers::add_class;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||||
@@ -11,16 +10,16 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
|||||||
if ImageProvider::is_definitely_image_input(input) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
add_class(&image, "image");
|
add_class(&image, "image");
|
||||||
|
add_class(&image, "icon");
|
||||||
|
|
||||||
match ImageProvider::parse(input, icon_theme, size)
|
match ImageProvider::parse(input, icon_theme, size)
|
||||||
.and_then(|provider| provider.load_into_image(image.clone()))
|
.map(|provider| provider.load_into_image(image.clone()))
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Some(_) => {
|
||||||
button.set_image(Some(&image));
|
button.set_image(Some(&image));
|
||||||
button.set_always_show_image(true);
|
button.set_always_show_image(true);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
None => {
|
||||||
error!("{err:?}");
|
|
||||||
button.set_label(input);
|
button.set_label(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,18 +36,17 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
|
|||||||
|
|
||||||
if ImageProvider::is_definitely_image_input(input) {
|
if ImageProvider::is_definitely_image_input(input) {
|
||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
|
add_class(&image, "icon");
|
||||||
add_class(&image, "image");
|
add_class(&image, "image");
|
||||||
|
|
||||||
container.add(&image);
|
container.add(&image);
|
||||||
|
|
||||||
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
ImageProvider::parse(input, icon_theme, size)
|
||||||
.and_then(|provider| provider.load_into_image(image))
|
.map(|provider| provider.load_into_image(image));
|
||||||
{
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let label = Label::new(Some(input));
|
let label = Label::new(Some(input));
|
||||||
add_class(&label, "label");
|
add_class(&label, "icon");
|
||||||
|
add_class(&label, "text-icon");
|
||||||
|
|
||||||
container.add(&label);
|
container.add(&label);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
use crate::desktop_file::get_desktop_icon_name;
|
use crate::desktop_file::get_desktop_icon_name;
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use color_eyre::{Help, Report, Result};
|
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::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{IconLookupFlags, IconTheme};
|
use gtk::{IconLookupFlags, IconTheme};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
cfg_if!(
|
cfg_if!(
|
||||||
if #[cfg(feature = "http")] {
|
if #[cfg(feature = "http")] {
|
||||||
@@ -38,9 +41,9 @@ impl<'a> ImageProvider<'a> {
|
|||||||
///
|
///
|
||||||
/// Note this checks that icons exist in theme, or files exist on disk
|
/// Note this checks that icons exist in theme, or files exist on disk
|
||||||
/// but no other check is performed.
|
/// but no other check is performed.
|
||||||
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
|
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
|
||||||
let location = Self::get_location(input, theme, size)?;
|
let location = Self::get_location(input, theme, size)?;
|
||||||
Ok(Self { location, size })
|
Some(Self { location, size })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the input starts with a prefix
|
/// Returns true if the input starts with a prefix
|
||||||
@@ -54,44 +57,56 @@ impl<'a> ImageProvider<'a> {
|
|||||||
|| input.starts_with("https://")
|
|| input.starts_with("https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
|
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
|
||||||
let (input_type, input_name) = input
|
let (input_type, input_name) = input
|
||||||
.split_once(':')
|
.split_once(':')
|
||||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||||
|
|
||||||
match input_type {
|
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(),
|
name: input_name.to_string(),
|
||||||
theme,
|
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(),
|
input_name[2..].to_string(),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
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(),
|
input_name.chars().skip("steam_app_".len()).collect(),
|
||||||
)),
|
)),
|
||||||
None if theme
|
None if theme
|
||||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||||
.is_some() =>
|
.is_some() =>
|
||||||
{
|
{
|
||||||
Ok(ImageLocation::Icon {
|
Some(ImageLocation::Icon {
|
||||||
name: input_name.to_string(),
|
name: input_name.to_string(),
|
||||||
theme,
|
theme,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))
|
Some(input_type) => {
|
||||||
.note("You may need to recompile with support if available")),
|
warn!(
|
||||||
None if PathBuf::from(input_name).is_file() => {
|
"{:?}",
|
||||||
Ok(ImageLocation::Local(PathBuf::from(input_name)))
|
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||||
|
.note("You may need to recompile with support if available")
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None if PathBuf::from(input_name).is_file() => {
|
||||||
|
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(location) = get_desktop_icon_name(input_name)
|
||||||
|
.map(|input| Self::get_location(&input, theme, size))
|
||||||
|
{
|
||||||
|
location
|
||||||
|
} else {
|
||||||
|
warn!("Failed to find image: {input}");
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 +130,24 @@ impl<'a> ImageProvider<'a> {
|
|||||||
let size = self.size;
|
let size = self.size;
|
||||||
rx.attach(None, move |bytes| {
|
rx.attach(None, move |bytes| {
|
||||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||||
|
|
||||||
|
let scale = image.scale_factor();
|
||||||
|
let scaled_size = size * scale;
|
||||||
|
|
||||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||||
&stream,
|
&stream,
|
||||||
size,
|
scaled_size,
|
||||||
size,
|
scaled_size,
|
||||||
true,
|
true,
|
||||||
Some(&Cancellable::new()),
|
Some(&Cancellable::new()),
|
||||||
);
|
);
|
||||||
|
|
||||||
match pixbuf {
|
// Different error types makes this a bit awkward
|
||||||
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
|
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
||||||
|
{
|
||||||
|
Ok(Err(err)) => error!("{err:?}"),
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(false)
|
Continue(false)
|
||||||
@@ -141,18 +163,35 @@ impl<'a> ImageProvider<'a> {
|
|||||||
Ok(())
|
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<()> {
|
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||||
|
let scale = image.scale_factor();
|
||||||
|
|
||||||
let pixbuf = match &self.location {
|
let pixbuf = match &self.location {
|
||||||
ImageLocation::Icon { name, theme } => {
|
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
||||||
self.get_from_icon(name, theme, image.scale_factor())
|
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
||||||
}
|
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
||||||
ImageLocation::Local(path) => self.get_from_file(path),
|
|
||||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
|
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
_ => unreachable!(), // handled above
|
_ => 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -161,7 +200,7 @@ impl<'a> ImageProvider<'a> {
|
|||||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||||
let pixbuf =
|
let pixbuf =
|
||||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||||
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
|
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
@@ -172,14 +211,15 @@ impl<'a> ImageProvider<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from a local file.
|
/// Attempts to get a `Pixbuf` from a local file.
|
||||||
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
|
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||||
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
|
let scaled_size = self.size * scale;
|
||||||
|
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||||
Ok(pixbuf)
|
Ok(pixbuf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get a `Pixbuf` from a local file,
|
/// Attempts to get a `Pixbuf` from a local file,
|
||||||
/// using the Steam game ID to look it up.
|
/// 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_{}`?
|
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||||
let path = dirs::data_dir().map_or_else(
|
let path = dirs::data_dir().map_or_else(
|
||||||
|| Err(Report::msg("Missing XDG data dir")),
|
|| Err(Report::msg("Missing XDG data dir")),
|
||||||
@@ -190,7 +230,7 @@ impl<'a> ImageProvider<'a> {
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.get_from_file(&path)
|
self.get_from_file(&path, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/ipc/commands.rs
Normal file
37
src/ipc/commands.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Command {
|
||||||
|
/// Return "ok"
|
||||||
|
Ping,
|
||||||
|
|
||||||
|
/// Open the GTK inspector
|
||||||
|
Inspect,
|
||||||
|
|
||||||
|
/// 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 valid UTF-8 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
33
src/ipc/mod.rs
Normal file
33
src/ipc/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
mod client;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod responses;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub use commands::Command;
|
||||||
|
pub use responses::Response;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ipc {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ipc {
|
||||||
|
/// Creates a new IPC instance.
|
||||||
|
/// This can be used as both a server and client.
|
||||||
|
pub fn new() -> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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")]
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/ipc/server.rs
Normal file
144
src/ipc/server.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use super::Ipc;
|
||||||
|
use crate::bridge_channel::BridgeChannel;
|
||||||
|
use crate::ipc::{Command, Response};
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::style::load_css;
|
||||||
|
use crate::{read_lock, send_async, try_send, write_lock};
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
use glib::Continue;
|
||||||
|
use std::fs;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
impl Ipc {
|
||||||
|
/// Starts the IPC server on its socket.
|
||||||
|
///
|
||||||
|
/// Once started, the server will begin accepting connections.
|
||||||
|
pub fn start(&self) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.recv(move |command| {
|
||||||
|
let res = Self::handle_command(command);
|
||||||
|
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) -> Response {
|
||||||
|
match command {
|
||||||
|
Command::Inspect => {
|
||||||
|
gtk::Window::set_interactive_debugging(true);
|
||||||
|
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::Ping => Response::Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuts down the IPC server,
|
||||||
|
/// removing the socket file in the process.
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
fs::remove_file(&self.path).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/ironvar.rs
Normal file
107
src/ironvar.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#![doc = include_str!("../docs/Ironvars.md")]
|
||||||
|
|
||||||
|
use crate::{arc_rw, send};
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
|
||||||
|
VARIABLE_MANAGER.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global singleton manager for `IronVar` variables.
|
||||||
|
pub struct VariableManager {
|
||||||
|
variables: HashMap<Box<str>, IronVar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VariableManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
variables: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value for a variable,
|
||||||
|
/// creating it if it does not exist.
|
||||||
|
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
|
||||||
|
if Self::key_is_valid(&key) {
|
||||||
|
if let Some(var) = self.variables.get_mut(&key) {
|
||||||
|
var.set(Some(value));
|
||||||
|
} else {
|
||||||
|
let var = IronVar::new(Some(value));
|
||||||
|
self.variables.insert(key, var);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Report::msg("Invalid key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current value of an `ironvar`.
|
||||||
|
/// Prefer to use `subscribe` where possible.
|
||||||
|
pub fn get(&self, key: &str) -> Option<String> {
|
||||||
|
self.variables.get(key).and_then(IronVar::get)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to an `ironvar`, creating it if it does not exist.
|
||||||
|
/// Any time the var is set, its value is sent on the channel.
|
||||||
|
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
||||||
|
self.variables
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| IronVar::new(None))
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_is_valid(key: &str) -> bool {
|
||||||
|
!key.is_empty()
|
||||||
|
&& key
|
||||||
|
.chars()
|
||||||
|
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ironbar dynamic variable representation.
|
||||||
|
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct IronVar {
|
||||||
|
value: Option<String>,
|
||||||
|
tx: broadcast::Sender<Option<String>>,
|
||||||
|
_rx: broadcast::Receiver<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IronVar {
|
||||||
|
/// Creates a new variable.
|
||||||
|
fn new(value: Option<String>) -> Self {
|
||||||
|
let (tx, rx) = broadcast::channel(32);
|
||||||
|
|
||||||
|
Self { value, tx, _rx: rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current variable value.
|
||||||
|
/// Prefer to subscribe to changes where possible.
|
||||||
|
fn get(&self) -> Option<String> {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the current variable value.
|
||||||
|
/// The change is broadcast to all receivers.
|
||||||
|
fn set(&mut self, value: Option<String>) {
|
||||||
|
self.value = value.clone();
|
||||||
|
send!(self.tx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to the variable.
|
||||||
|
/// The latest value is immediately sent to all receivers.
|
||||||
|
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
|
||||||
|
let rx = self.tx.subscribe();
|
||||||
|
send!(self.tx, self.value.clone());
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Sends a message on an asynchronous `Sender` using `send()`
|
/// Sends a message on an asynchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send_async!(tx, "my message");
|
/// send_async!(tx, "my message");
|
||||||
@@ -16,7 +16,7 @@ macro_rules! send_async {
|
|||||||
/// Sends a message on an synchronous `Sender` using `send()`
|
/// Sends a message on an synchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send!(tx, "my message");
|
/// send!(tx, "my message");
|
||||||
@@ -31,7 +31,7 @@ macro_rules! send {
|
|||||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// try_send!(tx, "my message");
|
/// try_send!(tx, "my message");
|
||||||
@@ -46,7 +46,7 @@ macro_rules! try_send {
|
|||||||
/// Locks a `Mutex`.
|
/// Locks a `Mutex`.
|
||||||
/// Panics if the `Mutex` cannot be locked.
|
/// Panics if the `Mutex` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = lock!(my_mutex);
|
/// let mut val = lock!(my_mutex);
|
||||||
@@ -62,7 +62,7 @@ macro_rules! lock {
|
|||||||
/// Gets a read lock on a `RwLock`.
|
/// Gets a read lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let val = read_lock!(my_rwlock);
|
/// let val = read_lock!(my_rwlock);
|
||||||
@@ -77,7 +77,7 @@ macro_rules! read_lock {
|
|||||||
/// Gets a write lock on a `RwLock`.
|
/// Gets a write lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = write_lock!(my_rwlock);
|
/// let mut val = write_lock!(my_rwlock);
|
||||||
@@ -88,3 +88,33 @@ macro_rules! write_lock {
|
|||||||
$rwlock.write().expect($crate::error::ERR_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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
96
src/main.rs
96
src/main.rs
@@ -2,23 +2,33 @@
|
|||||||
|
|
||||||
mod bar;
|
mod bar;
|
||||||
mod bridge_channel;
|
mod bridge_channel;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
mod cli;
|
||||||
mod clients;
|
mod clients;
|
||||||
mod config;
|
mod config;
|
||||||
mod desktop_file;
|
mod desktop_file;
|
||||||
mod dynamic_string;
|
mod dynamic_value;
|
||||||
mod error;
|
mod error;
|
||||||
mod gtk_helpers;
|
mod gtk_helpers;
|
||||||
mod image;
|
mod image;
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
mod ipc;
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
mod ironvar;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod modules;
|
mod modules;
|
||||||
mod popup;
|
mod popup;
|
||||||
mod script;
|
mod script;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod unique_id;
|
||||||
|
|
||||||
use crate::bar::create_bar;
|
use crate::bar::create_bar;
|
||||||
use crate::config::{Config, MonitorConfig};
|
use crate::config::{Config, MonitorConfig};
|
||||||
use crate::style::load_css;
|
use crate::style::load_css;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
use clap::Parser;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
@@ -31,11 +41,12 @@ use std::future::Future;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::mpsc;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::{block_in_place, spawn_blocking};
|
||||||
|
|
||||||
use crate::error::ExitCode;
|
use crate::error::ExitCode;
|
||||||
use clients::wayland::{self, WaylandClient};
|
use clients::wayland;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
use universal_config::ConfigLoader;
|
use universal_config::ConfigLoader;
|
||||||
|
|
||||||
@@ -46,12 +57,37 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
let _guard = logging::install_logging();
|
let _guard = logging::install_logging();
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "cli")] {
|
||||||
|
run_with_args().await;
|
||||||
|
} else {
|
||||||
|
start_ironbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
async fn run_with_args() {
|
||||||
|
let args = cli::Args::parse();
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Some(command) => {
|
||||||
|
let ipc = ipc::Ipc::new();
|
||||||
|
match ipc.send(command).await {
|
||||||
|
Ok(res) => cli::handle_response(res),
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => start_ironbar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_ironbar() {
|
||||||
info!("Ironbar version {}", VERSION);
|
info!("Ironbar version {}", VERSION);
|
||||||
info!("Starting application");
|
info!("Starting application");
|
||||||
|
|
||||||
let wayland_client = wayland::get_client().await;
|
|
||||||
|
|
||||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||||
|
let _ = wayland::get_client(); // force-init
|
||||||
|
|
||||||
let running = Rc::new(Cell::new(false));
|
let running = Rc::new(Cell::new(false));
|
||||||
|
|
||||||
@@ -63,6 +99,13 @@ async fn main() {
|
|||||||
|
|
||||||
running.set(true);
|
running.set(true);
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ipc")] {
|
||||||
|
let ipc = ipc::Ipc::new();
|
||||||
|
ipc.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let display = Display::default().map_or_else(
|
let display = Display::default().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
let report = Report::msg("Failed to get default GTK display");
|
let report = Report::msg("Failed to get default GTK display");
|
||||||
@@ -77,7 +120,7 @@ async fn main() {
|
|||||||
ConfigLoader::load,
|
ConfigLoader::load,
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = match config_res {
|
let mut config: Config = match config_res {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
@@ -87,7 +130,17 @@ async fn main() {
|
|||||||
|
|
||||||
debug!("Loaded config file");
|
debug!("Loaded config file");
|
||||||
|
|
||||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
#[cfg(feature = "ipc")]
|
||||||
|
if let Some(ironvars) = config.ironvar_defaults.take() {
|
||||||
|
let variable_manager = ironvar::get_variable_manager();
|
||||||
|
for (k, v) in ironvars {
|
||||||
|
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
|
||||||
|
tracing::warn!("Ignoring invalid ironvar: '{k}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = create_bars(app, &display, &config) {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(ExitCode::CreateBars as i32);
|
exit(ExitCode::CreateBars as i32);
|
||||||
}
|
}
|
||||||
@@ -111,24 +164,33 @@ async fn main() {
|
|||||||
if style_path.exists() {
|
if style_path.exists() {
|
||||||
load_css(style_path);
|
load_css(style_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
rx.recv().expect("to receive from channel");
|
||||||
|
|
||||||
|
info!("Shutting down");
|
||||||
|
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
ipc.shutdown();
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
|
||||||
|
.expect("Error setting Ctrl-C handler");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ignore CLI args
|
// Ignore CLI args
|
||||||
// Some are provided by swaybar_config but not currently supported
|
// Some are provided by swaybar_config but not currently supported
|
||||||
app.run_with_args(&Vec::<&str>::new());
|
app.run_with_args(&Vec::<&str>::new());
|
||||||
|
|
||||||
info!("Shutting down");
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates each of the bars across each of the (configured) outputs.
|
/// Creates each of the bars across each of the (configured) outputs.
|
||||||
fn create_bars(
|
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
|
||||||
app: &Application,
|
let wl = wayland::get_client();
|
||||||
display: &Display,
|
let outputs = lock!(wl).get_outputs();
|
||||||
wl: &WaylandClient,
|
|
||||||
config: &Config,
|
|
||||||
) -> Result<()> {
|
|
||||||
let outputs = wl.get_outputs();
|
|
||||||
|
|
||||||
debug!("Received {} outputs from Wayland", outputs.len());
|
debug!("Received {} outputs from Wayland", outputs.len());
|
||||||
debug!("Outputs: {:?}", outputs);
|
debug!("Outputs: {:?}", outputs);
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
let client = clipboard::get_client();
|
let client = clipboard::get_client();
|
||||||
match event {
|
match event {
|
||||||
UIEvent::Copy(id) => client.copy(id).await,
|
UIEvent::Copy(id) => client.copy(id),
|
||||||
UIEvent::Remove(id) => client.remove(id),
|
UIEvent::Remove(id) => client.remove(id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
use crate::{build, try_send};
|
use crate::{build, try_send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
@@ -25,7 +25,7 @@ impl CustomWidget for ButtonWidget {
|
|||||||
label.set_use_markup(true);
|
label.set_use_markup(true);
|
||||||
button.add(&label);
|
button.add(&label);
|
||||||
|
|
||||||
DynamicString::new(&text, move |string| {
|
dynamic_string(&text, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use super::{CustomWidget, CustomWidgetContext};
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
use crate::build;
|
use crate::build;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::image::ImageProvider;
|
use crate::image::ImageProvider;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Image;
|
use gtk::Image;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ImageWidget {
|
pub struct ImageWidget {
|
||||||
@@ -30,13 +29,9 @@ impl CustomWidget for ImageWidget {
|
|||||||
let gtk_image = gtk_image.clone();
|
let gtk_image = gtk_image.clone();
|
||||||
let icon_theme = context.icon_theme.clone();
|
let icon_theme = context.icon_theme.clone();
|
||||||
|
|
||||||
DynamicString::new(&self.src, move |src| {
|
dynamic_string(&self.src, move |src| {
|
||||||
let res = ImageProvider::parse(&src, &icon_theme, self.size)
|
ImageProvider::parse(&src, &icon_theme, self.size)
|
||||||
.and_then(|image| image.load_into_image(gtk_image.clone()));
|
.map(|image| image.load_into_image(gtk_image.clone()));
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{CustomWidget, CustomWidgetContext};
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
use crate::build;
|
use crate::build;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -22,7 +22,7 @@ impl CustomWidget for LabelWidget {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
DynamicString::new(&self.label, move |string| {
|
dynamic_string(&self.label, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::modules::custom::set_length;
|
use crate::modules::custom::set_length;
|
||||||
use crate::script::{OutputStream, Script, ScriptInput};
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
use crate::{build, send};
|
use crate::{build, send};
|
||||||
@@ -69,7 +69,7 @@ impl CustomWidget for ProgressWidget {
|
|||||||
let progress = progress.clone();
|
let progress = progress.clone();
|
||||||
progress.set_show_text(true);
|
progress.set_show_text(true);
|
||||||
|
|
||||||
DynamicString::new(&text, move |string| {
|
dynamic_string(&text, move |string| {
|
||||||
progress.set_text(Some(&string));
|
progress.set_text(Some(&string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::config::{CommonConfig, TruncateMode};
|
|||||||
use crate::gtk_helpers::add_class;
|
use crate::gtk_helpers::add_class;
|
||||||
use crate::image::ImageProvider;
|
use crate::image::ImageProvider;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::{send_async, try_send};
|
use crate::{lock, send_async, try_send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
@@ -11,7 +11,7 @@ use gtk::Label;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::{debug, error};
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct FocusedModule {
|
pub struct FocusedModule {
|
||||||
@@ -52,7 +52,8 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let (mut wlrx, handles) = {
|
let (mut wlrx, handles) = {
|
||||||
let wl = wayland::get_client().await;
|
let wl = wayland::get_client();
|
||||||
|
let wl = lock!(wl);
|
||||||
wl.subscribe_toplevels()
|
wl.subscribe_toplevels()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +98,10 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
||||||
|
|
||||||
let icon = gtk::Image::new();
|
let icon = gtk::Image::new();
|
||||||
add_class(&icon, "icon");
|
if self.show_icon {
|
||||||
|
add_class(&icon, "icon");
|
||||||
|
container.add(&icon);
|
||||||
|
}
|
||||||
|
|
||||||
let label = Label::new(None);
|
let label = Label::new(None);
|
||||||
add_class(&label, "label");
|
add_class(&label, "label");
|
||||||
@@ -106,17 +110,17 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.add(&icon);
|
|
||||||
container.add(&label);
|
container.add(&label);
|
||||||
|
|
||||||
{
|
{
|
||||||
let icon_theme = icon_theme.clone();
|
let icon_theme = icon_theme.clone();
|
||||||
context.widget_rx.attach(None, move |(name, id)| {
|
context.widget_rx.attach(None, move |(name, id)| {
|
||||||
if self.show_icon {
|
if self.show_icon {
|
||||||
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
match ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||||
.and_then(|image| image.load_into_image(icon.clone()))
|
.map(|image| image.load_into_image(icon.clone()))
|
||||||
{
|
{
|
||||||
error!("{err:?}");
|
Some(Ok(_)) => icon.show(),
|
||||||
|
_ => icon.hide(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::try_send;
|
use crate::try_send;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
@@ -31,7 +31,7 @@ impl Module<Label> for LabelModule {
|
|||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
DynamicString::new(&self.label, move |string| {
|
dynamic_string(&self.label, move |string| {
|
||||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -192,16 +192,13 @@ impl ItemButton {
|
|||||||
let gtk_image = gtk::Image::new();
|
let gtk_image = gtk::Image::new();
|
||||||
let image =
|
let image =
|
||||||
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
|
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
|
||||||
match image {
|
if let Some(image) = image {
|
||||||
Ok(image) => {
|
button.set_image(Some(>k_image));
|
||||||
button.set_image(Some(>k_image));
|
button.set_always_show_image(true);
|
||||||
button.set_always_show_image(true);
|
|
||||||
|
|
||||||
if let Err(err) = image.load_into_image(gtk_image) {
|
if let Err(err) = image.load_into_image(gtk_image) {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => error!("{err:?}"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
let tx = tx2;
|
let tx = tx2;
|
||||||
|
|
||||||
let (mut wlrx, handles) = {
|
let (mut wlrx, handles) = {
|
||||||
let wl = wayland::get_client().await;
|
let wl = wayland::get_client();
|
||||||
|
let wl = lock!(wl);
|
||||||
wl.subscribe_toplevels()
|
wl.subscribe_toplevels()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,14 +271,18 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
} else {
|
} else {
|
||||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
|
|
||||||
let wl = wayland::get_client().await;
|
let wl = wayland::get_client();
|
||||||
let items = lock!(items);
|
let items = lock!(items);
|
||||||
|
|
||||||
let id = match event {
|
let id = match event {
|
||||||
ItemEvent::FocusItem(app_id) => items
|
ItemEvent::FocusItem(app_id) => items.get(&app_id).and_then(|item| {
|
||||||
.get(&app_id)
|
item.windows
|
||||||
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
|
.iter()
|
||||||
ItemEvent::FocusWindow(id) => Some(id), // FIXME: Broken on wlroots-git
|
.find(|(_, win)| !win.open_state.is_focused())
|
||||||
|
.or_else(|| item.windows.first())
|
||||||
|
.map(|(_, win)| win.id)
|
||||||
|
}),
|
||||||
|
ItemEvent::FocusWindow(id) => Some(id),
|
||||||
ItemEvent::OpenItem(_) => unreachable!(),
|
ItemEvent::OpenItem(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,13 +290,18 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
if let Some(window) =
|
if let Some(window) =
|
||||||
items.iter().find_map(|(_, item)| item.windows.get(&id))
|
items.iter().find_map(|(_, item)| item.windows.get(&id))
|
||||||
{
|
{
|
||||||
let seat = wl.get_seats().pop().expect("Failed to get Wayland seat");
|
debug!("Focusing window {id}: {}", window.name);
|
||||||
|
|
||||||
|
let seat = lock!(wl)
|
||||||
|
.get_seats()
|
||||||
|
.pop()
|
||||||
|
.expect("Failed to get Wayland seat");
|
||||||
window.focus(&seat);
|
window.focus(&seat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// roundtrip to immediately send activate event
|
// roundtrip to immediately send activate event
|
||||||
wl.roundtrip();
|
lock!(wl).roundtrip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -466,12 +476,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let tx = controller_tx.clone();
|
let tx = controller_tx.clone();
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |_button| {
|
||||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||||
|
|
||||||
if let Some(win) = button.window() {
|
|
||||||
win.hide();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,27 +121,27 @@ fn default_icon_pause() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_prev() -> String {
|
fn default_icon_prev() -> String {
|
||||||
String::from("\u{f9ad}")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_next() -> String {
|
fn default_icon_next() -> String {
|
||||||
String::from("\u{f9ac}")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_volume() -> String {
|
fn default_icon_volume() -> String {
|
||||||
String::from("墳")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_track() -> String {
|
fn default_icon_track() -> String {
|
||||||
String::from("\u{f886}")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_album() -> String {
|
fn default_icon_album() -> String {
|
||||||
String::from("\u{f524}")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_icon_artist() -> String {
|
fn default_icon_artist() -> String {
|
||||||
String::from("\u{fd01}")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_music_dir() -> PathBuf {
|
fn default_music_dir() -> PathBuf {
|
||||||
|
|||||||
@@ -342,19 +342,15 @@ impl Module<Button> for MusicModule {
|
|||||||
let new_cover = update.song.cover_path;
|
let new_cover = update.song.cover_path;
|
||||||
if prev_cover != new_cover {
|
if prev_cover != new_cover {
|
||||||
prev_cover = new_cover.clone();
|
prev_cover = new_cover.clone();
|
||||||
let res = match new_cover.map(|cover_path| {
|
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||||
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
||||||
}) {
|
}) {
|
||||||
Some(Ok(image)) => image.load_into_image(album_image.clone()),
|
image.load_into_image(album_image.clone())
|
||||||
Some(Err(err)) => {
|
} else {
|
||||||
album_image.set_from_pixbuf(None);
|
album_image.set_from_pixbuf(None);
|
||||||
Err(err)
|
Ok(())
|
||||||
}
|
|
||||||
None => {
|
|
||||||
album_image.set_from_pixbuf(None);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
@@ -458,7 +454,7 @@ impl IconLabel {
|
|||||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||||
let label = Label::new(label);
|
let label = Label::new(label);
|
||||||
|
|
||||||
add_class(&icon, "icon");
|
add_class(&icon, "icon-box");
|
||||||
add_class(&label, "label");
|
add_class(&label, "label");
|
||||||
|
|
||||||
container.add(&icon);
|
container.add(&icon);
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ pub struct UpowerModule {
|
|||||||
#[serde(default = "default_format")]
|
#[serde(default = "default_format")]
|
||||||
format: String,
|
format: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_icon_size")]
|
||||||
|
icon_size: i32,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub common: Option<CommonConfig>,
|
pub common: Option<CommonConfig>,
|
||||||
}
|
}
|
||||||
@@ -32,6 +35,10 @@ fn default_format() -> String {
|
|||||||
String::from("{percentage}%")
|
String::from("{percentage}%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn default_icon_size() -> i32 {
|
||||||
|
24
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct UpowerProperties {
|
pub struct UpowerProperties {
|
||||||
percentage: f64,
|
percentage: f64,
|
||||||
@@ -41,7 +48,7 @@ pub struct UpowerProperties {
|
|||||||
time_to_empty: i64,
|
time_to_empty: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for UpowerModule {
|
impl Module<gtk::Button> for UpowerModule {
|
||||||
type SendMessage = UpowerProperties;
|
type SendMessage = UpowerProperties;
|
||||||
type ReceiveMessage = ();
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
@@ -143,7 +150,7 @@ impl Module<gtk::Box> for UpowerModule {
|
|||||||
self,
|
self,
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleWidget<gtk::Box>> {
|
) -> Result<ModuleWidget<Button>> {
|
||||||
let icon_theme = info.icon_theme.clone();
|
let icon_theme = info.icon_theme.clone();
|
||||||
let icon = gtk::Image::new();
|
let icon = gtk::Image::new();
|
||||||
add_class(&icon, "icon");
|
add_class(&icon, "icon");
|
||||||
@@ -154,15 +161,15 @@ impl Module<gtk::Box> for UpowerModule {
|
|||||||
.build();
|
.build();
|
||||||
add_class(&label, "label");
|
add_class(&label, "label");
|
||||||
|
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||||
add_class(&container, "upower");
|
add_class(&container, "contents");
|
||||||
|
|
||||||
let button = Button::new();
|
let button = Button::new();
|
||||||
add_class(&button, "button");
|
add_class(&button, "button");
|
||||||
|
|
||||||
button.add(&label);
|
|
||||||
container.add(&button);
|
|
||||||
container.add(&icon);
|
container.add(&icon);
|
||||||
|
container.add(&label);
|
||||||
|
button.add(&container);
|
||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
@@ -180,11 +187,8 @@ impl Module<gtk::Box> for UpowerModule {
|
|||||||
.attach(None, move |properties: UpowerProperties| {
|
.attach(None, move |properties: UpowerProperties| {
|
||||||
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
||||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||||
if let Err(err) = ImageProvider::parse(&icon_name, &icon_theme, 32)
|
ImageProvider::parse(&icon_name, &icon_theme, self.icon_size)
|
||||||
.and_then(|provider| provider.load_into_image(icon.clone()))
|
.map(|provider| provider.load_into_image(icon.clone()));
|
||||||
{
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
label.set_markup(format.as_ref());
|
label.set_markup(format.as_ref());
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
@@ -192,7 +196,7 @@ impl Module<gtk::Box> for UpowerModule {
|
|||||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||||
|
|
||||||
Ok(ModuleWidget {
|
Ok(ModuleWidget {
|
||||||
widget: container,
|
widget: button,
|
||||||
popup,
|
popup,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -212,28 +216,35 @@ impl Module<gtk::Box> for UpowerModule {
|
|||||||
|
|
||||||
let label = Label::new(None);
|
let label = Label::new(None);
|
||||||
add_class(&label, "upower-details");
|
add_class(&label, "upower-details");
|
||||||
|
container.add(&label);
|
||||||
|
|
||||||
rx.attach(None, move |properties| {
|
rx.attach(None, move |properties| {
|
||||||
let mut format = String::new();
|
|
||||||
let state = u32_to_battery_state(properties.state);
|
let state = u32_to_battery_state(properties.state);
|
||||||
match state {
|
let format = match state {
|
||||||
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
|
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
|
||||||
let ttf = properties.time_to_full;
|
let ttf = properties.time_to_full;
|
||||||
if ttf > 0 {
|
if ttf > 0 {
|
||||||
format = format!("Full in {}", seconds_to_string(ttf));
|
format!("Full in {}", seconds_to_string(ttf))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
|
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
|
||||||
let tte = properties.time_to_empty;
|
let tte = properties.time_to_empty;
|
||||||
if tte > 0 {
|
if tte > 0 {
|
||||||
format = format!("Empty in {}", seconds_to_string(tte));
|
format!("Empty in {}", seconds_to_string(tte))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(state) => error!("Invalid battery state: {state}"),
|
Err(state) => {
|
||||||
_ => {}
|
error!("Invalid battery state: {state}");
|
||||||
}
|
String::new()
|
||||||
label.set_markup(&format);
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
label.set_markup(&format);
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ impl Script {
|
|||||||
|
|
||||||
debug!("Running sh with args: {args_list:?}");
|
debug!("Running sh with args: {args_list:?}");
|
||||||
|
|
||||||
let output = Command::new("sh")
|
let output = Command::new("/bin/sh")
|
||||||
.args(&args_list)
|
.args(&args_list)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -265,7 +265,7 @@ impl Script {
|
|||||||
/// Returns a `mpsc::Receiver` that sends a message
|
/// Returns a `mpsc::Receiver` that sends a message
|
||||||
/// every time a new line is written to `stdout` or `stderr`.
|
/// every time a new line is written to `stdout` or `stderr`.
|
||||||
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
|
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
|
||||||
let mut handle = Command::new("sh")
|
let mut handle = Command::new("/bin/sh")
|
||||||
.args(["-c", &self.cmd])
|
.args(["-c", &self.cmd])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::send;
|
use crate::send;
|
||||||
use color_eyre::{Help, Report};
|
use color_eyre::{Help, Report};
|
||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
|
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||||
use gtk::prelude::CssProviderExt;
|
use gtk::prelude::CssProviderExt;
|
||||||
use gtk::{gdk, gio, CssProvider, StyleContext};
|
use gtk::{gdk, gio, CssProvider, StyleContext};
|
||||||
use notify::event::{DataChange, ModifyKind};
|
use notify::event::{DataChange, ModifyKind};
|
||||||
@@ -29,7 +30,11 @@ pub fn load_css(style_path: PathBuf) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||||
StyleContext::add_provider_for_screen(&screen, &provider, 800);
|
StyleContext::add_provider_for_screen(
|
||||||
|
&screen,
|
||||||
|
&provider,
|
||||||
|
GTK_STYLE_PROVIDER_PRIORITY_USER as u32,
|
||||||
|
);
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
|||||||
9
src/unique_id.rs
Normal file
9
src/unique_id.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||||
|
|
||||||
|
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
|
||||||
|
/// This is just an `AtomicUsize` that increments every time this function is called.
|
||||||
|
pub fn get_unique_usize() -> usize {
|
||||||
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user