Compare commits
255 Commits
v0.9.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 | ||
|
|
6836abefd1 | ||
|
|
91e766d6ab | ||
|
|
dea66415c2 | ||
|
|
528a8d6dd6 | ||
|
|
e1abadcf39 | ||
|
|
cf32870f8a | ||
|
|
139bc5d23f | ||
|
|
735f5cc9f1 | ||
|
|
aed04c1ccf | ||
|
|
c1ea5fad7e | ||
|
|
38da59cd41 | ||
|
|
7f46cb4976 | ||
|
|
5c18ec8ba0 | ||
|
|
81acc176ed | ||
|
|
618b7ef552 | ||
|
|
2a155b9aa8 | ||
|
|
31a57ae637 | ||
|
|
bc87c7f0d4 | ||
|
|
bde469816a | ||
|
|
98aaaa0d14 | ||
|
|
51e95d9e01 | ||
|
|
ea9f7caaf7 | ||
|
|
338829e275 | ||
|
|
610c3528af | ||
|
|
f95e1e8f74 | ||
|
|
35dfbbf91d | ||
|
|
14b6c1a69f | ||
|
|
0e3102de8c | ||
|
|
ad3c171eca | ||
|
|
e5bc44168f | ||
|
|
cc62927f15 | ||
|
|
76e2b7ba3e | ||
|
|
033d0f7e6e | ||
|
|
dc16b1e15a | ||
|
|
03cd263095 | ||
|
|
db0868a3fc | ||
|
|
0382b50cf4 | ||
|
|
338f5a0e1b | ||
|
|
20949a7744 | ||
|
|
2da28b9bf5 | ||
|
|
618e97f1e8 | ||
|
|
dd7c9f30db | ||
|
|
1fa0c0e977 | ||
|
|
74d18aedfb | ||
|
|
2c88c99cb6 | ||
|
|
236bb09170 | ||
|
|
83f44fd92f | ||
|
|
1855416db4 | ||
|
|
e63509a3a7 | ||
|
|
4a09b70854 | ||
|
|
9d09855fce | ||
|
|
e9d0273176 | ||
|
|
7926bb07eb | ||
|
|
6fd69d657c | ||
|
|
27d11de661 | ||
|
|
07df51c249 | ||
|
|
b038e7671a | ||
|
|
e5ab9f33b5 | ||
|
|
68bc8230dd | ||
|
|
246313136f | ||
|
|
15a9d8d42c | ||
|
|
a87d8d5c30 | ||
|
|
8e99fd4d0f | ||
|
|
1e1d65ae49 | ||
|
|
2815cef440 | ||
|
|
138b5b3903 | ||
|
|
7355db74ec | ||
|
|
c214f65ecb | ||
|
|
3d308ab572 | ||
|
|
b770ae716c | ||
|
|
3613aef5c5 | ||
|
|
a9d1233909 | ||
|
|
72b14b6c4e | ||
|
|
910945306c | ||
|
|
dfe1964abf | ||
|
|
e928b30f99 | ||
|
|
2ab06f044e | ||
|
|
4b4f1ffc21 | ||
|
|
0691db3b87 | ||
|
|
cac064f479 | ||
|
|
6c622864b3 | ||
|
|
55c06c4766 | ||
|
|
1b0287becc | ||
|
|
7bf44ca75d | ||
|
|
fb04ceab7d | ||
|
|
102d2478a9 | ||
|
|
80a414ab67 | ||
|
|
72ba17add3 | ||
|
|
2b07620847 | ||
|
|
ba488ad38f | ||
|
|
d0b7bdbafc | ||
|
|
0f5ec1fe34 | ||
|
|
6221f7454a | ||
|
|
ecdd71a43d | ||
|
|
01a36a9476 | ||
|
|
d4dd8c41ea | ||
|
|
83c5dceaa7 | ||
|
|
711644e190 | ||
|
|
8cbb73b75e | ||
|
|
7212bbcf61 | ||
|
|
0125ce5916 | ||
|
|
2b26eaf410 | ||
|
|
33676fc4dc | ||
|
|
7978c48d5c | ||
|
|
1d37e010c8 | ||
|
|
54b9b28c75 | ||
|
|
3a44d74cf3 | ||
|
|
b1475a1aff | ||
|
|
b2749fee92 | ||
|
|
9984b638b5 | ||
|
|
207b60db7e | ||
|
|
7779c33e0c | ||
|
|
575d6cc30f | ||
|
|
5bbe64bb86 | ||
|
|
83a49165c4 | ||
|
|
d84139a914 | ||
|
|
ca4fe422f2 | ||
|
|
1ad1961396 | ||
|
|
d253c4bd7f | ||
|
|
fbee6e8bd4 | ||
|
|
7c36f5cb0c | ||
|
|
7dff3e6f8b | ||
|
|
2ac507144b | ||
|
|
82875cde68 | ||
|
|
d40b3b7d80 | ||
|
|
181561fe2a | ||
|
|
7b23e61e7d | ||
|
|
6a39905b43 | ||
|
|
2780d98ee0 | ||
|
|
51d2c2279f | ||
|
|
c347b6c944 | ||
|
|
e83618b1d6 | ||
|
|
90f57d61b9 | ||
|
|
0b9af6bb26 | ||
|
|
11a65d4fbc | ||
|
|
054262365e | ||
|
|
058c8f4228 | ||
|
|
d78d851858 | ||
|
|
db72bc09b4 | ||
|
|
5fb412572f | ||
|
|
400ac00d23 | ||
|
|
80a4b1d177 | ||
|
|
96141d4990 | ||
|
|
b054c17d14 | ||
|
|
3cf9be89fd | ||
|
|
393800aaa2 | ||
|
|
5772711192 | ||
|
|
15f0857859 | ||
|
|
8ba9826cd9 | ||
|
|
07dbf78010 | ||
|
|
97502559b3 | ||
|
|
2b0eb6506a | ||
|
|
012762e102 | ||
|
|
8691824db1 | ||
|
|
ad97550583 | ||
|
|
1ed3220733 |
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"
|
||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -22,13 +22,24 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
name: Cache dependencies
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
- name: Clippy (base features)
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --no-default-features --features config+json
|
||||
|
||||
- name: Clippy (all features)
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -50,14 +61,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: cachix/install-nix-action@v17
|
||||
- uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
install_url: https://nixos.org/nix/install
|
||||
extra_nix_config: |
|
||||
auto-optimise-store = true
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
- uses: cachix/cachix-action@v11
|
||||
- uses: cachix/cachix-action@v12
|
||||
with:
|
||||
name: jakestanger
|
||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||
|
||||
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
@@ -18,7 +18,9 @@ jobs:
|
||||
override: true
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Update CHANGELOG
|
||||
id: changelog
|
||||
|
||||
2
.github/workflows/update-nix-flake-lock.yml
vendored
2
.github/workflows/update-nix-flake-lock.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v16
|
||||
uses: cachix/install-nix-action@v20
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
218
CHANGELOG.md
218
CHANGELOG.md
@@ -4,6 +4,217 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v0.12.1] - 2023-06-18
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
Where both textual and image icons are supported, CSS classes have changed to better reflect their targets. `.icon` has changed to `.icon-box` and `.icon` now targets the underlying element. `.label` has been changed to `.icon.text-icon`. This affects icons on the **music**, **workspaces**, and **clipboard** modules.
|
||||
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`31a57ae`](https://github.com/JakeStanger/ironbar/commit/31a57ae637fa5918f163c8b191916867395912f3) - scripts don't work while running ironbar under a systemd service *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`f82f897`](https://github.com/JakeStanger/ironbar/commit/f82f897982e87906e2c9156d4115013bc8e99763) - **upower**: popup always empty *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9012fee`](https://github.com/JakeStanger/ironbar/commit/9012feee4f9b60b2c22a956de732847892331222) - **image**: still blurry on hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0e65f93`](https://github.com/JakeStanger/ironbar/commit/0e65f93a230cb5ab010b43962fd2e829945c291b) - excess popup windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`87ca399`](https://github.com/JakeStanger/ironbar/commit/87ca399220e5d48eefe2f295d1dba1b9452c4472) - poor error handling for missing images *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`22b630a`](https://github.com/JakeStanger/ironbar/commit/22b630a10b9836531a8b03eb904e6f9fcf839fe6) - broken nerd font icons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`48d6af0`](https://github.com/JakeStanger/ironbar/commit/48d6af0281f460d3ed3745a2ffb2b61848430ecb) - **music**: showing when no mpris player found *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b9740cb`](https://github.com/JakeStanger/ironbar/commit/b9740cba8f2fa9dfa18a57345027283610f6487e) - upower icon too large *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a6b6866`](https://github.com/JakeStanger/ironbar/commit/a6b686624b750863aa1c26ca4f1688dfa8c81a61) - **upower**: icon outside button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a5ecb36`](https://github.com/JakeStanger/ironbar/commit/a5ecb363fdb2eb3ab543ad56c55c186414500469) - popups occasionally getting jumbled with multiple bars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ac34c05`](https://github.com/JakeStanger/ironbar/commit/ac34c05d2ecb07fd871ed03ef6ee545dc2e6743d) - **focused**: empty icon rendered when `show_icon = false` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de3aa5d`](https://github.com/JakeStanger/ironbar/commit/de3aa5d7b10e0bf6d5ff3a39b009ff53a3316a5e) - **focused**: previous icon does not clear if new icon fails to load *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`de98cf3`](https://github.com/JakeStanger/ironbar/commit/de98cf3daee816a0ff72d1f6ba6bc0e15ec53fca) - **tray**: (maybe?) sometimes bus name is taken *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`103a224`](https://github.com/JakeStanger/ironbar/commit/103a224355e8f700904a2b8fbc87cd7be4f64566) - **launcher**: crash when focusing newly opened window in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`d116a51`](https://github.com/JakeStanger/ironbar/commit/d116a510830be59f4ebaba4fe06f9f4489da7ebc) - update CHANGELOG.md for v0.12.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`327e345`](https://github.com/JakeStanger/ironbar/commit/327e345630a5a89a6f7e464d873c16666d929c0f) - **examples**: fix css button styles *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`13d3923`](https://github.com/JakeStanger/ironbar/commit/13d39235ad032623745baecb6911057ec057ff11) - **examples**: fix casing of steam in launcher favourites *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cdeafbd`](https://github.com/JakeStanger/ironbar/commit/cdeafbdc7245d37120e3e8338b6f933a39d4e428) - **sys info**: add typical temperature sensors for intel/amd cpus *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ff315ff`](https://github.com/JakeStanger/ironbar/commit/ff315ff5dbd545d8b72b6aa10087c940cb8a5eee) - **music**: fix incorrect type for `host`/`music_dir` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`bd144e8`](https://github.com/JakeStanger/ironbar/commit/bd144e87a8f6668c877d42697ebbedbe5a374c3d) - **readme**: make prettier *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`242b70e`](https://github.com/JakeStanger/ironbar/commit/242b70ed3988b85455b0dbbcb3243b31f89d2ee1) - **contributing**: enforce conventional commits *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`96d36c4`](https://github.com/JakeStanger/ironbar/commit/96d36c43d43ba2f9e9d9441ae01c0743cc56f627) - add missing icon/image selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.12.0] - 2023-05-06
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
To allow for the `name` property, any widgets that were previously targeted by name should be targeted by class instead. This affects **all modules and all popups**, as well as several widgets inside modules. **This will break a lot of rules in your stylesheet**. To attempt to mitigate the damage, a migration script can be found [here](https://raw.githubusercontent.com/JakeStanger/ironbar/master/scripts/migrate-styles.sh) that should get you most of the way.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`6c62286`](https://github.com/JakeStanger/ironbar/commit/6c622864b388548eaaa595f41993606cc151d585) - new label module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cac064f`](https://github.com/JakeStanger/ironbar/commit/cac064f4795e9f418cc0820f04944f91121c426a) - ability to configure popup gap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dfe1964`](https://github.com/JakeStanger/ironbar/commit/dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d) - **custom**: slider widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`72b14b6`](https://github.com/JakeStanger/ironbar/commit/72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2) - **custom**: progress bar widget. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a9d1233`](https://github.com/JakeStanger/ironbar/commit/a9d12339097cbe0fef1628460ef538319a048223) - **custom**: support dynamic strings on buttons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3d308ab`](https://github.com/JakeStanger/ironbar/commit/3d308ab572a39ada2501ddc1b822e50e1f8a8363) - **custom**: support dynamic string in image source *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`4a09b70`](https://github.com/JakeStanger/ironbar/commit/4a09b70854dad33bf890a3fe766f854d9195e786) - **custom**: support common options in widgets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83f44fd`](https://github.com/JakeStanger/ironbar/commit/83f44fd92fe74b45fcdfc242fb90fc932dd2b00b) - wrap modules in a revealer to support animated show/hide *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`1fa0c0e`](https://github.com/JakeStanger/ironbar/commit/1fa0c0e9774c302727d414f5aef999ab71a7acb8) - **custom**: support mouse wheel on slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2da28b9`](https://github.com/JakeStanger/ironbar/commit/2da28b9bf5790adfc46c58b6f6d5fdd13cc17195) - ability to configure image icon sizes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`033d0f7`](https://github.com/JakeStanger/ironbar/commit/033d0f7e6e450b3f2d62d9a75210d52611cf346d) - **custom**: option to toggle slider label *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`76e2b7b`](https://github.com/JakeStanger/ironbar/commit/76e2b7ba3e788f273039d74635881ddb96264258) - **music**: option to hide status icon on widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ad3c171`](https://github.com/JakeStanger/ironbar/commit/ad3c171ecacaebf10408c2583ed7361ed029075e) - implement upower module *(commit by [@p00f](https://github.com/p00f))*
|
||||
- [`2a155b9`](https://github.com/JakeStanger/ironbar/commit/2a155b9aa8a3634908512d9b83680925962d478f) - **music**: add css selector for button contents *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c1ea5fa`](https://github.com/JakeStanger/ironbar/commit/c1ea5fad7ec308895f0454b6de05a3177563626c) - **logging**: include line numbers *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dea6641`](https://github.com/JakeStanger/ironbar/commit/dea66415c2e11e34ba44d016aaa6cfb4ef7b9f9b) - module-level `name` and `class` options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`9109453`](https://github.com/JakeStanger/ironbar/commit/910945306c3261190a16300da2ed28efb945a6ac) - **dynamic string**: parser issue related to incorrectly matching braces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7355db7`](https://github.com/JakeStanger/ironbar/commit/7355db74ec9118c2cb46899534a3adac8d7165d9) - **image**: http provider not handling non-success codes *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`a87d8d5`](https://github.com/JakeStanger/ironbar/commit/a87d8d5c3071a1d8ab149deae17d261ae97368ea) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`15a9d8d`](https://github.com/JakeStanger/ironbar/commit/15a9d8d42c9319a7062e6a90086e0c1c3323f5d8) - **script**: parser incorrectly handling colons *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`68bc823`](https://github.com/JakeStanger/ironbar/commit/68bc8230ddf3352cc0de9f8cc770632744c22747) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b038e76`](https://github.com/JakeStanger/ironbar/commit/b038e7671af4bfa41060adf724deb8c6151fac1f) - **tray**: icons sometimes not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7926bb0`](https://github.com/JakeStanger/ironbar/commit/7926bb07eb181edaf6da2f11a7dc00f8be2240eb) - **nix**: Fix `nix run` support *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`2c88c99`](https://github.com/JakeStanger/ironbar/commit/2c88c99cb605d312e2d76d620f502c7e7cd8866e) - **dynamic string**: crash when last segment is static and a single char *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`338f5a0`](https://github.com/JakeStanger/ironbar/commit/338f5a0e1b58dc9b52caee61d6a9748cf13153c5) - **nix**: Attempt to fix image blurriness *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`db0868a`](https://github.com/JakeStanger/ironbar/commit/db0868a3fc0734daa61067e377018c692599ebff) - **image**: not scaling icons for hidpi *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`14b6c1a`](https://github.com/JakeStanger/ironbar/commit/14b6c1a69f28836ed9e3b74eeb97a42ea60ffc27) - bars duplicate when starting second instance *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`98aaaa0`](https://github.com/JakeStanger/ironbar/commit/98aaaa0d1407681b3d790c933c4972b8122f8007) - fallback to default icon theme for notifier items *(commit by [@oknozor](https://github.com/oknozor))*
|
||||
- [`735f5cc`](https://github.com/JakeStanger/ironbar/commit/735f5cc9f1518c256785d42f3d21ed5c68b11711) - **launcher**: crash when focusing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e1abadc`](https://github.com/JakeStanger/ironbar/commit/e1abadcf39a2d39078e75179a167e9277ee5e550) - **clipboard**: copying large images filling write pipe *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`2ab06f0`](https://github.com/JakeStanger/ironbar/commit/2ab06f044ec300628d6648852d395889b6752b76) - **custom**: split into enum with separate file per widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3613aef`](https://github.com/JakeStanger/ironbar/commit/3613aef5c5a4051b5a44e33342c0eaaab3d4a690) - **custom**: reduce a lot of repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c214f65`](https://github.com/JakeStanger/ironbar/commit/c214f65ecb86a0da6559025203701661924f65bb) - fix strict clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`27d11de`](https://github.com/JakeStanger/ironbar/commit/27d11de6616c410422d7abd579d09b3abc02f43a) - **config**: split common code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6fd69d6`](https://github.com/JakeStanger/ironbar/commit/6fd69d657c6224bc47c9b3cb5affcf74b63a6aa6) - move module creation code to module module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e63509a`](https://github.com/JakeStanger/ironbar/commit/e63509a3a7673ea41b4c937089a1cf6d2362fed3) - fix a few new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7f46cb4`](https://github.com/JakeStanger/ironbar/commit/7f46cb49767bd722be8d42999a9ba69887efcd40) - **wayland**: update to 0.30.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`38da59c`](https://github.com/JakeStanger/ironbar/commit/38da59cd419fa0023d0ea0b435b11f0f9dea3f15) - fix a few pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1b0287b`](https://github.com/JakeStanger/ironbar/commit/1b0287becc161e5addd8a8fed8bd9e8c437cd242) - update CHANGELOG.md for v0.11.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`e928b30`](https://github.com/JakeStanger/ironbar/commit/e928b30f9927aa7c895c0d9855ee3ef09e559dc7) - **custom**: rewrite widget options to be clearer *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`138b5b3`](https://github.com/JakeStanger/ironbar/commit/138b5b39038a005d17069830a04b88d52730bed5) - **custom**: fix potential error in progress example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`07df51c`](https://github.com/JakeStanger/ironbar/commit/07df51c2497977a31b2f5ef5bc7d051e0bd88564) - include readme in rust docs *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`dd7c9f3`](https://github.com/JakeStanger/ironbar/commit/dd7c9f30db6e4e1ede4d57255122b359636b8f58) - add transition module-level options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`610c352`](https://github.com/JakeStanger/ironbar/commit/610c3528af98b8c6b02af7ce5c07190776522c3a) - add missing link to upower page *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ea9f7ca`](https://github.com/JakeStanger/ironbar/commit/ea9f7caaf7a35eebd603ce2854672d5af2901018) - add missing `upower` feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`618b7ef`](https://github.com/JakeStanger/ironbar/commit/618b7ef5520de6f3796b66e42422a36c5a191ab0) - improve example css *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`139bc5d`](https://github.com/JakeStanger/ironbar/commit/139bc5d23f7f887b7b65d50adc21fa6679ea291e) - **compiling**: improve requirements list *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`cf32870`](https://github.com/JakeStanger/ironbar/commit/cf32870f8a380c305a436593950c3da524a2296f) - **compiling**: add ron feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.11.0] - 2023-04-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This changes the behaviour of `truncate.length`. A new property, `truncate.max_length`, has been introduced that uses the old behaviour.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`d253c4b`](https://github.com/JakeStanger/ironbar/commit/d253c4bd7f306c7b8fef223d1beb7b1f6e77629b) - add configurable margins around bar *(commit by [@ttoino](https://github.com/ttoino))*
|
||||
- [`ca4fe42`](https://github.com/JakeStanger/ironbar/commit/ca4fe422f22866748f2cb6239b31170a974d254b) - **truncate**: ability to set fixed length *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`575d6cc`](https://github.com/JakeStanger/ironbar/commit/575d6cc30f9e28079aed8425566048abd3d9e022) - new clipboard manager module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9984b63`](https://github.com/JakeStanger/ironbar/commit/9984b638b55adea11ba90412346fbb8220f05682) - **nix**: initial nix feature flags impl *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`b1475a1`](https://github.com/JakeStanger/ironbar/commit/b1475a1affd2f101f1f707ab1a0e8e5509a1d99f) - **nix**: use cargo default features *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`102d247`](https://github.com/JakeStanger/ironbar/commit/102d2478a9d0ecc8be12c5ea6019a5a5411cc6ab) - module hover options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`2ac5071`](https://github.com/JakeStanger/ironbar/commit/2ac507144b42a80507f8d2df214889c114c069df) - not setting layer shell namespace *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7dff3e6`](https://github.com/JakeStanger/ironbar/commit/7dff3e6f8b989132ff0c4406caa72f063dd57c9f) - **image**: widgets missing names *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`54b9b28`](https://github.com/JakeStanger/ironbar/commit/54b9b28c75b2fe300e2bad1436d315da1950953e) - make readme more concise *(commit by [@yavko](https://github.com/yavko))*
|
||||
- [`8cbb73b`](https://github.com/JakeStanger/ironbar/commit/8cbb73b75e7aca1aa163406f4583273e6ff4bac2) - **dynamic string**: dynamic sections not respecting ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`d0b7bdb`](https://github.com/JakeStanger/ironbar/commit/d0b7bdbafcc34967dd5b048ea12e6267ba293566) - **nix**: home manager module, and features *(commit by [@yavko](https://github.com/yavko))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`d84139a`](https://github.com/JakeStanger/ironbar/commit/d84139a914f9b35054dc6048715e1ed7e79d7441) - general tidy up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7212bbc`](https://github.com/JakeStanger/ironbar/commit/7212bbcf61e097b35a7ab341e19e9daefd2edf95) - **dynamic string**: use vec instead of indexmap *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ecdd71a`](https://github.com/JakeStanger/ironbar/commit/ecdd71a43d267161f84e3c4a3c22e9454c0f7184) - **config**: use `universal-config` crate. *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6221f74`](https://github.com/JakeStanger/ironbar/commit/6221f7454a2da2ec8a5a7f84e6fd35a8dc1a1548) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`82875cd`](https://github.com/JakeStanger/ironbar/commit/82875cde687628f3ee3436343068825440128599) - update CHANGELOG.md for v0.10.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7c36f5c`](https://github.com/JakeStanger/ironbar/commit/7c36f5cb0cf03191c9b03e2455b63829a64e402e) - fix a couple of issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`83a4916`](https://github.com/JakeStanger/ironbar/commit/83a49165c42fa793ef1224f93cbc147bc69de894) - **compiling**: add info about build deps *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5bbe64b`](https://github.com/JakeStanger/ironbar/commit/5bbe64bb86fb2db0921e284a1560db2f6c1a1920) - **clock**: format table *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`2b26eaf`](https://github.com/JakeStanger/ironbar/commit/2b26eaf41036609be4dfc57689ca8d770dcb6b9b) - **clipboard**: fix incorrect setting description *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0125ce5`](https://github.com/JakeStanger/ironbar/commit/0125ce5916c003d1ea9a141fe5a0f6a54b2778ab) - **examples**: update styles example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.10.0] - 2023-02-01
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
This removes the `icon_theme` option from `launcher` and `focused`. You will need to set this at the top of your config instead.
|
||||
|
||||
- due to [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
(Missed from #96141d4) The `{icon}` token has been removed from the `music` module due to incompatibility with the new image/icon support. The icon now always displays as a separate widget before the label and should be removed from your formatting string.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`8691824`](https://github.com/JakeStanger/ironbar/commit/8691824db1a12c3f3589ff8b5315b8dba5cb8aec) - **music**: ability to truncate button text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`07dbf78`](https://github.com/JakeStanger/ironbar/commit/07dbf780105027b533b0bb34c9ae3e4e96f29f4a) - **focused**: ability to truncate label text *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`393800a`](https://github.com/JakeStanger/ironbar/commit/393800aaa2093b9257c43fde8bdb8399f26ebc74) - **custom**: image widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`b054c17`](https://github.com/JakeStanger/ironbar/commit/b054c17d14628c9188bfa9aed506ea1de3051f9c) - **workspaces**: support for using images in `name_map` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`96141d4`](https://github.com/JakeStanger/ironbar/commit/96141d49907412ea26d23ef30c10ade8b32b89b9) - **music**: support for using images in `name_map`, additional icon options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c347b6c`](https://github.com/JakeStanger/ironbar/commit/c347b6c9449ce4e16e2e133d7dd35544ab9a533c) - add feature flags *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`5772711`](https://github.com/JakeStanger/ironbar/commit/57727111923a419f9b7613103283aa4cf6bd082c) - **music**: remote mpris album art not showing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5fb4125`](https://github.com/JakeStanger/ironbar/commit/5fb412572f3da60ac482a1960d891f70bc29287b) - **tray**: some init issues *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`058c8f4`](https://github.com/JakeStanger/ironbar/commit/058c8f4228f9f7faa66cda9dd1636ea32e9de68b) - **hyprland**: issues with tracking workspaces *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`51d2c22`](https://github.com/JakeStanger/ironbar/commit/51d2c2279f50add992def0d58cfaa9890ea3d041) - **images**: incorrectly resolving non-files *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`012762e`](https://github.com/JakeStanger/ironbar/commit/012762e10203fb2d58160acdae4dc7ca7689b131) - swap out some code for existing macros *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9750255`](https://github.com/JakeStanger/ironbar/commit/97502559b30c51e77c1dd9a7d794a88756294c83) - **music**: split config code into separate file *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`15f0857`](https://github.com/JakeStanger/ironbar/commit/15f0857859d5d4a590b60b6b1a4347b4b84a58a1) - replace icon loading with improved general image loading *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`1ed3220`](https://github.com/JakeStanger/ironbar/commit/1ed3220733c2dcb7c5e5cbf377b3324d3183609e) - update CHANGELOG.md for v0.9.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`90f57d6`](https://github.com/JakeStanger/ironbar/commit/90f57d61b94c50c98a6f55de18c6edf3d18aa3fa) - **music**: remove irrelevant `icon` format token *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6a39905`](https://github.com/JakeStanger/ironbar/commit/6a39905b4333582fbcda81a66a9b91055333d698) - **compiling**: add missing full stop *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`7b23e61`](https://github.com/JakeStanger/ironbar/commit/7b23e61e7dedf2736a30580b6c1aa84e002c462c) - **wiki**: update screenshots and examples *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.9.0] - 2023-01-28
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
`on_click` is now called `on_click_left` for consistency with new options.
|
||||
|
||||
- due to [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
|
||||
The `mpd` module has been renamed to `music`. You will need to update the `type` value in your config and add `player_type` to continue using MPD. You will also need to update your styles.
|
||||
|
||||
|
||||
### :sparkles: New Features
|
||||
- [`1dd5863`](https://github.com/JakeStanger/ironbar/commit/1dd586343143bfd501a44c6556719fac9d582d6b) - better surface some config error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`fa67d07`](https://github.com/JakeStanger/ironbar/commit/fa67d077b136b109edf6dbaa11a33aebf3e044b4) - mouse event config options *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6d8e647`](https://github.com/JakeStanger/ironbar/commit/6d8e647f123e54ba389c5ab2fe908200aa5e4cf6) - mpris support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6e5d0c1`](https://github.com/JakeStanger/ironbar/commit/6e5d0c1e8c0b5d7e330608fc835e1e9733f156de) - **workspaces**: hyprland support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9ba28fe`](https://github.com/JakeStanger/ironbar/commit/9ba28fe7faf84e06febc2ffea089442f8f5b90a2) - **workspaces**: better ordering *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`e1f523c`](https://github.com/JakeStanger/ironbar/commit/e1f523cf2a15b74a5c570dd7440db4c1b476d782) - **music**: popup artist label using wrong name *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`08cfbbc`](https://github.com/JakeStanger/ironbar/commit/08cfbbc2eaf6e74780dd7196efcc15ea6d2e7d12) - **music**: unable to go to prev with mpris *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0cefcbd`](https://github.com/JakeStanger/ironbar/commit/0cefcbd02b0af518352e35060644f281da249d3e) - **music**: wrong widget name on vol slider *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`90cd078`](https://github.com/JakeStanger/ironbar/commit/90cd078973b23b2291cf156e46729842f33c1806) - **mpd**: stops working if connection lost *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :recycle: Refactors
|
||||
- [`2c1b292`](https://github.com/JakeStanger/ironbar/commit/2c1b2924d4a103183d3974ac066623a80277a79a) - move most of the horrible `add_module` macro content into proper functions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`fd2d7e5`](https://github.com/JakeStanger/ironbar/commit/fd2d7e5c7ab8de50c4621b19d07d8b012a451564) - move startup logging code to logging module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`9d5049d`](https://github.com/JakeStanger/ironbar/commit/9d5049dde01cdb76f4772f8ce8f61a8b5bad3a50) - standardise error messages *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`5e21cbc`](https://github.com/JakeStanger/ironbar/commit/5e21cbcca6cc30d725acdea0f6561cfd6acdcc3c) - macros to reduce repeated code *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`ea2c84d`](https://github.com/JakeStanger/ironbar/commit/ea2c84d1bd15798e32496397c4a6aa42fab39d95) - general code tidy-up *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`0d7ab54`](https://github.com/JakeStanger/ironbar/commit/0d7ab541604691455ed39c73e039ac0635307bc8) - remove redundant clone *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :memo: Documentation Changes
|
||||
- [`b97f018`](https://github.com/JakeStanger/ironbar/commit/b97f018e81aa55a871a12aa3e1e4b07b1f8eb50f) - update CHANGELOG.md for v0.8.0 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`c223892`](https://github.com/JakeStanger/ironbar/commit/c223892a57b29ae56431fc585b8cec503f3206c7) - **workspaces**: update for hyprland/new ordering option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.8.0] - 2022-11-30
|
||||
### :boom: BREAKING CHANGES
|
||||
- due to [`df77020`](https://github.com/JakeStanger/ironbar/commit/df77020c5277ae9e379bb4fd67c221be5cb20426) - use snake_case for module tokens for consistency *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||
@@ -156,4 +367,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
|
||||
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
|
||||
[v0.7.0]: https://github.com/JakeStanger/ironbar/compare/v0.6.0...v0.7.0
|
||||
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
|
||||
[v0.8.0]: https://github.com/JakeStanger/ironbar/compare/v0.7.0...v0.8.0
|
||||
[v0.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
|
||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
||||
[v0.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.
|
||||
- Make sure your code is formatted using `cargo fmt`.
|
||||
- Keep any documentation up to date.
|
||||
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
- Please use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
|
||||
This ensures your contributions are automatically included in the changelog.
|
||||
|
||||
|
||||
- For PRs:
|
||||
|
||||
2243
Cargo.lock
generated
2243
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
154
Cargo.toml
154
Cargo.toml
@@ -1,41 +1,141 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.9.0"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||
repository = "https://github.com/jakestanger/ironbar"
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"cli",
|
||||
"ipc",
|
||||
"http",
|
||||
"config+all",
|
||||
"clipboard",
|
||||
"clock",
|
||||
"music+all",
|
||||
"sys_info",
|
||||
"tray",
|
||||
"upower",
|
||||
"workspaces+all"
|
||||
]
|
||||
|
||||
cli = ["dep:clap", "ipc"]
|
||||
ipc = ["dep:serde_json"]
|
||||
|
||||
http = ["dep:reqwest"]
|
||||
|
||||
"config+all" = [
|
||||
"config+json",
|
||||
"config+yaml",
|
||||
"config+toml",
|
||||
"config+corn",
|
||||
"config+ron",
|
||||
]
|
||||
"config+json" = ["universal-config/json"]
|
||||
"config+yaml" = ["universal-config/yaml"]
|
||||
"config+toml" = ["universal-config/toml"]
|
||||
"config+corn" = ["universal-config/corn"]
|
||||
"config+ron" = ["universal-config/ron"]
|
||||
|
||||
clipboard = ["nix"]
|
||||
|
||||
clock = ["chrono"]
|
||||
|
||||
music = ["regex"]
|
||||
"music+all" = ["music", "music+mpris", "music+mpd"]
|
||||
"music+mpris" = ["music", "mpris"]
|
||||
"music+mpd" = ["music", "mpd_client"]
|
||||
|
||||
sys_info = ["sysinfo", "regex"]
|
||||
|
||||
tray = ["stray"]
|
||||
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
workspaces = ["futures-util"]
|
||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||
"workspaces+hyprland" = ["workspaces", "hyprland"]
|
||||
|
||||
[dependencies]
|
||||
gtk = "0.16.0"
|
||||
gtk-layer-shell = "0.5.0"
|
||||
glib = "0.16.2"
|
||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process"] }
|
||||
# core
|
||||
gtk = "0.17.0"
|
||||
gtk-layer-shell = "0.6.0"
|
||||
glib = "0.17.10"
|
||||
tokio = { version = "1.28.2", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
"process",
|
||||
"sync",
|
||||
"io-util",
|
||||
"net",
|
||||
] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-appender = "0.2.2"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
color-eyre = "0.6.2"
|
||||
serde = { version = "1.0.141", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
serde_yaml = "0.9.4"
|
||||
toml = "0.5.9"
|
||||
libcorn = "0.6.1"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
indexmap = "2.0.0"
|
||||
dirs = "5.0.1"
|
||||
walkdir = "2.3.2"
|
||||
notify = { version = "6.0.1", default-features = false }
|
||||
wayland-client = "0.30.2"
|
||||
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
|
||||
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
|
||||
"calloop",
|
||||
] }
|
||||
universal-config = { version = "0.4.0", default_features = false }
|
||||
ctrlc = "3.4.0"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
indexmap = "1.9.1"
|
||||
futures-util = "0.3.21"
|
||||
chrono = "0.4.19"
|
||||
regex = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
stray = { version = "0.1.2" }
|
||||
dirs = "4.0.0"
|
||||
walkdir = "2.3.2"
|
||||
notify = { version = "5.0.0", default-features = false }
|
||||
mpd_client = "1.0.0"
|
||||
mpris = "2.0.0"
|
||||
swayipc-async = { version = "2.0.1" }
|
||||
hyprland = "0.3.0-alpha.0"
|
||||
sysinfo = "0.27.0"
|
||||
wayland-client = "0.29.5"
|
||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
||||
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
|
||||
reqwest = { version = "0.11.18", optional = true }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.26.2", optional = true, features = ["event"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.26", optional = true }
|
||||
|
||||
# music
|
||||
mpd_client = { version = "1.0.0", optional = true }
|
||||
mpris = { version = "2.0.1", optional = true }
|
||||
|
||||
# sys_info
|
||||
sysinfo = { version = "0.29.2", optional = true }
|
||||
|
||||
# tray
|
||||
stray = { version = "0.1.3", optional = true }
|
||||
|
||||
# upower
|
||||
upower_dbus = { version = "0.3.2", optional = true }
|
||||
futures-lite = { version = "1.12.0", optional = true }
|
||||
zbus = { version = "3.13.1", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "=0.3.1", optional = true }
|
||||
futures-util = { version = "0.3.21", optional = true }
|
||||
|
||||
# shared
|
||||
regex = { version = "1.8.4", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true } # music, sys_info
|
||||
|
||||
[patch.crates-io]
|
||||
stray = { git = "https://github.com/jakestanger/stray", branch = "fix/connection-errors" }
|
||||
|
||||
133
README.md
133
README.md
@@ -1,37 +1,86 @@
|
||||
# Ironbar
|
||||
<h1 align="center" >--- Ironbar ---</h1>
|
||||
|
||||
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
|
||||
It uses GTK3 and gtk-layer-shell.
|
||||
<div align="center">
|
||||
<a href="https://github.com/JakeStanger/ironbar/releases">
|
||||
<img src="https://img.shields.io/crates/v/ironbar?label=version&style=for-the-badge" alt="Current version" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/actions/workflows/build.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/jakestanger/ironbar/build.yml?style=for-the-badge" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/issues">
|
||||
<img src="https://img.shields.io/github/issues/jakestanger/ironbar?style=for-the-badge" alt="Open issues" />
|
||||
</a>
|
||||
<a href="https://github.com/JakeStanger/ironbar/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/jakestanger/ironbar?style=for-the-badge" alt="License" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/ironbar">
|
||||
<img src="https://img.shields.io/crates/d/ironbar?label=crates.io%20downloads&style=for-the-badge" alt="Crates.io downloads" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
The bar can be styled to your liking using CSS and hot-loads style changes.
|
||||
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
|
||||
---
|
||||
|
||||

|
||||
<div align="center">
|
||||
A customisable and feature-rich GTK bar for wlroots compositors, written in Rust.
|
||||
|
||||
Ironbar is designed to support anything from a lightweight bar to a full desktop panel with ease.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
[Wiki](https://github.com/JakeStanger/ironbar/wiki)
|
||||
|
|
||||
[Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide)
|
||||
|
|
||||
[Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide)
|
||||
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
✨ Looking for a starting point, or want to show off? Head to [Show and tell](https://github.com/JakeStanger/ironbar/discussions/categories/show-and-tell) ✨
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- First-class support for Sway and Hyprland
|
||||
- Fully themeable with hot-loaded CSS
|
||||
- Popups to show rich content
|
||||
- Ability to create custom widgets, run scripts and embed dynamic content
|
||||
- Easy to configure anything from a single bar across all monitors, to multiple different unique bars per monitor
|
||||
- Support for multiple config languages
|
||||
|
||||
## Installation
|
||||
|
||||
### Cargo
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
cargo install ironbar
|
||||
```
|
||||
|
||||
[crate](https://crates.io/crates/ironbar)
|
||||
|
||||
### Arch Linux
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
|
||||
```sh
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
[aur package](https://aur.archlinux.org/packages/ironbar-git)
|
||||
|
||||
### Nix Flake
|
||||
|
||||
#### Example
|
||||
Here is an example nix flake that uses ironbar, this is just a
|
||||
proof of concept, please adapt it to your config
|
||||
A flake is included with the repo which can be used with Home Manager.
|
||||
|
||||
<details>
|
||||
<summary>Example usage</summary>
|
||||
|
||||
```nix
|
||||
{
|
||||
@@ -58,6 +107,8 @@ proof of concept, please adapt it to your config
|
||||
enable = true;
|
||||
config = {};
|
||||
style = "";
|
||||
package = inputs.ironbar;
|
||||
features = ["feature" "another_feature"];
|
||||
};
|
||||
}
|
||||
];
|
||||
@@ -66,12 +117,16 @@ proof of concept, please adapt it to your config
|
||||
}
|
||||
```
|
||||
|
||||
#### Binary Caching
|
||||
There is also a cachix cache at `https://app.cachix.org/cache/jakestanger`
|
||||
incase you don't want to compile ironbar!
|
||||
</details>
|
||||
|
||||
There is a Cachix cache available at `https://app.cachix.org/cache/jakestanger`.
|
||||
|
||||
### Source
|
||||
|
||||
[repo](https://github.com/jakestanger/ironbar)
|
||||
|
||||
Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jakestanger/ironbar.git
|
||||
cd ironbar
|
||||
@@ -80,53 +135,39 @@ cargo build --release
|
||||
install target/release/ironbar ~/.local/bin/ironbar
|
||||
```
|
||||
|
||||
[repo](https://github.com/jakestanger/ironbar)
|
||||
By default, all features are enabled.
|
||||
See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
|
||||
|
||||
## Running
|
||||
|
||||
All of the above installation methods provide a binary called `ironbar`.
|
||||
Once installed, you will need to create a config and optionally a stylesheet in `.config/ironbar`.
|
||||
See the [Configuration Guide](https://github.com/JakeStanger/ironbar/wiki/configuration-guide) and [Style Guide](https://github.com/JakeStanger/ironbar/wiki/styling-guide) for full details.
|
||||
|
||||
Ironbar can be launched using the `ironbar` binary.
|
||||
|
||||
Log verbosity can be changed using `IRONBAR_LOG` or `IRONBAR_FILE_LOG`. You can use any of `error`, `warn`, `info`, `debug` or `trace`.
|
||||
|
||||
You can set the `IRONBAR_LOG` or `IRONBAR_FILE_LOG` environment variables to
|
||||
`error`, `warn`, `info`, `debug` or `trace` to configure the log output level.
|
||||
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||
|
||||
File output can be found at `~/.local/share/ironbar/error.log`.
|
||||
|
||||
## Configuration
|
||||
## Status
|
||||
|
||||
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||
and options for scaling complexity: you can use a single config across all monitors,
|
||||
or configure different/multiple bars per monitor.
|
||||
Ironbar is an **alpha** project.
|
||||
It is unfinished and subject to constant breaking changes, and will continue that way until the foundation is rock solid.
|
||||
|
||||
A full configuration guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/configuration-guide).
|
||||
If you would like to take the risk and help shape development, any bug reports, feature requests and discussion is welcome.
|
||||
|
||||
## Styling
|
||||
I use Ironbar on my daily driver, so development is active. Features aim to be stable and well documented before being merged.
|
||||
|
||||
To get started, create a stylesheet at `.config/ironbar/style.css`. Changes will be hot-reloaded every time you save the
|
||||
file.
|
||||
|
||||
A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/wiki/styling-guide).
|
||||
|
||||
## Project Status
|
||||
|
||||
This project is in alpha, but should be usable.
|
||||
Everything that is implemented works and should be documented.
|
||||
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
|
||||
|
||||
There is currently room for lots more modules, and lots more configuration options for the existing modules.
|
||||
The current configuration schema is not set in stone and breaking changes could come along at any point;
|
||||
until the project matures I am more interested in ease of use than backwards compatibility.
|
||||
|
||||
A few bugs do exist, and I am sure there are plenty more to be found.
|
||||
|
||||
The project will be *actively developed* as I am using it on my daily driver.
|
||||
Bugs will be fixed, features will be added, code will be refactored.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
|
||||
All are welcome, but I ask a few basic things to help make things easier. Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
||||
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
||||
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
||||
81
docs/Compiling.md
Normal file
81
docs/Compiling.md
Normal file
@@ -0,0 +1,81 @@
|
||||
You can compile Ironbar from source using `cargo`.
|
||||
Just clone the repo and build:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jakestanger/ironbar.git
|
||||
cd ironbar
|
||||
cargo build --release
|
||||
# change path to wherever you want to install
|
||||
install target/release/ironbar ~/.local/bin/ironbar
|
||||
```
|
||||
|
||||
## Build requirements
|
||||
|
||||
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
|
||||
You also need rust; only the latest stable version is supported.
|
||||
|
||||
### Arch
|
||||
|
||||
```shell
|
||||
pacman -S gtk3 gtk-layer-shell
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
|
||||
```shell
|
||||
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
|
||||
# for http support
|
||||
apt install libssl-dev
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```shell
|
||||
dnf install gtk3 gtk-layer-shell
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
By default, all features are enabled for convenience. This can result in a significant compile time.
|
||||
If you know you are not going to need all the features, you can compile with only the features you need.
|
||||
|
||||
As of `v0.10.0`, compiling with no features is about 33% faster.
|
||||
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
|
||||
This difference is expected to increase as the bar develops.
|
||||
|
||||
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
|
||||
|
||||
To build using only specific features, disable default features and pass a comma separated list to `cargo build`:
|
||||
|
||||
```shell
|
||||
cargo build --release --no-default-features \
|
||||
--features http,config+json,clock
|
||||
```
|
||||
|
||||
> ⚠ Make sure you enable at least one `config` feature otherwise you will not be able to start the bar!
|
||||
|
||||
| Feature | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------------|
|
||||
| **Core** | |
|
||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||
| ipc | Enables the IPC server. |
|
||||
| cli | Enables the CLI. Will also enable `ipc`. |
|
||||
| config+all | Enables support for all configuration languages. |
|
||||
| config+json | Enables configuration support for JSON. |
|
||||
| config+yaml | Enables configuration support for YAML. |
|
||||
| config+toml | Enables configuration support for TOML. |
|
||||
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
|
||||
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
|
||||
| **Modules** | |
|
||||
| clipboard | Enables the `clipboard` module. |
|
||||
| clock | Enables the `clock` module. |
|
||||
| music+all | Enables the `music` module with support for all player types. |
|
||||
| music+mpris | Enables the `music` module with MPRIS support. |
|
||||
| music+mpd | Enables the `music` module with MPD support. |
|
||||
| sys_info | Enables the `sys_info` module. |
|
||||
| tray | Enables the `tray` module. |
|
||||
| upower | Enables the `upower` module. |
|
||||
| workspaces+all | Enables the `workspaces` module with support for all compositors. |
|
||||
| workspaces+sway | Enables the `workspaces` module with support for Sway. |
|
||||
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |
|
||||
|
||||
@@ -267,14 +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:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
|
||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
|
||||
### 3.2 Module-level options
|
||||
|
||||
@@ -283,12 +290,33 @@ For details on available modules and each of their config options, check the sid
|
||||
|
||||
For information on the `Script` type, and embedding scripts in strings, see [here](script).
|
||||
|
||||
#### Events
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. |
|
||||
| `on_click_middle` | `Script [oneshot]` | `null` | Runs the script when the module is middle clicked. |
|
||||
| `on_click_right` | `Script [oneshot]` | `null` | Runs the script when the module is right clicked. |
|
||||
| `on_scroll_up` | `Script [oneshot]` | `null` | Runs the script when the module is scroll up on. |
|
||||
| `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
|
||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||
| `on_mouse_enter` | `Script [oneshot]` | `null` | Runs the script when the module is hovered over. |
|
||||
| `on_mouse_exit` | `Script [oneshot]` | `null` | Runs the script when the module is no longer hovered over. |
|
||||
|
||||
#### Visibility
|
||||
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||
|
||||
#### Appearance
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------|--------------------|---------|-----------------------------------------------------------------------------------|
|
||||
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
|
||||
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
|
||||
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
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
|
||||
```
|
||||
15
docs/Images.md
Normal file
15
docs/Images.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Ironbar is capable of loading images from multiple sources.
|
||||
In any situation where an option takes text or an icon,
|
||||
you can use a string in any of the following formats, and it will automatically be detected as an image:
|
||||
|
||||
| Source | Example |
|
||||
|-------------------------------|---------------------------------|
|
||||
| GTK icon theme | `icon:firefox` |
|
||||
| Local file | `file:///path/to/file.jpg` |
|
||||
| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` |
|
||||
|
||||
Remote images are loaded asynchronously to avoid blocking the UI thread.
|
||||
Be aware this can cause elements to change size upon load if the image is large enough.
|
||||
|
||||
Note that mixing text and images is not supported.
|
||||
Your best option here is to use Nerd Font icons instead.
|
||||
9
docs/Ironvars.md
Normal file
9
docs/Ironvars.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Ironvars are runtime variables that can be referenced in several places in your config,
|
||||
then set using the IPC server (such as via the CLI) using the `set` command.
|
||||
|
||||
Any UTF-8 string *without whitespace* is a valid key.
|
||||
Any UTF-8 string is a valid value.
|
||||
|
||||
Reference values using `#my_variable`. These update as soon as the value changes.
|
||||
|
||||
You can set defaults using the `ironvar_defaults` key in your top-level config.
|
||||
@@ -4,17 +4,37 @@ To style the bar, create a file at `~/.config/ironbar/style.css`.
|
||||
|
||||
Style changes are hot-loaded so there is no need to reload the bar.
|
||||
|
||||
A reminder: since the bar is GTK-based, it uses GTK's implementation of CSS,
|
||||
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html),
|
||||
which only includes a subset of the full web spec (plus a few non-standard properties).
|
||||
|
||||
The below table describes the selectors provided by the bar itself.
|
||||
Information on styling individual modules can be found on their pages in the sidebar.
|
||||
|
||||
| Selector | Description |
|
||||
|----------------|-------------------------------------------|
|
||||
| `.background` | Top-level window |
|
||||
| `#bar` | Bar root box |
|
||||
| `#bar #start` | Bar left or top modules container box |
|
||||
| `#bar #center` | Bar center modules container box |
|
||||
| `#bar #end` | Bar right or bottom modules container box |
|
||||
| `.container` | All of the above |
|
||||
| Selector | Description |
|
||||
|----------------|--------------------------------------------|
|
||||
| `.background` | Top-level window. |
|
||||
| `#bar` | Bar root box. |
|
||||
| `#bar #start` | Bar left or top modules container box. |
|
||||
| `#bar #center` | Bar center modules container box. |
|
||||
| `#bar #end` | Bar right or bottom modules container box. |
|
||||
| `.container` | All of the above. |
|
||||
| `.popup` | Any popup box. |
|
||||
|
||||
Every widget can be selected using a `kebab-case` class name matching its name.
|
||||
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
|
||||
|
||||
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
|
||||
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
|
||||
|
||||
You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
|
||||
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
|
||||
|
||||
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
|
||||
|
||||
```css
|
||||
@define-color color_bg #2d2d2d;
|
||||
|
||||
box, menubar {
|
||||
background-color: @color_bg;
|
||||
}
|
||||
```
|
||||
@@ -1,13 +1,21 @@
|
||||
# Guides
|
||||
|
||||
- [Compiling from source](compiling)
|
||||
- [Configuration guide](configuration-guide)
|
||||
- [Scripts](scripts)
|
||||
- [Images](images)
|
||||
- [Styling guide](styling-guide)
|
||||
|
||||
# Dynamic content
|
||||
|
||||
- [Controlling Ironbar](controlling-ironbar)
|
||||
- [Dynamic values](dynamic-values)
|
||||
- [Scripts](scripts)
|
||||
- [Ironvars](ironvars)
|
||||
|
||||
# Examples
|
||||
|
||||
- [Config](config)
|
||||
- [Stylesheet](stylesheet)
|
||||
- [Stylesheet](https://github.com/JakeStanger/ironbar/blob/master/examples/style.css)
|
||||
|
||||
## Custom
|
||||
|
||||
@@ -15,12 +23,15 @@
|
||||
|
||||
# Modules
|
||||
|
||||
- [Clipboard](clipboard)
|
||||
- [Clock](clock)
|
||||
- [Custom](custom)
|
||||
- [Focused](focused)
|
||||
- [Label](label)
|
||||
- [Launcher](launcher)
|
||||
- [Music](music)
|
||||
- [Script](script)
|
||||
- [Sys_Info](sys-info)
|
||||
- [Tray](tray)
|
||||
- [Workspaces](workspaces)
|
||||
- [Upower](upower)
|
||||
- [Workspaces](workspaces)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,202 +1,10 @@
|
||||
The below config shows a module of each type being used.
|
||||
The configs linked below show a module of each type being used.
|
||||
|
||||
The Corn format makes heavy use of variables
|
||||
to show how module configs can be easily referenced to improve readability
|
||||
and reduce config length when using multiple bars.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"start": [
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "ﭮ",
|
||||
"2": "",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
"Games": ""
|
||||
},
|
||||
"type": "workspaces"
|
||||
},
|
||||
{
|
||||
"favorites": [
|
||||
"firefox",
|
||||
"discord",
|
||||
"Steam"
|
||||
],
|
||||
"icon_theme": "Paper",
|
||||
"show_icons": true,
|
||||
"show_names": false,
|
||||
"type": "launcher"
|
||||
}
|
||||
],
|
||||
"end": [
|
||||
{
|
||||
"music_dir": "/home/jake/Music",
|
||||
"type": "mpd"
|
||||
},
|
||||
{
|
||||
"host": "chloe:6600",
|
||||
"type": "mpd"
|
||||
},
|
||||
{
|
||||
"path": "/home/jake/bin/phone-battery",
|
||||
"type": "script"
|
||||
},
|
||||
{
|
||||
"format": [
|
||||
"{cpu-percent}% ",
|
||||
"{memory-percent}% "
|
||||
],
|
||||
"type": "sys-info"
|
||||
},
|
||||
{
|
||||
"type": "clock"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[start]]
|
||||
all_monitors = false
|
||||
type = 'workspaces'
|
||||
|
||||
[start.name_map]
|
||||
1 = 'ﭮ'
|
||||
2 = ''
|
||||
3 = ''
|
||||
Code = ''
|
||||
Games = ''
|
||||
|
||||
[[start]]
|
||||
icon_theme = 'Paper'
|
||||
show_icons = true
|
||||
show_names = false
|
||||
type = 'launcher'
|
||||
favorites = [
|
||||
'firefox',
|
||||
'discord',
|
||||
'Steam',
|
||||
]
|
||||
|
||||
[[end]]
|
||||
music_dir = '/home/jake/Music'
|
||||
type = 'mpd'
|
||||
|
||||
[[end]]
|
||||
host = 'chloe:6600'
|
||||
type = 'mpd'
|
||||
|
||||
[[end]]
|
||||
path = '/home/jake/bin/phone-battery'
|
||||
type = 'script'
|
||||
|
||||
[[end]]
|
||||
type = 'sys-info'
|
||||
format = [
|
||||
'{cpu-percent}% ',
|
||||
'{memory-percent}% ',
|
||||
]
|
||||
|
||||
[[end]]
|
||||
type = 'clock'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
---
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
"1": ﭮ
|
||||
"2":
|
||||
"3":
|
||||
Code:
|
||||
Games:
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- Steam
|
||||
icon_theme: Paper
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
end:
|
||||
- music_dir: /home/jake/Music
|
||||
type: mpd
|
||||
- host: "chloe:6600"
|
||||
type: mpd
|
||||
- path: /home/jake/bin/phone-battery
|
||||
type: script
|
||||
- format:
|
||||
- "{cpu-percent}% "
|
||||
- "{memory-percent}% "
|
||||
type: sys-info
|
||||
- type: clock
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
let {
|
||||
$workspaces = {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
2 = ""
|
||||
3 = ""
|
||||
Games = ""
|
||||
Code = ""
|
||||
}
|
||||
}
|
||||
|
||||
$launcher = {
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord" "Steam"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
icon_theme = "Paper"
|
||||
}
|
||||
|
||||
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
|
||||
$mpd_server = { type = "mpd" host = "chloe:6600" }
|
||||
|
||||
$sys_info = {
|
||||
type = "sys-info"
|
||||
format = ["{cpu-percent}% " "{memory-percent}% "]
|
||||
}
|
||||
|
||||
$tray = { type = "tray" }
|
||||
$clock = { type = "clock" }
|
||||
|
||||
$phone_battery = {
|
||||
type = "script"
|
||||
path = "/home/jake/bin/phone-battery"
|
||||
}
|
||||
|
||||
$start = [ $workspaces $launcher ]
|
||||
$end = [ $mpd_local $mpd_server $phone_battery $sys_info $clock ]
|
||||
}
|
||||
in {
|
||||
start = $start
|
||||
end = $end
|
||||
}
|
||||
```
|
||||
</details>
|
||||
- [JSON](https://github.com/JakeStanger/ironbar/blob/master/examples/config.json)
|
||||
- [TOML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.toml)
|
||||
- [YAML](https://github.com/JakeStanger/ironbar/blob/master/examples/config.yaml)
|
||||
- [Corn](https://github.com/JakeStanger/ironbar/blob/master/examples/config.corn)
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
The below example is a full stylesheet for all modules:
|
||||
|
||||
```css
|
||||
* {
|
||||
/* a nerd font is required to be installed for icons */
|
||||
font-family: Noto Sans Nerd Font, sans-serif;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#bar {
|
||||
border-top: 1px solid #424242;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.container#end > * + * {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
#workspaces .item {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#workspaces *:not(.focused):hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
#launcher .item {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#launcher .item:not(.focused):hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#launcher .open {
|
||||
border-bottom: 2px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .focused {
|
||||
color: white;
|
||||
background-color: black;
|
||||
border-bottom: 4px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .urgent {
|
||||
color: white;
|
||||
background-color: #8f0a0a;
|
||||
}
|
||||
|
||||
#script {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sysinfo {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tray .item {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
#mpd {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#popup-mpd {
|
||||
color: white;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #album-art {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #title .icon, #popup-mpd #title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
#popup-mpd #controls * {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#popup-mpd #controls *:disabled {
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
#clock {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#popup-clock {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#popup-clock #calendar-clock {
|
||||
color: white;
|
||||
font-size: 2.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
#popup-clock #calendar {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#popup-clock #calendar .header {
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #424242;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#popup-clock #calendar:selected {
|
||||
background-color: #6699cc;
|
||||
}
|
||||
|
||||
#focused {
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
Creates a button on the bar, which opens a popup. The popup contains a header, shutdown button, restart button, and uptime.
|
||||
|
||||

|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -16,9 +16,9 @@ Creates a button on the bar, which opens a popup. The popup contains a header, s
|
||||
{
|
||||
"bar": [
|
||||
{
|
||||
"on_click": "popup:toggle",
|
||||
"label": "",
|
||||
"name": "power-btn",
|
||||
"on_click": "popup:toggle",
|
||||
"type": "button"
|
||||
}
|
||||
],
|
||||
@@ -38,26 +38,27 @@ Creates a button on the bar, which opens a popup. The popup contains a header, s
|
||||
"widgets": [
|
||||
{
|
||||
"class": "power-btn",
|
||||
"on_click": "!shutdown now",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!shutdown now",
|
||||
"type": "button"
|
||||
},
|
||||
{
|
||||
"class": "power-btn",
|
||||
"on_click": "!reboot",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!reboot",
|
||||
"type": "button"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"name": "uptime",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
@@ -75,12 +76,13 @@ type = 'clock'
|
||||
|
||||
[[end]]
|
||||
class = 'power-menu'
|
||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
type = 'custom'
|
||||
|
||||
[[end.bar]]
|
||||
on_click = 'popup:toggle'
|
||||
label = ''
|
||||
name = 'power-btn'
|
||||
on_click = 'popup:toggle'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup]]
|
||||
@@ -97,18 +99,18 @@ type = 'box'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
on_click = '!shutdown now'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!shutdown now'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
on_click = '!reboot'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!reboot'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
name = 'uptime'
|
||||
type = 'label'
|
||||
```
|
||||
@@ -121,10 +123,11 @@ type = 'label'
|
||||
```yaml
|
||||
end:
|
||||
- type: clock
|
||||
|
||||
- bar:
|
||||
- on_click: popup:toggle
|
||||
label:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
@@ -137,16 +140,17 @@ end:
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
on_click: '!shutdown now'
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
- class: power-btn
|
||||
on_click: '!reboot'
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
```
|
||||
|
||||
@@ -157,30 +161,37 @@ end:
|
||||
|
||||
```corn
|
||||
let {
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
popup = [ {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
} ]
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
|
||||
$clock = { type = "clock" }
|
||||
} in {
|
||||
end = [ $power_menu ]
|
||||
end = [ $power_menu $clock ]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
97
docs/modules/Clipboard.md
Normal file
97
docs/modules/Clipboard.md
Normal file
@@ -0,0 +1,97 @@
|
||||
Shows recent clipboard items, allowing you to switch between them to re-copy previous values.
|
||||
Clicking the icon button opens the popup containing all functionality.
|
||||
|
||||
Supports plain text and images.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `clipboard`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": {
|
||||
"type": "clipboard",
|
||||
"max_items": 3,
|
||||
"truncate": {
|
||||
"mode": "end",
|
||||
"length": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
|
||||
[[end.truncate]]
|
||||
mode = "end"
|
||||
length = 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: 'clipboard'
|
||||
max_items: 3
|
||||
truncate:
|
||||
mode: 'end'
|
||||
length: 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [ {
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
truncate.mode = "end"
|
||||
truncate.length = 50
|
||||
} ]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------------------|------------------------------------------------------|
|
||||
| `.clipboard` | Clipboard widget. |
|
||||
| `.clipboard .btn` | Clipboard widget button. |
|
||||
| `.clipboard .btn .icon` | Clipboard widget button icon (any type). |
|
||||
| `.clipboard .btn .text-icon` | Clipboard widget button icon (textual only). |
|
||||
| `.clipboard .btn .image` | Clipboard widget button icon (image only). |
|
||||
| `.popup-clipboard` | Clipboard popup box. |
|
||||
| `.popup-clipboard .item` | Clipboard row item inside the popup. |
|
||||
| `.popup-clipboard .item .btn` | Clipboard row item radio button. |
|
||||
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
|
||||
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
|
||||
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -8,8 +8,8 @@ Clicking on the widget opens a popup with the time and a calendar.
|
||||
|
||||
> Type: `clock`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------|--------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Name | Type | Default | Description |
|
||||
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
|
||||
|
||||
<details>
|
||||
@@ -69,9 +69,11 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------------------|------------------------------------------------------------------------------------|
|
||||
| `#clock` | Clock widget button |
|
||||
| `#popup-clock` | Clock popup box |
|
||||
| `#popup-clock #calendar-clock` | Clock inside the popup |
|
||||
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||
| Selector | Description |
|
||||
|--------------------------------|------------------------------------------------------------------------------------|
|
||||
| `.clock` | Clock widget button |
|
||||
| `.popup-clock` | Clock popup box |
|
||||
| `.popup-clock .calendar-clock` | Clock inside the popup |
|
||||
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -1,7 +1,10 @@
|
||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
||||
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||
|
||||

|
||||
If you only intend to run a single script, prefer the [script](script) module,
|
||||
or [label](label) if you only need a single text label.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -10,27 +13,140 @@ Labels can display dynamic content from scripts, and buttons can interact with t
|
||||
This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand.
|
||||
It is well worth looking at the examples.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|------------|---------|--------------------------------------|
|
||||
| `class` | `string` | `null` | Container class name. |
|
||||
| `bar` | `Widget[]` | `null` | List of widgets to add to the bar. |
|
||||
| `popup` | `Widget[]` | `[]` | List of widgets to add to the popup. |
|
||||
|
||||
### `Widget`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|------------------------------|--------------|---------------------------------------------------------------------------|
|
||||
| `widget_type` | `box` or `label` or `button` | `null` | Type of GTK widget to create. |
|
||||
| `name` | `string` | `null` | Widget name. |
|
||||
| `class` | `string` | `null` | Widget class name. |
|
||||
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
|
||||
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
|
||||
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
|
||||
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
|
||||
There are many widget types, each with their own config options.
|
||||
You can think of these like HTML elements and their attributes.
|
||||
|
||||
### Labels
|
||||
Every widget has the following options available; `type` is mandatory.
|
||||
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
|
||||
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
|
||||
| `name` | `string` | `null` | Widget name. |
|
||||
| `class` | `string` | `null` | Widget class name. |
|
||||
|
||||
#### Box
|
||||
|
||||
A container to place nested widgets inside.
|
||||
|
||||
> Type: `box`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
|
||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||
|
||||
#### Label
|
||||
|
||||
A text label. Pango markup is supported.
|
||||
|
||||
> Type `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
|
||||
#### Button
|
||||
|
||||
A clickable button, which can run a command when clicked.
|
||||
|
||||
> Type `button`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||
|
||||
#### Image
|
||||
|
||||
An image or icon from disk or http.
|
||||
|
||||
> Type `image`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
||||
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
|
||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||
|
||||
#### Slider
|
||||
|
||||
A draggable slider.
|
||||
|
||||
> Type: `slider`
|
||||
|
||||
Note that `on_change` will provide the **floating point** value as an argument.
|
||||
If your input program requires an integer, you will need to round it.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
|
||||
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
||||
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
||||
| `min` | `float` | `0` | Minimum slider value. |
|
||||
| `max` | `float` | `100` | Maximum slider value. |
|
||||
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
||||
|
||||
The example slider widget below shows a volume control for MPC,
|
||||
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
||||
|
||||
```corn
|
||||
$slider = {
|
||||
type = "custom"
|
||||
bar = [
|
||||
{
|
||||
type = "slider"
|
||||
length = 100
|
||||
max = 100
|
||||
on_change="!mpc volume ${0%.*}"
|
||||
value = "200:mpc volume | cut -d ':' -f2 | cut -d '%' -f1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Progress
|
||||
|
||||
A progress bar.
|
||||
|
||||
> Type: `progress`
|
||||
|
||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
|
||||
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
||||
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||
|
||||
The example below shows progress for the current playing song in MPD,
|
||||
and displays the elapsed/length timestamps as a label above:
|
||||
|
||||
```corn
|
||||
$progress = {
|
||||
type = "custom"
|
||||
bar = [
|
||||
{
|
||||
type = "progress"
|
||||
value = "500:mpc | sed -n 2p | awk '{ print $4 }' | grep -Eo '[0-9]+' || echo 0"
|
||||
label = "{{500:mpc | sed -n 2p | awk '{ print $3 }'}} elapsed"
|
||||
length = 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Label Attributes
|
||||
|
||||
> ℹ This is different to the `label` widget, although applies to it.
|
||||
|
||||
Any widgets with a `label` attribute support embedded scripts,
|
||||
meaning you can interpolate text from scripts to dynamically show content.
|
||||
|
||||
Labels can interpolate text from scripts to dynamically show content.
|
||||
This can be done by including scripts in `{{double braces}}` using the shorthand script syntax.
|
||||
|
||||
For example, the following label would output your system uptime, updated every 30 seconds.
|
||||
@@ -39,7 +155,7 @@ For example, the following label would output your system uptime, updated every
|
||||
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
|
||||
```
|
||||
|
||||
Both polling and watching mode are supported. For more information on script syntax, see [here](script).
|
||||
Both polling and watching mode are supported. For more information on script syntax, see [here](scripts).
|
||||
|
||||
### Commands
|
||||
|
||||
@@ -50,12 +166,17 @@ To execute shell commands, prefix them with an `!`.
|
||||
For example, if you want to run `~/.local/bin/my-script.sh` on click,
|
||||
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
|
||||
|
||||
Some widgets provide a value when they run the command, such as `slider`.
|
||||
This is passed as an argument and can be accessed using `$0`.
|
||||
|
||||
The following bar commands are supported:
|
||||
|
||||
- `popup:toggle`
|
||||
- `popup:open`
|
||||
- `popup:close`
|
||||
|
||||
---
|
||||
|
||||
XML is arguably better-suited and easier to read for this sort of markup,
|
||||
but currently is not supported.
|
||||
Nonetheless, it may be worth comparing the examples to the below equivalent
|
||||
@@ -234,27 +355,32 @@ end:
|
||||
|
||||
```corn
|
||||
let {
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
popup = [ {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
} ]
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
} in {
|
||||
end = [ $power_menu ]
|
||||
@@ -265,8 +391,13 @@ let {
|
||||
|
||||
## Styling
|
||||
|
||||
Since the widgets are all custom, you can target them using `#name` and `.class`.
|
||||
Since the widgets are all custom, you can use their `name` and `class` attributes, then target them using `#name` and `.class`.
|
||||
|
||||
| Selector | Description |
|
||||
|-----------|-------------------------|
|
||||
| `#custom` | Custom widget container |
|
||||
The following top-level selectors are always available:
|
||||
|
||||
| Selector | Description |
|
||||
|-----------------|--------------------------------|
|
||||
| `.custom` | Custom widget container. |
|
||||
| `.popup-custom` | Custom widget popup container. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -7,12 +7,15 @@ Displays the title and/or icon of the currently focused window.
|
||||
|
||||
> Type: `focused`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------------|-----------|---------|---------------------------------|
|
||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
||||
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
||||
| `icon_size` | `integer` | `32` | Size of icon in pixels |
|
||||
| `icon_theme` | `string` | `null` | GTK icon theme to use |
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
||||
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
||||
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -25,7 +28,7 @@ Displays the title and/or icon of the currently focused window.
|
||||
"show_icon": true,
|
||||
"show_title": true,
|
||||
"icon_size": 32,
|
||||
"icon_theme": "Paper"
|
||||
"truncate": "end"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -43,7 +46,7 @@ type = "focused"
|
||||
show_icon = true
|
||||
show_title = true
|
||||
icon_size = 32
|
||||
icon_theme = "Paper"
|
||||
truncate = "end"
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -57,7 +60,7 @@ end:
|
||||
show_icon: true
|
||||
show_title: true
|
||||
icon_size: 32
|
||||
icon_theme: "Paper"
|
||||
truncate: "end"
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -73,7 +76,7 @@ end:
|
||||
show_icon = true
|
||||
show_title = true
|
||||
icon_size = 32
|
||||
icon_theme = "Paper"
|
||||
truncate = "end"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -83,8 +86,10 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|--------------------------|--------------------|
|
||||
| `#focused` | Focused widget box |
|
||||
| `#focused #icon` | App icon |
|
||||
| `#focused #label` | App name |
|
||||
| Selector | Description |
|
||||
|-------------------|--------------------|
|
||||
| `.focused` | Focused widget box |
|
||||
| `.focused .icon` | App icon |
|
||||
| `.focused .label` | App name |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
75
docs/modules/Label.md
Normal file
75
docs/modules/Label.md
Normal file
@@ -0,0 +1,75 @@
|
||||
Displays custom text, with markup support.
|
||||
|
||||
If you only intend to run a single script, prefer the [script](script) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `label`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|---------|-------------------------------------------------|---------|------------------------|
|
||||
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "label"
|
||||
label: "random num: {{500:echo $RANDOM}}"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "label"
|
||||
label = "random num: {{500:echo $RANDOM}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|----------|------------------------------------------------------------------------------------|
|
||||
| `.label` | Label widget |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -3,7 +3,7 @@ Hovering over a program with multiple windows open shows a popup with each windo
|
||||
Clicking an icon/popup item focuses or launches the program.
|
||||
Optionally displays a launchable set of favourites.
|
||||
|
||||

|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -14,7 +14,7 @@ Optionally displays a launchable set of favourites.
|
||||
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
|
||||
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
|
||||
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
|
||||
| `icon_theme` | `string` | `null` | GTK icon theme to use. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -29,8 +29,7 @@ Optionally displays a launchable set of favourites.
|
||||
"discord"
|
||||
],
|
||||
"show_names": false,
|
||||
"show_icons": true,
|
||||
"icon_theme": "Paper"
|
||||
"show_icons": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -49,7 +48,6 @@ type = "launcher"
|
||||
favorites = ["firefox", "discord"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
icon_theme = "Paper"
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -65,7 +63,6 @@ start:
|
||||
- discord
|
||||
show_names: false
|
||||
show_icons: true
|
||||
icon_theme: "Paper"
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -78,10 +75,9 @@ start:
|
||||
start = [
|
||||
{
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord"]
|
||||
favorites = [ "firefox" "discord" ]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
icon_theme = "Paper"
|
||||
|
||||
}
|
||||
]
|
||||
@@ -94,10 +90,12 @@ start:
|
||||
|
||||
| Selector | Description |
|
||||
|-------------------------------|--------------------------|
|
||||
| `#launcher` | Launcher widget box |
|
||||
| `#launcher .item` | App button |
|
||||
| `#launcher .item.open` | App button (open app) |
|
||||
| `#launcher .item.focused` | App button (focused app) |
|
||||
| `#launcher .item.urgent` | App button (urgent app) |
|
||||
| `#launcher-popup` | Popup container |
|
||||
| `#launcher-popup .popup-item` | Window button in popup |
|
||||
| `.launcher` | Launcher widget box |
|
||||
| `.launcher .item` | App button |
|
||||
| `.launcher .item.open` | App button (open app) |
|
||||
| `.launcher .item.focused` | App button (focused app) |
|
||||
| `.launcher .item.urgent` | App button (urgent app) |
|
||||
| `.popup-launcher` | Popup container |
|
||||
| `.popup-launcher .popup-item` | Window button in popup |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -5,21 +5,35 @@ and playback controls.
|
||||
|
||||
in MPRIS mode, the widget will listen to all players and automatically detect/display the active one.
|
||||
|
||||

|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `music`
|
||||
|
||||
| | Type | Default | Description |
|
||||
|----------------|------------------|-----------------------------|----------------------------------------------------------------------------------|
|
||||
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
|
||||
| `icons.play` | `string` | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string` | `` | Icon to show when paused. |
|
||||
| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. |
|
||||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
| | Type | Default | Description |
|
||||
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
||||
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
|
||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
|
||||
See [here](images) for information on images.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -30,7 +44,8 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
||||
{
|
||||
"type": "music",
|
||||
"player_type": "mpd",
|
||||
"format": "{icon} {title} / {artist}",
|
||||
"format": "{title} / {artist}",
|
||||
"truncate": "end",
|
||||
"icons": {
|
||||
"play": "",
|
||||
"pause": ""
|
||||
@@ -50,8 +65,9 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
||||
[[start]]
|
||||
type = "music"
|
||||
player_type = "mpd"
|
||||
format = "{icon} {title} / {artist}"
|
||||
format = "{title} / {artist}"
|
||||
music_dir = "/home/jake/Music"
|
||||
truncate = "end"
|
||||
|
||||
[[start.icons]]
|
||||
play = ""
|
||||
@@ -67,7 +83,8 @@ pause = ""
|
||||
start:
|
||||
- type: "music"
|
||||
player_type: "mpd"
|
||||
format: "{icon} {title} / {artist}"
|
||||
format: "{title} / {artist}"
|
||||
truncate: "end"
|
||||
icons:
|
||||
play: ""
|
||||
pause: ""
|
||||
@@ -85,7 +102,8 @@ start:
|
||||
{
|
||||
type = "music"
|
||||
player_type = "mpd"
|
||||
format = "{icon} {title} / {artist}"
|
||||
format = "{title} / {artist}"
|
||||
truncate = "end"
|
||||
icons.play = ""
|
||||
icons.pause = ""
|
||||
music_dir = "/home/jake/Music"
|
||||
@@ -103,7 +121,6 @@ and will be replaced with values from the currently playing track:
|
||||
|
||||
| Token | Description |
|
||||
|--------------|--------------------------------------|
|
||||
| `{icon}` | Either `icons.play` or `icons.pause` |
|
||||
| `{title}` | Title |
|
||||
| `{album}` | Album name |
|
||||
| `{artist}` | Artist name |
|
||||
@@ -116,24 +133,40 @@ and will be replaced with values from the currently playing track:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|------------------------------------------|------------------------------------------|
|
||||
| `#music` | Tray widget button |
|
||||
| `#popup-music` | Popup box |
|
||||
| `#popup-music #album-art` | Album art image inside popup box |
|
||||
| `#popup-music #title` | Track title container inside popup box |
|
||||
| `#popup-music #title .icon` | Track title icon label inside popup box |
|
||||
| `#popup-music #title .label` | Track title label inside popup box |
|
||||
| `#popup-music #album` | Track album container inside popup box |
|
||||
| `#popup-music #album .icon` | Track album icon label inside popup box |
|
||||
| `#popup-music #album .label` | Track album label inside popup box |
|
||||
| `#popup-music #artist` | Track artist container inside popup box |
|
||||
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
|
||||
| `#popup-music #artist .label` | Track artist label inside popup box |
|
||||
| `#popup-music #controls` | Controls container inside popup box |
|
||||
| `#popup-music #controls #btn-prev` | Previous button inside popup box |
|
||||
| `#popup-music #controls #btn-play-pause` | Play/pause button inside popup box |
|
||||
| `#popup-music #controls #btn-next` | Next button inside popup box |
|
||||
| `#popup-music #volume` | Volume container inside popup box |
|
||||
| `#popup-music #volume #slider` | Volume slider popup box |
|
||||
| `#popup-music #volume .icon` | Volume icon label inside popup box |
|
||||
| Selector | Description |
|
||||
|---------------------------------------------|-------------------------------------------------------|
|
||||
| `.music` | Tray widget button |
|
||||
| `.music .contents` | Tray widget button contents box |
|
||||
| `.music .contents .icon` | Tray widget button icon (any type) |
|
||||
| `.music .contents .text-icon` | Tray widget button icon (textual only) |
|
||||
| `.music .contents .image` | Tray widget button icon (image only) |
|
||||
| `.popup-music` | Popup box |
|
||||
| `.popup-music .album-art` | Album art image inside popup box |
|
||||
| `.popup-music .title` | Track title container inside popup box |
|
||||
| `.popup-music .title .icon-box` | Track title icon container inside popup box |
|
||||
| `.popup-music .title .icon-box .icon` | Track title icon inside its container (any type) |
|
||||
| `.popup-music .title .icon-box .text-icon` | Track title icon inside its container (textual only) |
|
||||
| `.popup-music .title .icon-box .image` | Track title icon inside its container (image only) |
|
||||
| `.popup-music .title .label` | Track title label inside popup box |
|
||||
| `.popup-music .album` | Track album container inside popup box |
|
||||
| `.popup-music .album .icon-box` | Track album icon container inside popup box |
|
||||
| `.popup-music .album .icon-box .icon` | Track album icon inside its container (any type) |
|
||||
| `.popup-music .album .icon-box .text-icon` | Track album icon inside its container (textual only) |
|
||||
| `.popup-music .album .icon-box .image` | Track album icon inside its container (image only) |
|
||||
| `.popup-music .album .label` | Track album label inside popup box |
|
||||
| `.popup-music .artist` | Track artist container inside popup box |
|
||||
| `.popup-music .artist .icon-box` | Track artist icon container inside popup box |
|
||||
| `.popup-music .artist .icon-box .icon` | Track artist icon inside its container (any type) |
|
||||
| `.popup-music .artist .icon-box .text-icon` | Track artist icon inside its container (textual only) |
|
||||
| `.popup-music .artist .icon-box .image` | Track artist icon inside its container (image only) |
|
||||
| `.popup-music .artist .label` | Track artist label inside popup box |
|
||||
| `.popup-music .controls` | Controls container inside popup box |
|
||||
| `.popup-music .controls .btn-prev` | Previous button inside popup box |
|
||||
| `.popup-music .controls .btn-play` | Play button inside popup box |
|
||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
||||
| `.popup-music .volume` | Volume container inside popup box |
|
||||
| `.popup-music .volume .slider` | 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).
|
||||
@@ -1,6 +1,9 @@
|
||||
Executes a script and shows the result of `stdout` on a label.
|
||||
Pango markup is supported.
|
||||
|
||||
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
|
||||
For more advanced use-cases, use [custom](custom).
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `script`
|
||||
@@ -82,6 +85,8 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------|---------------------|
|
||||
| `#script` | Script widget label |
|
||||
| Selector | Description |
|
||||
|-----------|---------------------|
|
||||
| `.script` | Script widget label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -168,9 +168,13 @@ The following tokens can be used in the `format` configuration option:
|
||||
| `{load_average:15}` | 15-minute load average. |
|
||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
||||
|
||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|------------------|------------------------------|
|
||||
| `#sysinfo` | Sysinfo widget box |
|
||||
| `#sysinfo #item` | Individual information label |
|
||||
| `.sysinfo` | Sysinfo widget box |
|
||||
| `.sysinfo .item` | Individual information label |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
|
||||
@@ -60,5 +60,7 @@ end:
|
||||
|
||||
| Selector | Description |
|
||||
|---------------|------------------|
|
||||
| `#tray` | Tray widget box |
|
||||
| `#tray .item` | Tray icon button |
|
||||
| `.tray` | Tray widget box |
|
||||
| `.tray .item` | Tray icon button |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
84
docs/modules/Upower.md
Normal file
84
docs/modules/Upower.md
Normal file
@@ -0,0 +1,84 @@
|
||||
Displays system power information such as the battery percentage, and estimated time to empty.
|
||||
|
||||
`TODO: ADD SCREENSHOT`
|
||||
|
||||
[//]: # ()
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
> Type: `upower`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------|-----------|-----------------|---------------------------------------------------|
|
||||
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||
| `icon_size` | `integer` | `24` | Size to render icon at. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": [
|
||||
{
|
||||
"type": "upower",
|
||||
"format": "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: "upower"
|
||||
format: "{percentage}%"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "upower"
|
||||
format = "{percentage}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------------------|--------------------------------|
|
||||
| `.upower` | Upower widget container. |
|
||||
| `.upower .button` | Upower widget button. |
|
||||
| `.upower .button .contents` | Upower widget button contents. |
|
||||
| `.upower .button .icon` | Upower widget battery icon. |
|
||||
| `.upower .button .label` | Upower widget button label. |
|
||||
| `.popup-upower` | Upower popup box. |
|
||||
| `.popup-upower .upower-details` | Label inside the popup. |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -8,11 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
|
||||
> Type: `workspaces`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string>` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -72,15 +73,15 @@ end:
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "workspaces",
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
end = [
|
||||
{
|
||||
type = "workspaces",
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -88,8 +89,13 @@ end:
|
||||
|
||||
## Styling
|
||||
|
||||
| Selector | Description |
|
||||
|-----------------------------|--------------------------------------|
|
||||
| `#workspaces` | Workspaces widget box |
|
||||
| `#workspaces .item` | Workspace button |
|
||||
| `#workspaces .item.focused` | Workspace button (workspace focused) |
|
||||
| Selector | Description |
|
||||
|--------------------------------|--------------------------------------|
|
||||
| `.workspaces` | Workspaces widget box |
|
||||
| `.workspaces .item` | Workspace button |
|
||||
| `.workspaces .item.focused` | Workspace button (workspace focused) |
|
||||
| `.workspaces .item .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).
|
||||
@@ -4,78 +4,109 @@ let {
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = "ﭮ"
|
||||
2 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Games = ""
|
||||
Games = "icon:steam"
|
||||
Code = ""
|
||||
}
|
||||
}
|
||||
|
||||
$focused = { type = "focused" }
|
||||
|
||||
$launcher = {
|
||||
type = "launcher"
|
||||
favorites = ["firefox" "discord" "Steam"]
|
||||
favorites = ["firefox" "discord" "steam"]
|
||||
show_names = false
|
||||
show_icons = true
|
||||
icon_theme = "Paper"
|
||||
}
|
||||
|
||||
$mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
|
||||
$mpd_server = { type = "mpd" host = "chloe:6600" }
|
||||
$mpris = {
|
||||
type = "music"
|
||||
player_type = "mpris"
|
||||
|
||||
on_click_middle = "playerctl play-pause"
|
||||
on_scroll_up = "playerctl volume +5"
|
||||
on_scroll_down = "playerctl volume -5"
|
||||
|
||||
}
|
||||
|
||||
$mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
|
||||
$mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
|
||||
|
||||
$sys_info = {
|
||||
type = "sys_info"
|
||||
format = ["{cpu_percent}% " "{memory_percent}% "]
|
||||
|
||||
interval.memory = 30
|
||||
interval.cpu = 1
|
||||
interval.temps = 5
|
||||
interval.disks = 300
|
||||
interval.networks = 3
|
||||
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
|
||||
$tray = { type = "tray" }
|
||||
$clock = {
|
||||
type = "clock"
|
||||
// show-if = "500:[ $(($(date +%s) % 2)) -eq 0 ]"
|
||||
show_if.cmd = "exit 0"
|
||||
show_if.interval = 500
|
||||
}
|
||||
|
||||
$clock = { type = "clock" }
|
||||
|
||||
$phone_battery = {
|
||||
type = "script"
|
||||
cmd = "/home/jake/bin/phone-battery"
|
||||
|
||||
show_if.cmd = "/home/jake/bin/phone-connected"
|
||||
show_if.interval = 500
|
||||
}
|
||||
|
||||
$log_tail = {
|
||||
type = "script"
|
||||
path = "tail -f /home/jake/.local/share/ironbar/error.log"
|
||||
mode = "watch"
|
||||
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
|
||||
|
||||
$label = { type = "label" label = "random num: {{500:echo $RANDOM}}" }
|
||||
|
||||
// -- begin custom --
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
||||
|
||||
popup = [ {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
} ]
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
}
|
||||
// -- end custom --
|
||||
|
||||
$left = [ $workspaces $launcher ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $power_menu $clock ]
|
||||
$left = [ $workspaces $launcher $label ]
|
||||
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
|
||||
}
|
||||
in {
|
||||
anchor_to_edges = true
|
||||
position = "top"
|
||||
start = $left end = $right
|
||||
position = "bottom"
|
||||
icon_theme = "Paper"
|
||||
|
||||
start = $left
|
||||
end = $right
|
||||
}
|
||||
|
||||
@@ -1,43 +1,135 @@
|
||||
{
|
||||
"start": [
|
||||
{
|
||||
"type": "workspaces"
|
||||
},
|
||||
{
|
||||
"type": "launcher",
|
||||
"icon_theme": "Paper",
|
||||
"favorites": [
|
||||
"firefox",
|
||||
"discord",
|
||||
"Steam"
|
||||
],
|
||||
"show_names": false
|
||||
}
|
||||
],
|
||||
"anchor_to_edges": true,
|
||||
"end": [
|
||||
{
|
||||
"type": "mpd"
|
||||
"music_dir": "/home/jake/Music",
|
||||
"player_type": "mpd",
|
||||
"truncate": {
|
||||
"max_length": 100,
|
||||
"mode": "end"
|
||||
},
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"type": "mpd",
|
||||
"host": "chloe:6600"
|
||||
"host": "chloe:6600",
|
||||
"player_type": "mpd",
|
||||
"truncate": "end",
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"path": "/home/jake/bin/phone-battery",
|
||||
"cmd": "/home/jake/bin/phone-battery",
|
||||
"show_if": {
|
||||
"cmd": "/home/jake/bin/phone-connected",
|
||||
"interval": 500
|
||||
},
|
||||
"type": "script"
|
||||
},
|
||||
{
|
||||
"format": [
|
||||
"{cpu_percent}% ",
|
||||
"{memory_percent}% "
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
],
|
||||
"interval": {
|
||||
"cpu": 1,
|
||||
"disks": 300,
|
||||
"memory": 30,
|
||||
"networks": 3,
|
||||
"temps": 5
|
||||
},
|
||||
"type": "sys_info"
|
||||
},
|
||||
{
|
||||
"type": "tray"
|
||||
"max_items": 3,
|
||||
"truncate": {
|
||||
"length": 50,
|
||||
"mode": "end"
|
||||
},
|
||||
"type": "clipboard"
|
||||
},
|
||||
{
|
||||
"bar": [
|
||||
{
|
||||
"label": "",
|
||||
"name": "power-btn",
|
||||
"on_click": "popup:toggle",
|
||||
"type": "button"
|
||||
}
|
||||
],
|
||||
"class": "power-menu",
|
||||
"popup": [
|
||||
{
|
||||
"orientation": "vertical",
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"label": "Power menu",
|
||||
"name": "header",
|
||||
"type": "label"
|
||||
},
|
||||
{
|
||||
"type": "box",
|
||||
"widgets": [
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!shutdown now",
|
||||
"type": "button"
|
||||
},
|
||||
{
|
||||
"class": "power-btn",
|
||||
"label": "<span font-size='40pt'></span>",
|
||||
"on_click": "!reboot",
|
||||
"type": "button"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"name": "uptime",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tooltip": "Up: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"type": "clock"
|
||||
}
|
||||
],
|
||||
"icon_theme": "Paper",
|
||||
"position": "bottom",
|
||||
"start": [
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "ﭮ",
|
||||
"2": "icon:firefox",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
"Games": "icon:steam"
|
||||
},
|
||||
"type": "workspaces"
|
||||
},
|
||||
{
|
||||
"favorites": [
|
||||
"firefox",
|
||||
"discord",
|
||||
"steam"
|
||||
],
|
||||
"show_icons": true,
|
||||
"show_names": false,
|
||||
"type": "launcher"
|
||||
},
|
||||
{
|
||||
"label": "random num: {{500:echo $RANDOM}}",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
122
examples/config.toml
Normal file
122
examples/config.toml
Normal file
@@ -0,0 +1,122 @@
|
||||
anchor_to_edges = true
|
||||
icon_theme = 'Paper'
|
||||
position = 'bottom'
|
||||
|
||||
[[end]]
|
||||
music_dir = '/home/jake/Music'
|
||||
player_type = 'mpd'
|
||||
type = 'music'
|
||||
|
||||
[end.truncate]
|
||||
max_length = 100
|
||||
mode = 'end'
|
||||
|
||||
[[end]]
|
||||
host = 'chloe:6600'
|
||||
player_type = 'mpd'
|
||||
truncate = 'end'
|
||||
type = 'music'
|
||||
|
||||
[[end]]
|
||||
cmd = '/home/jake/bin/phone-battery'
|
||||
type = 'script'
|
||||
|
||||
[end.show_if]
|
||||
cmd = '/home/jake/bin/phone-connected'
|
||||
interval = 500
|
||||
|
||||
[[end]]
|
||||
type = 'sys_info'
|
||||
format = [
|
||||
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
]
|
||||
|
||||
[end.interval]
|
||||
cpu = 1
|
||||
disks = 300
|
||||
memory = 30
|
||||
networks = 3
|
||||
temps = 5
|
||||
|
||||
[[end]]
|
||||
max_items = 3
|
||||
type = 'clipboard'
|
||||
|
||||
[end.truncate]
|
||||
length = 50
|
||||
mode = 'end'
|
||||
|
||||
[[end]]
|
||||
class = 'power-menu'
|
||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
type = 'custom'
|
||||
|
||||
[[end.bar]]
|
||||
label = ''
|
||||
name = 'power-btn'
|
||||
on_click = 'popup:toggle'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup]]
|
||||
orientation = 'vertical'
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = 'Power menu'
|
||||
name = 'header'
|
||||
type = 'label'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!shutdown now'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!reboot'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
name = 'uptime'
|
||||
type = 'label'
|
||||
|
||||
[[end]]
|
||||
type = 'clock'
|
||||
|
||||
[[start]]
|
||||
all_monitors = false
|
||||
type = 'workspaces'
|
||||
|
||||
[start.name_map]
|
||||
1 = 'ﭮ'
|
||||
2 = 'icon:firefox'
|
||||
3 = ''
|
||||
Code = ''
|
||||
Games = 'icon:steam'
|
||||
|
||||
[[start]]
|
||||
show_icons = true
|
||||
show_names = false
|
||||
type = 'launcher'
|
||||
favorites = [
|
||||
'firefox',
|
||||
'discord',
|
||||
'steam',
|
||||
]
|
||||
|
||||
[[start]]
|
||||
label = 'random num: {{500:echo $RANDOM}}'
|
||||
type = 'label'
|
||||
|
||||
87
examples/config.yaml
Normal file
87
examples/config.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
anchor_to_edges: true
|
||||
end:
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- format:
|
||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
||||
- 李 {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||
- 猪 {load_average:1} | {load_average:5} | {load_average:15}
|
||||
- {uptime}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
icon_theme: Paper
|
||||
position: bottom
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1': ﭮ
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo $RANDOM}}'
|
||||
type: label
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
let {
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
$popup = {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||
]
|
||||
}
|
||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||
]
|
||||
}
|
||||
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ $button ]
|
||||
popup = [ $popup ]
|
||||
}
|
||||
} in {
|
||||
end = [ $power_menu { type = "clock" } ]
|
||||
}
|
||||
@@ -1,148 +1,195 @@
|
||||
@define-color color_bg #2d2d2d;
|
||||
@define-color color_bg_dark #1c1c1c;
|
||||
@define-color color_border #424242;
|
||||
@define-color color_border_active #6699cc;
|
||||
@define-color color_text #ffffff;
|
||||
@define-color color_urgent #8f0a0a;
|
||||
|
||||
/* -- base styles -- */
|
||||
|
||||
* {
|
||||
/* `otf-font-awesome` is required to be installed for icons */
|
||||
font-family: Noto Sans Nerd Font, sans-serif;
|
||||
/* font-family: 'Jetbrains Mono', monospace;*/
|
||||
font-size: 16px;
|
||||
|
||||
/*color: white;*/
|
||||
/*background-color: #2d2d2d;*/
|
||||
/*background-color: red;*/
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/*opacity: 0.4;*/
|
||||
box, menubar, button {
|
||||
background-color: @color_bg;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
button, label {
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
#bar {
|
||||
border-top: 1px solid #424242;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
/* test 34543*/
|
||||
|
||||
#right > * + * {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#workspaces .item {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#workspaces *:not(.focused):hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
#launcher .item {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#launcher .item:not(.focused):hover {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#launcher .open {
|
||||
border-bottom: 2px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .focused {
|
||||
color: white;
|
||||
background-color: black;
|
||||
border-bottom: 4px solid #6699cc;
|
||||
}
|
||||
|
||||
#launcher .urgent {
|
||||
color: white;
|
||||
background-color: #8f0a0a;
|
||||
}
|
||||
|
||||
#clock {
|
||||
color: white;
|
||||
background-color: #2d2d2d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#script {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sysinfo {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tray .item {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
#mpd {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #424242;
|
||||
}
|
||||
|
||||
#popup-clock {
|
||||
border: 1px solid @color_border;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#calendar-clock {
|
||||
color: white;
|
||||
|
||||
/* -- clipboard -- */
|
||||
|
||||
.clipboard {
|
||||
margin-left: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.popup-clipboard .item {
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
|
||||
/* -- clock -- */
|
||||
|
||||
.clock {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.popup-clock .calendar-clock {
|
||||
color: @color_text;
|
||||
font-size: 2.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-clock .calendar {
|
||||
background-color: @color_bg;
|
||||
color: @color_text;
|
||||
}
|
||||
|
||||
#calendar .header {
|
||||
.popup-clock .calendar .header {
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #424242;
|
||||
border-top: 1px solid @color_border;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#calendar:selected {
|
||||
background-color: #6699cc;
|
||||
.popup-clock .calendar:selected {
|
||||
background-color: @color_border_active;
|
||||
}
|
||||
|
||||
#popup-mpd {
|
||||
color: white;
|
||||
padding: 1em;
|
||||
|
||||
/* -- launcher -- */
|
||||
|
||||
.launcher .item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#popup-mpd #album-art {
|
||||
/*border: 1px solid #424242;*/
|
||||
.launcher .item:not(.focused):hover {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.launcher .open {
|
||||
border-bottom: 1px solid @color_text;
|
||||
}
|
||||
|
||||
.launcher .focused {
|
||||
border-bottom: 2px solid @color_border_active;
|
||||
}
|
||||
|
||||
.launcher .urgent {
|
||||
border-bottom-color: @color_urgent;
|
||||
}
|
||||
|
||||
.popup-launcher {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popup-launcher .popup-item:not(:first-child) {
|
||||
border-top: 1px solid @color_border;
|
||||
}
|
||||
|
||||
|
||||
/* -- music -- */
|
||||
|
||||
.music:hover * {
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.popup-music .album-art {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#popup-mpd #title .icon, #popup-mpd #title .label {
|
||||
.popup-music .title .icon *, .popup-music .title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
#popup-mpd #controls * {
|
||||
border-radius: 0;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
.popup-music .controls *:disabled {
|
||||
color: @color_border;
|
||||
}
|
||||
|
||||
#popup-mpd #controls *:disabled {
|
||||
color: #424242;
|
||||
.popup-music .volume scale slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#focused {
|
||||
color: white;
|
||||
/* volume icon */
|
||||
.popup-music .volume > box:last-child label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
|
||||
/* -- script -- */
|
||||
|
||||
.script {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* -- sys_info -- */
|
||||
|
||||
.sysinfo {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sysinfo .item {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* -- tray -- */
|
||||
|
||||
.tray {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* -- workspaces -- */
|
||||
|
||||
.workspaces .item.focused {
|
||||
box-shadow: inset 0 -3px;
|
||||
background-color: @color_bg_dark;
|
||||
}
|
||||
|
||||
.workspaces .item:hover {
|
||||
box-shadow: inset 0 -3px;
|
||||
}
|
||||
|
||||
|
||||
/* -- custom: power menu -- */
|
||||
|
||||
.popup-power-menu #header {
|
||||
font-size: 1.4em;
|
||||
padding-bottom: 0.4em;
|
||||
margin-bottom: 0.6em;
|
||||
border-bottom: 1px solid @color_border;
|
||||
}
|
||||
|
||||
.popup-power-menu .power-btn {
|
||||
border: 1px solid @color_border;
|
||||
padding: 0.6em 1em;
|
||||
}
|
||||
|
||||
.popup-power-menu #buttons > *:nth-child(1) .power-btn {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
end = [
|
||||
{
|
||||
type = "sys_info"
|
||||
|
||||
interval.memory = 30
|
||||
interval.cpu = 1
|
||||
interval.temps = 5
|
||||
interval.disks = 300
|
||||
interval.networks = 3
|
||||
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
36
flake.lock
generated
36
flake.lock
generated
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -17,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672350804,
|
||||
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
|
||||
"lastModified": 1686960236,
|
||||
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
|
||||
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -45,11 +48,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1672453260,
|
||||
"narHash": "sha256-ruR2xo30Vn7kY2hAgg2Z2xrCvNePxck6mgR5a8u+zow=",
|
||||
"lastModified": 1686968542,
|
||||
"narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "176b6fd3dd3d7cea8d22ab1131364a050228d94c",
|
||||
"rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -57,6 +60,21 @@
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
100
flake.nix
100
flake.nix
@@ -6,9 +6,6 @@
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
#nci.url = "github:yusdacra/nix-cargo-integration";
|
||||
#nci.inputs.nixpkgs.follows = "nixpkgs";
|
||||
#nci.inputs.rust-overlay.follows = "rust-overlay";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
@@ -39,18 +36,16 @@
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
props = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
mkDate = longDate: (lib.concatStringsSep "-" [
|
||||
(builtins.substring 0 4 longDate)
|
||||
(builtins.substring 4 2 longDate)
|
||||
(builtins.substring 6 2 longDate)
|
||||
]);
|
||||
in {
|
||||
ironbar = rustPlatform.buildRustPackage {
|
||||
pname = "ironbar";
|
||||
version = self.rev or "dirty";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = prev.lib.cleanSource ./.;
|
||||
};
|
||||
cargoDeps = rustPlatform.importCargoLock {lockFile = ./Cargo.lock;};
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
nativeBuildInputs = with prev; [pkg-config];
|
||||
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon];
|
||||
ironbar = prev.callPackage ./nix/default.nix {
|
||||
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
|
||||
inherit rustPlatform;
|
||||
};
|
||||
};
|
||||
packages = genSystems (
|
||||
@@ -62,6 +57,18 @@
|
||||
default = self.packages.${system}.ironbar;
|
||||
}
|
||||
);
|
||||
apps = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
in {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
ironbar = {
|
||||
type = "app";
|
||||
program = "${pkgs.ironbar}/bin/ironbar";
|
||||
};
|
||||
});
|
||||
devShells = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
@@ -74,6 +81,7 @@
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
pkg-config
|
||||
openssl
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
@@ -94,49 +102,61 @@
|
||||
package = lib.mkOption {
|
||||
type = with lib.types; package;
|
||||
default = defaultIronbarPackage;
|
||||
description = "The package for ironbar to use";
|
||||
description = "The package for ironbar to use.";
|
||||
};
|
||||
systemd = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = pkgs.stdenv.isLinux;
|
||||
description = "Whether to enable to systemd service for ironbar";
|
||||
description = "Whether to enable to systemd service for ironbar.";
|
||||
};
|
||||
style = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
default = "";
|
||||
description = "The stylesheet to apply to ironbar";
|
||||
description = "The stylesheet to apply to ironbar.";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = jsonFormat.type;
|
||||
default = {};
|
||||
description = "The config to pass to ironbar";
|
||||
description = "The config to pass to ironbar.";
|
||||
};
|
||||
features = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.nonEmptyStr;
|
||||
default = [];
|
||||
description = "The features to be used.";
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
home.packages = [cfg.package];
|
||||
xdg.configFile = {
|
||||
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
||||
source = jsonFormat.generate "ironbar-config" cfg.config;
|
||||
config = let
|
||||
pkg = cfg.package.override {features = cfg.features;};
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
home.packages = [pkg];
|
||||
xdg.configFile = {
|
||||
"ironbar/config.json" = lib.mkIf (cfg.config != "") {
|
||||
source = jsonFormat.generate "ironbar-config" cfg.config;
|
||||
};
|
||||
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
|
||||
text = cfg.style;
|
||||
};
|
||||
};
|
||||
"ironbar/style.css" = lib.mkIf (cfg.style != "") {
|
||||
text = cfg.style;
|
||||
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
|
||||
Unit = {
|
||||
Description = "Systemd service for Ironbar";
|
||||
Requires = ["graphical-session.target"];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkg}/bin/ironbar";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
|
||||
];
|
||||
};
|
||||
};
|
||||
systemd.user.services.ironbar = lib.mkIf cfg.systemd {
|
||||
Unit = {
|
||||
Description = "Systemd service for Ironbar";
|
||||
Requires = ["graphical-session.target"];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${cfg.package}/bin/ironbar";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
nixConfig = {
|
||||
extra-substituters = ["https://jakestanger.cachix.org"];
|
||||
extra-trusted-public-keys = ["jakestanger.cachix.org-1:VWJE7AWNe5/KOEvCQRxoE8UsI2Xs2nHULJ7TEjYm7mM="];
|
||||
};
|
||||
}
|
||||
|
||||
67
nix/default.nix
Normal file
67
nix/default.nix
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
gtk3,
|
||||
gdk-pixbuf,
|
||||
librsvg,
|
||||
webp-pixbuf-loader,
|
||||
gobject-introspection,
|
||||
glib-networking,
|
||||
glib,
|
||||
shared-mime-info,
|
||||
gsettings-desktop-schemas,
|
||||
wrapGAppsHook,
|
||||
gtk-layer-shell,
|
||||
gnome,
|
||||
libxkbcommon,
|
||||
openssl,
|
||||
pkg-config,
|
||||
hicolor-icon-theme,
|
||||
rustPlatform,
|
||||
lib,
|
||||
version ? "git",
|
||||
features ? [],
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {
|
||||
lockFile = ../Cargo.lock;
|
||||
};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||
propagatedBuildInputs = [
|
||||
gtk3
|
||||
];
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# Thumbnailers
|
||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||
)
|
||||
'';
|
||||
passthru = {
|
||||
updateScript = gnome.updateScript {
|
||||
packageName = pname;
|
||||
attrPath = "gnome.${pname}";
|
||||
};
|
||||
};
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
};
|
||||
}
|
||||
5
scripts/generate-examples.sh
Executable file
5
scripts/generate-examples.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
corn examples/config.corn -t json > examples/config.json
|
||||
corn examples/config.corn -t toml > examples/config.toml
|
||||
corn examples/config.corn -t yaml > examples/config.yaml
|
||||
72
scripts/migrate-styles.sh
Executable file
72
scripts/migrate-styles.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Migrates CSS selectors from widget names to CSS classes.
|
||||
# These changed as part of the 0.12 release.
|
||||
|
||||
# ⚠ This script will **NOT** check for custom styles and may mangle them!
|
||||
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
|
||||
|
||||
style_path="$HOME/.config/ironbar/style.css"
|
||||
|
||||
# general
|
||||
sed -i 's/#icon/.icon/g' "$style_path"
|
||||
sed -i 's/#label/.label/g' "$style_path"
|
||||
sed -i 's/#image/.image/g' "$style_path"
|
||||
|
||||
# clipboard
|
||||
sed -i 's/#clipboard/.clipboard/g' "$style_path"
|
||||
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
|
||||
|
||||
# clock
|
||||
sed -i 's/#clock/.clock/g' "$style_path"
|
||||
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
|
||||
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
|
||||
sed -i 's/#calendar/.calendar/g' "$style_path"
|
||||
|
||||
# custom
|
||||
sed -i 's/#custom/.custom/g' "$style_path"
|
||||
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
|
||||
|
||||
# focused
|
||||
sed -i 's/#focused/.focused/g' "$style_path"
|
||||
|
||||
# launcher
|
||||
sed -i 's/#launcher/.launcher/g' "$style_path"
|
||||
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
|
||||
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
|
||||
|
||||
# music
|
||||
sed -i 's/#music/.music/g' "$style_path"
|
||||
sed -i 's/#contents/.contents/g' "$style_path"
|
||||
sed -i 's/#popup-music/.popup-music/g' "$style_path"
|
||||
sed -i 's/#album-art/.album-art/g' "$style_path"
|
||||
sed -i 's/#title/.title/g' "$style_path"
|
||||
sed -i 's/#album/.album/g' "$style_path"
|
||||
sed -i 's/#artist/.artist/g' "$style_path"
|
||||
sed -i 's/#controls/.controls/g' "$style_path"
|
||||
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
|
||||
sed -i 's/#btn-play/.btn-play/g' "$style_path"
|
||||
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
|
||||
sed -i 's/#btn-next/.btn-next/g' "$style_path"
|
||||
sed -i 's/#volume/.volume/g' "$style_path"
|
||||
sed -i 's/#slider/.slider/g' "$style_path"
|
||||
|
||||
# script
|
||||
sed -i 's/#script/.script/g' "$style_path"
|
||||
|
||||
# sys_info
|
||||
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
|
||||
# tray
|
||||
sed -i 's/#tray/.tray/g' "$style_path"
|
||||
|
||||
# upower
|
||||
sed -i 's/#upower/.upower/g' "$style_path"
|
||||
sed -i 's/#button/.button/g' "$style_path"
|
||||
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
|
||||
sed -i 's/#upower-details/.upower-details/g' "$style_path"
|
||||
|
||||
# workspaces
|
||||
sed -i 's/#workspaces/.workspaces/g' "$style_path"
|
||||
sed -i 's/#item/.item/g' "$style_path"
|
||||
297
src/bar.rs
297
src/bar.rs
@@ -1,18 +1,16 @@
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::config::{BarPosition, CommonConfig, ModuleConfig};
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
||||
use crate::modules::{
|
||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
||||
};
|
||||
use crate::popup::Popup;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{await_sync, read_lock, send, write_lock, Config};
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::Config;
|
||||
use color_eyre::Result;
|
||||
use gtk::gdk::{EventMask, Monitor, ScrollDirection};
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, EventBox, Orientation, Widget};
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, trace};
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Creates a new window for a bar,
|
||||
/// sets it up and adds its widgets.
|
||||
@@ -24,7 +22,13 @@ pub fn create_bar(
|
||||
) -> Result<()> {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
|
||||
setup_layer_shell(&win, monitor, config.position, config.anchor_to_edges);
|
||||
setup_layer_shell(
|
||||
&win,
|
||||
monitor,
|
||||
config.position,
|
||||
config.anchor_to_edges,
|
||||
config.margin,
|
||||
);
|
||||
|
||||
let orientation = config.position.get_orientation();
|
||||
|
||||
@@ -79,16 +83,18 @@ fn setup_layer_shell(
|
||||
monitor: &Monitor,
|
||||
position: BarPosition,
|
||||
anchor_to_edges: bool,
|
||||
margin: MarginConfig,
|
||||
) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
||||
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
|
||||
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, 0);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
|
||||
|
||||
let bar_orientation = position.get_orientation();
|
||||
|
||||
@@ -140,6 +146,11 @@ fn load_modules(
|
||||
monitor: &Monitor,
|
||||
output_name: &str,
|
||||
) -> Result<()> {
|
||||
let icon_theme = IconTheme::new();
|
||||
if let Some(ref theme) = config.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(theme));
|
||||
}
|
||||
|
||||
macro_rules! info {
|
||||
($location:expr) => {
|
||||
ModuleInfo {
|
||||
@@ -148,23 +159,28 @@ fn load_modules(
|
||||
monitor,
|
||||
output_name,
|
||||
location: $location,
|
||||
icon_theme: &icon_theme,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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 {
|
||||
let info = info!(ModuleLocation::Left);
|
||||
add_modules(left, modules, &info)?;
|
||||
add_modules(left, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
if let Some(modules) = config.center {
|
||||
let info = info!(ModuleLocation::Center);
|
||||
add_modules(center, modules, &info)?;
|
||||
add_modules(center, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
if let Some(modules) = config.end {
|
||||
let info = info!(ModuleLocation::Right);
|
||||
add_modules(right, modules, &info)?;
|
||||
add_modules(right, modules, &info, &popup)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -172,234 +188,49 @@ fn load_modules(
|
||||
|
||||
/// Adds modules into a provided GTK box,
|
||||
/// which should be one of its left, center or right containers.
|
||||
fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
|
||||
let popup = Popup::new(info);
|
||||
let popup = Arc::new(RwLock::new(popup));
|
||||
fn add_modules(
|
||||
content: >k::Box,
|
||||
modules: Vec<ModuleConfig>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<()> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
macro_rules! add_module {
|
||||
($module:expr, $id:expr) => {{
|
||||
let common = $module.common.take().expect("Common config did not exist");
|
||||
let widget = create_module($module, $id, &info, &Arc::clone(&popup))?;
|
||||
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
|
||||
set_widget_identifiers(&widget_parts, &common);
|
||||
|
||||
let container = wrap_widget(&widget);
|
||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
||||
content.add(&container);
|
||||
setup_module_common_options(container, common);
|
||||
}};
|
||||
}
|
||||
|
||||
for (id, config) in modules.into_iter().enumerate() {
|
||||
for config in modules {
|
||||
let id = get_unique_usize();
|
||||
match config {
|
||||
#[cfg(feature = "clipboard")]
|
||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "clock")]
|
||||
ModuleConfig::Clock(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Script(mut module) => add_module!(module, id),
|
||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Focused(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Launcher(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Focused(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Label(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Launcher(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "music")]
|
||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Script(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "sys_info")]
|
||||
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "tray")]
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "upower")]
|
||||
ModuleConfig::Upower(mut module) => add_module!(module, id),
|
||||
#[cfg(feature = "workspaces")]
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a module and sets it up.
|
||||
/// This setup includes widget/popup content and event channels.
|
||||
fn create_module<TModule, TWidget, TSend, TRec>(
|
||||
module: TModule,
|
||||
id: usize,
|
||||
info: &ModuleInfo,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<TWidget>
|
||||
where
|
||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||
TWidget: IsA<Widget>,
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
|
||||
let channel = BridgeChannel::<ModuleUpdateEvent<TSend>>::new();
|
||||
let (ui_tx, ui_rx) = mpsc::channel::<TRec>(16);
|
||||
|
||||
module.spawn_controller(info, channel.create_sender(), ui_rx)?;
|
||||
|
||||
let context = WidgetContext {
|
||||
id,
|
||||
widget_rx: w_rx,
|
||||
popup_rx: p_rx,
|
||||
tx: channel.create_sender(),
|
||||
controller_tx: ui_tx,
|
||||
};
|
||||
|
||||
let name = TModule::name();
|
||||
|
||||
let module_parts = module.into_widget(context, info)?;
|
||||
module_parts.widget.set_widget_name(name);
|
||||
|
||||
let mut has_popup = false;
|
||||
if let Some(popup_content) = module_parts.popup {
|
||||
register_popup_content(popup, id, popup_content);
|
||||
has_popup = true;
|
||||
}
|
||||
|
||||
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
|
||||
|
||||
Ok(module_parts.widget)
|
||||
}
|
||||
|
||||
/// Registers the popup content with the popup.
|
||||
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
|
||||
write_lock!(popup).register_content(id, popup_content);
|
||||
}
|
||||
|
||||
/// Sets up the bridge channel receiver
|
||||
/// to pick up events from the controller, widget or popup.
|
||||
///
|
||||
/// Handles opening/closing popups
|
||||
/// and communicating update messages between controllers and widgets/popups.
|
||||
fn setup_receiver<TSend>(
|
||||
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
|
||||
w_tx: glib::Sender<TSend>,
|
||||
p_tx: glib::Sender<TSend>,
|
||||
popup: Arc<RwLock<Popup>>,
|
||||
name: &'static str,
|
||||
id: usize,
|
||||
has_popup: bool,
|
||||
) where
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
channel.recv(move |ev| {
|
||||
match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
if has_popup {
|
||||
send!(p_tx, update.clone());
|
||||
}
|
||||
|
||||
send!(w_tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(geometry) => {
|
||||
debug!("Toggling popup for {} [#{}]", name, id);
|
||||
let popup = read_lock!(popup);
|
||||
if popup.is_visible() {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
popup.hide();
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
|
||||
let popup = read_lock!(popup);
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
/// Takes a widget and adds it into a new `gtk::EventBox`.
|
||||
/// The event box container is returned.
|
||||
fn wrap_widget<W: IsA<Widget>>(widget: &W) -> EventBox {
|
||||
let container = EventBox::new();
|
||||
container.add_events(EventMask::SCROLL_MASK);
|
||||
container.add(widget);
|
||||
container
|
||||
}
|
||||
|
||||
/// Configures the module's container according to the common config options.
|
||||
fn setup_module_common_options(container: EventBox, common: CommonConfig) {
|
||||
common.show_if.map_or_else(
|
||||
|| {
|
||||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
let script = Script::new_polling(show_if);
|
||||
let container = container.clone();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
spawn(async move {
|
||||
script
|
||||
.run(|(_, success)| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
});
|
||||
rx.attach(None, move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
} else {
|
||||
container.hide();
|
||||
};
|
||||
Continue(true)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let left_click_script = common.on_click_left.map(Script::new_polling);
|
||||
let middle_click_script = common.on_click_middle.map(Script::new_polling);
|
||||
let right_click_script = common.on_click_right.map(Script::new_polling);
|
||||
|
||||
container.connect_button_press_event(move |_, event| {
|
||||
let script = match event.button() {
|
||||
1 => left_click_script.as_ref(),
|
||||
2 => middle_click_script.as_ref(),
|
||||
3 => right_click_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-click script: {}", event.button());
|
||||
|
||||
match await_sync(async { script.get_output().await }) {
|
||||
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = common.on_scroll_up.map(Script::new_polling);
|
||||
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
|
||||
|
||||
container.connect_scroll_event(move |_, event| {
|
||||
println!("{:?}", event.direction());
|
||||
|
||||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-scroll script: {}", event.direction());
|
||||
|
||||
match await_sync(async { script.get_output().await }) {
|
||||
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
if let Some(tooltip) = common.tooltip {
|
||||
DynamicString::new(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::send;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// MPSC async -> sync channel.
|
||||
/// MPSC async -> GTK sync channel.
|
||||
/// The sender uses `tokio::sync::mpsc`
|
||||
/// while the receiver uses `glib::MainContext::channel`.
|
||||
///
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
239
src/clients/clipboard.rs
Normal file
239
src/clients/clipboard.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use super::wayland::{self, ClipboardItem};
|
||||
use crate::{lock, try_send};
|
||||
use indexmap::map::Iter;
|
||||
use indexmap::IndexMap;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClipboardEvent {
|
||||
Add(Arc<ClipboardItem>),
|
||||
Remove(usize),
|
||||
Activate(usize),
|
||||
}
|
||||
|
||||
type EventSender = mpsc::Sender<ClipboardEvent>;
|
||||
|
||||
/// Clipboard client singleton,
|
||||
/// to ensure bars don't duplicate requests to the compositor.
|
||||
pub struct ClipboardClient {
|
||||
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
|
||||
cache: Arc<Mutex<ClipboardCache>>,
|
||||
}
|
||||
|
||||
impl ClipboardClient {
|
||||
fn new() -> Self {
|
||||
trace!("Initializing clipboard client");
|
||||
|
||||
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
|
||||
|
||||
let cache = Arc::new(Mutex::new(ClipboardCache::new()));
|
||||
|
||||
{
|
||||
let senders = senders.clone();
|
||||
let cache = cache.clone();
|
||||
|
||||
spawn(async move {
|
||||
let (mut rx, item) = {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.subscribe_clipboard()
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
|
||||
lock!(cache).insert(item, senders.len());
|
||||
}
|
||||
|
||||
while let Ok(item) = rx.recv().await {
|
||||
debug!("Received clipboard item (ID: {})", item.id);
|
||||
|
||||
let (existing_id, cache_size) = {
|
||||
let cache = lock!(cache);
|
||||
(cache.contains(&item), cache.len())
|
||||
};
|
||||
|
||||
existing_id.map_or_else(
|
||||
|| {
|
||||
{
|
||||
let mut cache = lock!(cache);
|
||||
let senders = lock!(senders);
|
||||
cache.insert(item.clone(), senders.len());
|
||||
}
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, sender_cache_size) in iter {
|
||||
if cache_size == *sender_cache_size {
|
||||
let removed_id = lock!(cache)
|
||||
.remove_ref_first()
|
||||
.expect("Clipboard cache unexpectedly empty");
|
||||
try_send!(tx, ClipboardEvent::Remove(removed_id));
|
||||
}
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
},
|
||||
|existing_id| {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(existing_id));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self { senders, cache }
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
|
||||
{
|
||||
let cache = lock!(self.cache);
|
||||
|
||||
let iter = cache.iter();
|
||||
for (_, (item, _)) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
lock!(self.senders).push((tx, cache_size));
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn copy(&self, id: usize) {
|
||||
debug!("Copying item with id {id}");
|
||||
|
||||
let item = {
|
||||
let cache = lock!(self.cache);
|
||||
cache.get(id)
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let wl = wayland::get_client();
|
||||
let wl = lock!(wl);
|
||||
wl.copy_to_clipboard(item);
|
||||
}
|
||||
|
||||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: usize) {
|
||||
lock!(self.cache).remove(id);
|
||||
|
||||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Remove(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared clipboard item cache.
|
||||
///
|
||||
/// Items are stored with a number of references,
|
||||
/// allowing different consumers to 'remove' cached items
|
||||
/// at different times.
|
||||
#[derive(Debug)]
|
||||
struct ClipboardCache {
|
||||
cache: IndexMap<usize, (Arc<ClipboardItem>, usize)>,
|
||||
}
|
||||
|
||||
impl ClipboardCache {
|
||||
/// Creates a new empty cache.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cache: IndexMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the entry with key `id` from the cache.
|
||||
fn get(&self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache.get(&id).map(|(item, _)| item).cloned()
|
||||
}
|
||||
|
||||
/// Inserts an entry with `ref_count` initial references.
|
||||
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache
|
||||
.insert(item.id, (item, ref_count))
|
||||
.map(|(item, _)| item)
|
||||
}
|
||||
|
||||
/// Removes the entry with key `id`.
|
||||
/// This ignores references.
|
||||
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {
|
||||
self.cache.shift_remove(&id).map(|(item, _)| item)
|
||||
}
|
||||
|
||||
/// Removes a reference to the entry with key `id`.
|
||||
///
|
||||
/// If the reference count reaches zero, the entry
|
||||
/// is removed from the cache.
|
||||
fn remove_ref(&mut self, id: usize) {
|
||||
if let Some(entry) = self.cache.get_mut(&id) {
|
||||
entry.1 -= 1;
|
||||
|
||||
if entry.1 == 0 {
|
||||
self.cache.shift_remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a reference to the first entry.
|
||||
///
|
||||
/// If the reference count reaches zero, the entry
|
||||
/// is removed from the cache.
|
||||
fn remove_ref_first(&mut self) -> Option<usize> {
|
||||
if let Some((id, _)) = self.cache.first() {
|
||||
let id = *id;
|
||||
self.remove_ref(id);
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an item with matching mime type and value
|
||||
/// already exists in the cache.
|
||||
fn contains(&self, item: &ClipboardItem) -> Option<usize> {
|
||||
self.cache.values().find_map(|(it, _)| {
|
||||
if it.mime_type == item.mime_type && it.value == item.value {
|
||||
Some(it.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the current number of items in the cache.
|
||||
fn len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
fn iter(&self) -> Iter<'_, usize, (Arc<ClipboardItem>, usize)> {
|
||||
self.cache.iter()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: ClipboardClient = ClipboardClient::new();
|
||||
}
|
||||
|
||||
pub fn get_client() -> &'static ClipboardClient {
|
||||
&CLIENT
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::error::{ERR_CHANNEL_SEND, ERR_MUTEX_LOCK};
|
||||
use crate::{lock, send};
|
||||
use color_eyre::Result;
|
||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
use hyprland::event_listener::EventListenerMutable as EventListener;
|
||||
@@ -9,10 +10,9 @@ use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{error, info};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub struct EventClient {
|
||||
workspaces: Arc<Mutex<Vec<Workspace>>>,
|
||||
workspace_tx: Sender<WorkspaceUpdate>,
|
||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||
}
|
||||
@@ -21,12 +21,7 @@ impl EventClient {
|
||||
fn new() -> Self {
|
||||
let (workspace_tx, workspace_rx) = channel(16);
|
||||
|
||||
let workspaces = Arc::new(Mutex::new(vec![]));
|
||||
// load initial list
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
|
||||
Self {
|
||||
workspaces,
|
||||
workspace_tx,
|
||||
_workspace_rx: workspace_rx,
|
||||
}
|
||||
@@ -35,164 +30,198 @@ impl EventClient {
|
||||
fn listen_workspace_events(&self) {
|
||||
info!("Starting Hyprland event listener");
|
||||
|
||||
let workspaces = self.workspaces.clone();
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let mut event_listener = EventListener::new();
|
||||
|
||||
// we need a lock to ensure events don't run at the same time
|
||||
let lock = Arc::new(Mutex::new(()));
|
||||
|
||||
// cache the active workspace since Hyprland doesn't give us the prev active
|
||||
let active = Self::get_active_workspace().expect("Failed to get active workspace");
|
||||
let active = Arc::new(Mutex::new(Some(active)));
|
||||
|
||||
{
|
||||
let workspaces = workspaces.clone();
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_added_handler(move |workspace_type, _state| {
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
let _lock = lock!(lock);
|
||||
debug!("Added workspace: {workspace_type:?}");
|
||||
|
||||
let workspace = Self::get_workspace(&workspaces, workspace_type);
|
||||
workspace.map_or_else(
|
||||
|| error!("Unable to locate workspace"),
|
||||
|workspace| {
|
||||
tx.send(WorkspaceUpdate::Add(workspace))
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
},
|
||||
);
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let prev_workspace = lock!(active);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let workspaces = workspaces.clone();
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_change_handler(move |workspace_type, _state| {
|
||||
let prev_workspace = Self::get_focused_workspace(&workspaces);
|
||||
let _lock = lock!(lock);
|
||||
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace = Self::get_workspace(&workspaces, workspace_type);
|
||||
|
||||
if let (Some(prev_workspace), Some(workspace)) = (prev_workspace, workspace) {
|
||||
if prev_workspace.id != workspace.id {
|
||||
tx.send(WorkspaceUpdate::Focus {
|
||||
old: prev_workspace,
|
||||
new: workspace.clone(),
|
||||
})
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
}
|
||||
|
||||
// there may be another type of update so dispatch that regardless of focus change
|
||||
tx.send(WorkspaceUpdate::Update(workspace))
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
} else {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let workspaces = workspaces.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
|
||||
let workspace = Self::get_workspace(&workspaces, workspace_type);
|
||||
workspace.map_or_else(
|
||||
|| error!("Unable to locate workspace"),
|
||||
|workspace| {
|
||||
tx.send(WorkspaceUpdate::Remove(workspace))
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
},
|
||||
debug!(
|
||||
"Received workspace change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.id)
|
||||
);
|
||||
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
});
|
||||
}
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
|
||||
{
|
||||
let workspaces = workspaces.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data, _state| {
|
||||
let workspace_type = event_data.1;
|
||||
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
|
||||
let workspace = Self::get_workspace(&workspaces, workspace_type);
|
||||
workspace.map_or_else(
|
||||
|| error!("Unable to locate workspace"),
|
||||
|| {
|
||||
error!("Unable to locate workspace");
|
||||
},
|
||||
|workspace| {
|
||||
tx.send(WorkspaceUpdate::Move(workspace))
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
// there may be another type of update so dispatch that regardless of focus change
|
||||
send!(tx, WorkspaceUpdate::Update(workspace.clone()));
|
||||
if !focused {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let workspaces = workspaces.clone();
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_active_monitor_change_handler(move |event_data, _state| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.1;
|
||||
|
||||
let prev_workspace = Self::get_focused_workspace(&workspaces);
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
debug!(
|
||||
"Received active monitor change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.name)
|
||||
);
|
||||
|
||||
let workspace = Self::get_workspace(&workspaces, workspace_type);
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
|
||||
if let (Some(prev_workspace), Some(workspace)) = (prev_workspace, workspace) {
|
||||
if prev_workspace.id != workspace.id {
|
||||
tx.send(WorkspaceUpdate::Focus {
|
||||
old: prev_workspace,
|
||||
new: workspace,
|
||||
})
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
}
|
||||
if let (Some(workspace), false) = (workspace, focused) {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
} else {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data, _state| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.1;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||
|
||||
if !focused {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace destroy: {workspace_type:?}");
|
||||
|
||||
let name = get_workspace_name(workspace_type);
|
||||
send!(tx, WorkspaceUpdate::Remove(name));
|
||||
});
|
||||
}
|
||||
|
||||
event_listener
|
||||
.start_listener()
|
||||
.expect("Failed to start listener");
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_workspaces(workspaces: &Mutex<Vec<Workspace>>) {
|
||||
let mut workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
|
||||
/// Sends a `WorkspaceUpdate::Focus` event
|
||||
/// and updates the active workspace cache.
|
||||
fn send_focus_change(
|
||||
prev_workspace: &mut Option<Workspace>,
|
||||
workspace: Workspace,
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
) {
|
||||
let old = prev_workspace
|
||||
.as_ref()
|
||||
.map(|w| w.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let active = HWorkspace::get_active().expect("Failed to get active workspace");
|
||||
let new_workspaces = Workspaces::get()
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Focus {
|
||||
old,
|
||||
new: workspace.name.clone(),
|
||||
}
|
||||
);
|
||||
|
||||
prev_workspace.replace(workspace);
|
||||
}
|
||||
|
||||
/// Gets a workspace by name from the server.
|
||||
///
|
||||
/// Use `focused` to manually mark the workspace as focused,
|
||||
/// as this is not automatically checked.
|
||||
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
|
||||
Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.collect()
|
||||
.into_iter()
|
||||
.map(|workspace| Workspace::from((workspace.id == active.id, workspace)));
|
||||
|
||||
workspaces.clear();
|
||||
workspaces.extend(new_workspaces);
|
||||
.find_map(|w| {
|
||||
if w.name == name {
|
||||
Some(Workspace::from((focused, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_workspace(workspaces: &Mutex<Vec<Workspace>>, id: WorkspaceType) -> Option<Workspace> {
|
||||
let id_string = id_to_string(id);
|
||||
|
||||
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
|
||||
workspaces
|
||||
.iter()
|
||||
.find(|workspace| workspace.id == id_string)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn get_focused_workspace(workspaces: &Mutex<Vec<Workspace>>) -> Option<Workspace> {
|
||||
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
|
||||
workspaces
|
||||
.iter()
|
||||
.find(|workspace| workspace.focused)
|
||||
.cloned()
|
||||
/// Gets the active workspace from the server.
|
||||
fn get_active_workspace() -> Result<Workspace> {
|
||||
let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
|
||||
Ok(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for EventClient {
|
||||
fn focus(&self, id: String) -> color_eyre::Result<()> {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
Dispatch::call(DispatchType::Workspace(
|
||||
WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
))?;
|
||||
@@ -205,13 +234,16 @@ impl WorkspaceClient for EventClient {
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
let workspaces = self.workspaces.clone();
|
||||
Self::refresh_workspaces(&workspaces);
|
||||
let active_name = HWorkspace::get_active()
|
||||
.map(|active| active.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
let workspaces = workspaces.lock().expect(ERR_MUTEX_LOCK);
|
||||
let workspaces = Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.map(|w| Workspace::from((w.name == active_name, w)))
|
||||
.collect();
|
||||
|
||||
tx.send(WorkspaceUpdate::Init(workspaces.clone()))
|
||||
.expect(ERR_CHANNEL_SEND);
|
||||
send!(tx, WorkspaceUpdate::Init(workspaces));
|
||||
}
|
||||
|
||||
rx
|
||||
@@ -230,10 +262,9 @@ pub fn get_client() -> &'static EventClient {
|
||||
&CLIENT
|
||||
}
|
||||
|
||||
fn id_to_string(id: WorkspaceType) -> String {
|
||||
match id {
|
||||
WorkspaceType::Unnamed(id) => id.to_string(),
|
||||
WorkspaceType::Named(name) => name,
|
||||
fn get_workspace_name(name: WorkspaceType) -> String {
|
||||
match name {
|
||||
WorkspaceType::Regular(name) => name,
|
||||
WorkspaceType::Special(name) => name.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
@@ -241,7 +272,7 @@ fn id_to_string(id: WorkspaceType) -> String {
|
||||
impl From<(bool, hyprland::data::Workspace)> for Workspace {
|
||||
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
|
||||
Self {
|
||||
id: id_to_string(workspace.id),
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.monitor,
|
||||
focused,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::debug;
|
||||
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
pub mod hyprland;
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
pub mod sway;
|
||||
|
||||
pub enum Compositor {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Sway,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Hyprland,
|
||||
Unsupported,
|
||||
}
|
||||
@@ -18,7 +23,9 @@ impl Display for Compositor {
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Self::Sway => "Sway",
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::Hyprland => "Hyprland",
|
||||
Self::Unsupported => "Unsupported",
|
||||
}
|
||||
@@ -31,9 +38,15 @@ impl Compositor {
|
||||
/// This is done by checking system env vars.
|
||||
fn get_current() -> Self {
|
||||
if std::env::var("SWAYSOCK").is_ok() {
|
||||
Self::Sway
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+sway")] { Self::Sway }
|
||||
else { tracing::error!("Not compiled with Sway support"); Self::Unsupported }
|
||||
}
|
||||
} else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
|
||||
Self::Hyprland
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland}
|
||||
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
|
||||
}
|
||||
} else {
|
||||
Self::Unsupported
|
||||
}
|
||||
@@ -44,7 +57,9 @@ impl Compositor {
|
||||
let current = Self::get_current();
|
||||
debug!("Getting workspace client for: {current}");
|
||||
match current {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Self::Sway => Ok(sway::get_sub_client()),
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::Hyprland => Ok(hyprland::get_client()),
|
||||
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
||||
.note("Currently workspaces are only supported by Sway and Hyprland")),
|
||||
@@ -70,13 +85,13 @@ pub enum WorkspaceUpdate {
|
||||
/// This is re-sent to all subscribers when a new subscription is created.
|
||||
Init(Vec<Workspace>),
|
||||
Add(Workspace),
|
||||
Remove(Workspace),
|
||||
Remove(String),
|
||||
Update(Workspace),
|
||||
Move(Workspace),
|
||||
/// Declares focus moved from the old workspace to the new.
|
||||
Focus {
|
||||
old: Workspace,
|
||||
new: Workspace,
|
||||
old: String,
|
||||
new: String,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::await_sync;
|
||||
use crate::error::ERR_CHANNEL_SEND;
|
||||
use crate::{await_sync, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use futures_util::StreamExt;
|
||||
@@ -75,7 +74,7 @@ impl WorkspaceClient for SwayEventClient {
|
||||
let event =
|
||||
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
|
||||
|
||||
tx.send(event).expect(ERR_CHANNEL_SEND);
|
||||
send!(tx, event);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,12 +131,24 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
|
||||
WorkspaceChange::Init => {
|
||||
Self::Add(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
WorkspaceChange::Empty => {
|
||||
Self::Remove(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
WorkspaceChange::Empty => Self::Remove(
|
||||
event
|
||||
.current
|
||||
.expect("Missing current workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
WorkspaceChange::Focus => Self::Focus {
|
||||
old: event.old.expect("Missing old workspace").into(),
|
||||
new: event.current.expect("Missing current workspace").into(),
|
||||
old: event
|
||||
.old
|
||||
.expect("Missing old workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
new: event
|
||||
.current
|
||||
.expect("Missing current workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
WorkspaceChange::Move => {
|
||||
Self::Move(event.current.expect("Missing current workspace").into())
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub mod clipboard;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod compositor;
|
||||
#[cfg(feature = "music")]
|
||||
pub mod music;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod system_tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
pub mod wayland;
|
||||
|
||||
@@ -4,7 +4,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
#[cfg(feature = "music+mpd")]
|
||||
pub mod mpd;
|
||||
#[cfg(feature = "music+mpris")]
|
||||
pub mod mpris;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -22,7 +24,7 @@ pub struct Track {
|
||||
pub disc: Option<u64>,
|
||||
pub genre: Option<String>,
|
||||
pub track: Option<u64>,
|
||||
pub cover_path: Option<PathBuf>,
|
||||
pub cover_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -121,12 +121,16 @@ impl MpdClient {
|
||||
fn convert_song(song: &Song, music_dir: &Path) -> Track {
|
||||
let (track, disc) = song.number();
|
||||
|
||||
let cover_path = music_dir.join(
|
||||
song.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
);
|
||||
let cover_path = music_dir
|
||||
.join(
|
||||
song.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok();
|
||||
|
||||
Track {
|
||||
title: song.title().map(std::string::ToString::to_string),
|
||||
@@ -136,7 +140,7 @@ impl MpdClient {
|
||||
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
|
||||
disc: Some(disc),
|
||||
track: Some(track),
|
||||
cover_path: Some(cover_path),
|
||||
cover_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{MusicClient, PlayerUpdate, Status, Track};
|
||||
use crate::clients::music::PlayerState;
|
||||
use crate::error::ERR_MUTEX_LOCK;
|
||||
use crate::{lock, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::string;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
@@ -44,7 +44,7 @@ impl Client {
|
||||
.find_all()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
let mut players_list_val = players_list.lock().expect(ERR_MUTEX_LOCK);
|
||||
let mut players_list_val = lock!(players_list);
|
||||
for player in players {
|
||||
let identity = player.identity();
|
||||
|
||||
@@ -57,8 +57,7 @@ impl Client {
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
{
|
||||
let mut current_player =
|
||||
current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
let mut current_player = lock!(current_player);
|
||||
|
||||
if status == PlaybackStatus::Playing || current_player.is_none() {
|
||||
debug!("Setting active player to '{identity}'");
|
||||
@@ -108,22 +107,19 @@ impl Client {
|
||||
trace!("Received player event from '{identity}': {event:?}");
|
||||
match event {
|
||||
Ok(Event::PlayerShutDown) => {
|
||||
current_player.lock().expect(ERR_MUTEX_LOCK).take();
|
||||
players.lock().expect(ERR_MUTEX_LOCK).remove(identity);
|
||||
lock!(current_player).take();
|
||||
lock!(players).remove(identity);
|
||||
break;
|
||||
}
|
||||
Ok(Event::Playing) => {
|
||||
current_player
|
||||
.lock()
|
||||
.expect(ERR_MUTEX_LOCK)
|
||||
.replace(identity.to_string());
|
||||
lock!(current_player).replace(identity.to_string());
|
||||
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
let current_player = current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
let current_player = lock!(current_player);
|
||||
let current_player = current_player.as_ref();
|
||||
if let Some(current_player) = current_player {
|
||||
if current_player == identity {
|
||||
@@ -171,15 +167,13 @@ impl Client {
|
||||
let track = Track::from(metadata);
|
||||
|
||||
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
|
||||
|
||||
tx.send(player_update)
|
||||
.expect("Failed to send player update");
|
||||
send!(tx, player_update);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_player(&self) -> Option<Player> {
|
||||
let player_name = self.current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
let player_name = lock!(self.current_player);
|
||||
let player_name = player_name.as_ref();
|
||||
|
||||
player_name.and_then(|player_name| {
|
||||
@@ -237,6 +231,16 @@ impl MusicClient for Client {
|
||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else {
|
||||
let status = Status {
|
||||
playlist_position: 0,
|
||||
playlist_length: 0,
|
||||
state: PlayerState::Stopped,
|
||||
elapsed: None,
|
||||
duration: None,
|
||||
volume_percent: 0,
|
||||
};
|
||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||
}
|
||||
|
||||
rx
|
||||
@@ -266,10 +270,7 @@ impl From<Metadata> for Track {
|
||||
.and_then(mpris::MetadataValue::as_str_array)
|
||||
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
||||
track: value.track_number().map(|track| track as u64),
|
||||
cover_path: value
|
||||
.art_url()
|
||||
.map(|path| path.replace("file://", ""))
|
||||
.map(PathBuf::from),
|
||||
cover_path: value.art_url().map(string::ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,75 @@
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{lock, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use stray::message::menu::TrayMenu;
|
||||
use stray::message::tray::StatusNotifierItem;
|
||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use stray::StatusNotifierWatcher;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
|
||||
|
||||
pub struct TrayEventReceiver {
|
||||
tx: mpsc::Sender<NotifierItemCommand>,
|
||||
b_tx: broadcast::Sender<NotifierItemMessage>,
|
||||
_b_rx: broadcast::Receiver<NotifierItemMessage>,
|
||||
|
||||
tray: Arc<Mutex<Tray>>,
|
||||
}
|
||||
|
||||
impl TrayEventReceiver {
|
||||
async fn new() -> stray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", get_unique_usize());
|
||||
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let (b_tx, b_rx) = broadcast::channel(16);
|
||||
|
||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||
let mut host = tray.create_notifier_host("ironbar").await?;
|
||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
||||
|
||||
let b_tx2 = b_tx.clone();
|
||||
spawn(async move {
|
||||
while let Ok(message) = host.recv().await {
|
||||
b_tx2.send(message)?;
|
||||
}
|
||||
let tray = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
|
||||
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
|
||||
});
|
||||
{
|
||||
let b_tx = b_tx.clone();
|
||||
let tray = tray.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Ok(message) = host.recv().await {
|
||||
trace!("Received message: {message:?} ");
|
||||
|
||||
send!(b_tx, message.clone());
|
||||
let mut tray = lock!(tray);
|
||||
match message {
|
||||
NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
} => {
|
||||
debug!("Adding item with address '{address}'");
|
||||
tray.insert(address, (item, menu));
|
||||
}
|
||||
NotifierItemMessage::Remove { address } => {
|
||||
debug!("Removing item with address '{address}'");
|
||||
tray.remove(&address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tx,
|
||||
b_tx,
|
||||
_b_rx: b_rx,
|
||||
tray,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +79,20 @@ impl TrayEventReceiver {
|
||||
mpsc::Sender<NotifierItemCommand>,
|
||||
broadcast::Receiver<NotifierItemMessage>,
|
||||
) {
|
||||
(self.tx.clone(), self.b_tx.subscribe())
|
||||
let tx = self.tx.clone();
|
||||
let b_rx = self.b_tx.subscribe();
|
||||
|
||||
let tray = lock!(self.tray).clone();
|
||||
for (address, (item, menu)) in tray {
|
||||
let update = NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
};
|
||||
send!(self.b_tx, update);
|
||||
}
|
||||
|
||||
(tx, b_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +106,16 @@ lazy_static! {
|
||||
let value = loop {
|
||||
retries += 1;
|
||||
|
||||
let tray = TrayEventReceiver::new().await;
|
||||
let tray = Box::pin(TrayEventReceiver::new()).await;
|
||||
|
||||
if tray.is_ok() || retries == MAX_RETRIES {
|
||||
break tray;
|
||||
match tray {
|
||||
Ok(tray) => break Some(tray),
|
||||
Err(err) => error!("{:?}", Report::new(err).wrap_err(format!("Failed to create StatusNotifierWatcher (attempt {retries})")))
|
||||
}
|
||||
|
||||
debug!("Failed to create StatusNotifierWatcher (attempt {retries})");
|
||||
if retries == MAX_RETRIES {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
value.expect("Failed to create StatusNotifierWatcher")
|
||||
|
||||
40
src/clients/upower.rs
Normal file
40
src/clients/upower.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use async_once::AsyncOnce;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use upower_dbus::UPowerProxy;
|
||||
use zbus::fdo::PropertiesProxy;
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||
let dbus = Box::pin(zbus::Connection::system())
|
||||
.await
|
||||
.expect("failed to create connection to system bus");
|
||||
|
||||
let device_proxy = UPowerProxy::new(&dbus)
|
||||
.await
|
||||
.expect("failed to create upower proxy");
|
||||
|
||||
let display_device = device_proxy
|
||||
.get_display_device()
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
|
||||
|
||||
let path = display_device.path().to_owned();
|
||||
|
||||
let proxy = PropertiesProxy::builder(&dbus)
|
||||
.destination("org.freedesktop.UPower")
|
||||
.expect("failed to set proxy destination address")
|
||||
.path(path)
|
||||
.expect("failed to set proxy path")
|
||||
.cache_properties(zbus::CacheProperties::No)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to build proxy");
|
||||
|
||||
Arc::new(proxy)
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
|
||||
DISPLAY_PROXY.get().await
|
||||
}
|
||||
@@ -1,87 +1,192 @@
|
||||
use super::toplevel::{ToplevelEvent, ToplevelInfo};
|
||||
use super::toplevel_manager::listen_for_toplevels;
|
||||
use super::ToplevelChange;
|
||||
use super::{Env, ToplevelHandler};
|
||||
use crate::{error as err, send, write_lock};
|
||||
use super::wlr_foreign_toplevel::handle::ToplevelHandle;
|
||||
use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use super::wlr_foreign_toplevel::ToplevelEvent;
|
||||
use super::Environment;
|
||||
use crate::error::ERR_CHANNEL_RECV;
|
||||
use crate::send;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Report;
|
||||
use indexmap::IndexMap;
|
||||
use smithay_client_toolkit::environment::Environment;
|
||||
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||
use smithay_client_toolkit::reexports::calloop;
|
||||
use smithay_client_toolkit::{new_default_environment, WaylandSource};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
use smithay_client_toolkit::registry::RegistryState;
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{error, trace};
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::globals::registry_queue_init;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
||||
};
|
||||
use wayland_client::{Connection, WaylandSource};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
use super::ClipboardItem;
|
||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
/// Sends a request for all the outputs.
|
||||
/// These are then sent on the `output` channel.
|
||||
Outputs,
|
||||
/// Sends a request for all the seats.
|
||||
/// These are then sent ont the `seat` channel.
|
||||
Seats,
|
||||
/// Sends a request for all the toplevels.
|
||||
/// These are then sent on the `toplevel_init` channel.
|
||||
Toplevels,
|
||||
/// Sends a request for the current clipboard item.
|
||||
/// This is then sent on the `clipboard_init` channel.
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard,
|
||||
/// Copies the value to the clipboard
|
||||
#[cfg(feature = "clipboard")]
|
||||
CopyToClipboard(Arc<ClipboardItem>),
|
||||
/// Forces a dispatch, flushing any currently queued events
|
||||
Roundtrip,
|
||||
}
|
||||
|
||||
pub struct WaylandClient {
|
||||
pub outputs: Vec<OutputInfo>,
|
||||
pub seats: Vec<WlSeat>,
|
||||
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||
// External channels
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
|
||||
// Internal channels
|
||||
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
|
||||
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
|
||||
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
|
||||
|
||||
request_tx: Sender<Request>,
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(super) async fn new() -> Self {
|
||||
let (output_tx, output_rx) = oneshot::channel();
|
||||
let (seat_tx, seat_rx) = oneshot::channel();
|
||||
|
||||
pub(super) fn new() -> Self {
|
||||
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
|
||||
|
||||
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
|
||||
#[cfg(feature = "clipboard")]
|
||||
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
|
||||
let (output_tx, output_rx) = mpsc::channel();
|
||||
let (seat_tx, seat_rx) = mpsc::channel();
|
||||
|
||||
let toplevel_tx2 = toplevel_tx.clone();
|
||||
|
||||
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
||||
let toplevels2 = toplevels.clone();
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
|
||||
let clipboard_tx2 = clipboard_tx.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// `queue` is not send so we need to handle everything inside the task
|
||||
let (ev_tx, ev_rx) = channel::<Request>();
|
||||
|
||||
// `queue` is not `Send` so we need to handle everything inside the task
|
||||
spawn_blocking(move || {
|
||||
let (env, _display, queue) =
|
||||
new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()])
|
||||
.expect("Failed to connect to Wayland compositor");
|
||||
let toplevel_tx = toplevel_tx2;
|
||||
#[cfg(feature = "clipboard")]
|
||||
let clipboard_tx = clipboard_tx2;
|
||||
|
||||
let outputs = Self::get_outputs(&env);
|
||||
send!(output_tx, outputs);
|
||||
|
||||
let seats = env.get_all_seats();
|
||||
send!(
|
||||
seat_tx,
|
||||
seats
|
||||
.into_iter()
|
||||
.map(|seat| seat.detach())
|
||||
.collect::<Vec<WlSeat>>()
|
||||
);
|
||||
|
||||
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
|
||||
|
||||
let _listener = listen_for_toplevels(env, move |handle, event, _ddata| {
|
||||
trace!("Received toplevel event: {:?}", event);
|
||||
|
||||
if event.change == ToplevelChange::Close {
|
||||
write_lock!(toplevels2).remove(&event.toplevel.id);
|
||||
} else {
|
||||
write_lock!(toplevels2)
|
||||
.insert(event.toplevel.id, (event.toplevel.clone(), handle));
|
||||
}
|
||||
|
||||
send!(toplevel_tx2, event);
|
||||
});
|
||||
let conn =
|
||||
Connection::connect_to_env().expect("Failed to connect to Wayland compositor");
|
||||
let (globals, queue) =
|
||||
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
|
||||
|
||||
let qh = queue.handle();
|
||||
let mut event_loop =
|
||||
calloop::EventLoop::<()>::try_new().expect("Failed to create new event loop");
|
||||
EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
|
||||
|
||||
WaylandSource::new(queue)
|
||||
.quick_insert(event_loop.handle())
|
||||
.expect("Failed to insert event loop into wayland event queue");
|
||||
.expect("Failed to create Wayland source from queue")
|
||||
.insert(event_loop.handle())
|
||||
.expect("Failed to insert Wayland event queue into event loop");
|
||||
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
// Initialize the registry handling
|
||||
// so other parts of Smithay's client toolkit may bind globals.
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
|
||||
let output_delegate = OutputState::new(&globals, &qh);
|
||||
let seat_delegate = SeatState::new(&globals, &qh);
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
let data_control_device_manager_delegate =
|
||||
DataControlDeviceManagerState::bind(&globals, &qh)
|
||||
.expect("data device manager is not available");
|
||||
|
||||
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
|
||||
.expect("foreign toplevel manager is not available");
|
||||
|
||||
let mut env = Environment {
|
||||
registry_state,
|
||||
output_state: output_delegate,
|
||||
seat_state: seat_delegate,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_device_manager_state: data_control_device_manager_delegate,
|
||||
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
|
||||
seats: vec![],
|
||||
handles: HashMap::new(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc::new(Mutex::new(None)),
|
||||
toplevel_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_devices: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
selection_offers: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
copy_paste_sources: vec![],
|
||||
loop_handle: event_loop.handle(),
|
||||
};
|
||||
|
||||
loop_handle
|
||||
.insert_source(ev_rx, move |event, _metadata, env| {
|
||||
trace!("{event:?}");
|
||||
match event {
|
||||
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"),
|
||||
Event::Msg(Request::Outputs) => {
|
||||
trace!("Received get outputs request");
|
||||
|
||||
send!(output_tx, env.output_info());
|
||||
}
|
||||
Event::Msg(Request::Seats) => {
|
||||
trace!("Receive get seats request");
|
||||
send!(seat_tx, env.seats.clone());
|
||||
}
|
||||
Event::Msg(Request::Toplevels) => {
|
||||
trace!("Receive get toplevels request");
|
||||
send!(toplevel_init_tx, env.handles.clone());
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::Clipboard) => {
|
||||
trace!("Receive get clipboard requests");
|
||||
let clipboard = lock!(env.clipboard).clone();
|
||||
send!(clipboard_init_tx, clipboard);
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Msg(Request::CopyToClipboard(value)) => {
|
||||
env.copy_to_clipboard(value, &qh);
|
||||
}
|
||||
Event::Closed => panic!("Channel unexpectedly closed"),
|
||||
}
|
||||
})
|
||||
.expect("Failed to insert channel into event queue");
|
||||
|
||||
loop {
|
||||
// TODO: Avoid need for duration here - can we force some event when sending requests?
|
||||
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
|
||||
trace!("Dispatching event loop");
|
||||
if let Err(err) = event_loop.dispatch(None, &mut env) {
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||
@@ -90,29 +195,76 @@ impl WaylandClient {
|
||||
}
|
||||
});
|
||||
|
||||
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||
|
||||
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
|
||||
|
||||
Self {
|
||||
outputs,
|
||||
seats,
|
||||
toplevels,
|
||||
toplevel_tx,
|
||||
_toplevel_rx: toplevel_rx,
|
||||
toplevel_init_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_init_rx,
|
||||
output_rx,
|
||||
seat_rx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
_clipboard_rx: clipboard_rx,
|
||||
request_tx: ev_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
|
||||
self.toplevel_tx.subscribe()
|
||||
pub fn subscribe_toplevels(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<ToplevelEvent>,
|
||||
HashMap<usize, ToplevelHandle>,
|
||||
) {
|
||||
let rx = self.toplevel_tx.subscribe();
|
||||
|
||||
let receiver = &self.toplevel_init_rx;
|
||||
send!(self.request_tx, Request::Toplevels);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
|
||||
let outputs = env.get_all_outputs();
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn subscribe_clipboard(
|
||||
&self,
|
||||
) -> (
|
||||
broadcast::Receiver<Arc<ClipboardItem>>,
|
||||
Option<Arc<ClipboardItem>>,
|
||||
) {
|
||||
let rx = self.clipboard_tx.subscribe();
|
||||
|
||||
outputs
|
||||
.iter()
|
||||
.filter_map(|output| with_output_info(output, Clone::clone))
|
||||
.collect()
|
||||
let receiver = &self.clipboard_init_rx;
|
||||
send!(self.request_tx, Request::Clipboard);
|
||||
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
|
||||
|
||||
(rx, data)
|
||||
}
|
||||
|
||||
/// Force a roundtrip on the wayland connection,
|
||||
/// flushing any queued events and immediately receiving any new ones.
|
||||
pub fn roundtrip(&self) {
|
||||
trace!("Sending roundtrip request");
|
||||
send!(self.request_tx, Request::Roundtrip);
|
||||
}
|
||||
|
||||
pub fn get_outputs(&self) -> Vec<OutputInfo> {
|
||||
trace!("Sending get outputs request");
|
||||
|
||||
send!(self.request_tx, Request::Outputs);
|
||||
self.output_rx.recv().expect(ERR_CHANNEL_RECV)
|
||||
}
|
||||
|
||||
pub fn get_seats(&self) -> Vec<WlSeat> {
|
||||
trace!("Sending get seats request");
|
||||
|
||||
send!(self.request_tx, Request::Seats);
|
||||
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
||||
send!(self.request_tx, Request::CopyToClipboard(item));
|
||||
}
|
||||
}
|
||||
|
||||
101
src/clients/wayland/macros.rs
Normal file
101
src/clients/wayland/macros.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
/// It is necessary to store macros in a separate file due to a compilation error.
|
||||
/// I believe this stems from the feature flags.
|
||||
/// Related issue: <https://github.com/rust-lang/rust/issues/81066>
|
||||
|
||||
// --- Data Control Device --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_device {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_offer {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_data_control_source {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
|
||||
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Foreign Toplevel --- \\
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_manager {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel_handle {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
|
||||
[
|
||||
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
|
||||
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,53 +1,113 @@
|
||||
mod client;
|
||||
mod toplevel;
|
||||
mod toplevel_manager;
|
||||
|
||||
extern crate smithay_client_toolkit as sctk;
|
||||
mod macros;
|
||||
mod wl_output;
|
||||
mod wl_seat;
|
||||
mod wlr_foreign_toplevel;
|
||||
|
||||
use async_once::AsyncOnce;
|
||||
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
|
||||
use cfg_if::cfg_if;
|
||||
use lazy_static::lazy_static;
|
||||
pub use toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
|
||||
use toplevel_manager::{ToplevelHandler, ToplevelHandling, ToplevelStatusListener};
|
||||
use wayland_client::{Attached, DispatchData, Interface};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
||||
use smithay_client_toolkit::output::OutputState;
|
||||
use smithay_client_toolkit::reexports::calloop::LoopHandle;
|
||||
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use smithay_client_toolkit::{
|
||||
delegate_output, delegate_registry, delegate_seat, registry_handlers,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
|
||||
pub use client::WaylandClient;
|
||||
pub use self::client::WaylandClient;
|
||||
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
|
||||
|
||||
/// A utility for lazy-loading globals.
|
||||
/// Taken from `smithay_client_toolkit` where it's not exposed
|
||||
#[derive(Debug)]
|
||||
enum LazyGlobal<I: Interface> {
|
||||
Unknown,
|
||||
Seen { id: u32, version: u32 },
|
||||
Bound(Attached<I>),
|
||||
}
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
mod wlr_data_control;
|
||||
|
||||
sctk::default_environment!(Env,
|
||||
fields = [
|
||||
toplevel: ToplevelHandler
|
||||
],
|
||||
singles = [
|
||||
ZwlrForeignToplevelManagerV1 => toplevel
|
||||
],
|
||||
);
|
||||
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
|
||||
use self::wlr_data_control::device::DataControlDevice;
|
||||
use self::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use self::wlr_data_control::source::CopyPasteSource;
|
||||
use self::wlr_data_control::SelectionOfferItem;
|
||||
|
||||
impl ToplevelHandling for Env {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.toplevel.listen(f)
|
||||
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
|
||||
|
||||
pub struct DataControlDeviceEntry {
|
||||
seat: WlSeat,
|
||||
device: DataControlDevice,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: AsyncOnce<WaylandClient> =
|
||||
AsyncOnce::new(async { WaylandClient::new().await });
|
||||
pub struct Environment {
|
||||
pub registry_state: RegistryState,
|
||||
pub output_state: OutputState,
|
||||
pub seat_state: SeatState,
|
||||
pub foreign_toplevel_manager_state: ToplevelManagerState,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_device_manager_state: DataControlDeviceManagerState,
|
||||
pub loop_handle: LoopHandle<'static, Self>,
|
||||
|
||||
pub seats: Vec<WlSeat>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub data_control_devices: Vec<DataControlDeviceEntry>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub selection_offers: Vec<SelectionOfferItem>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub copy_paste_sources: Vec<CopyPasteSource>,
|
||||
|
||||
pub handles: HashMap<usize, ToplevelHandle>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
|
||||
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
}
|
||||
|
||||
pub async fn get_client() -> &'static WaylandClient {
|
||||
CLIENT.get().await
|
||||
// Now we need to say we are delegating the responsibility of output related events for our application data
|
||||
// type to the requisite delegate.
|
||||
delegate_output!(Environment);
|
||||
delegate_seat!(Environment);
|
||||
|
||||
delegate_foreign_toplevel_manager!(Environment);
|
||||
delegate_foreign_toplevel_handle!(Environment);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
delegate_data_control_device_manager!(Environment);
|
||||
delegate_data_control_device!(Environment);
|
||||
delegate_data_control_source!(Environment);
|
||||
delegate_data_control_offer!(Environment);
|
||||
}
|
||||
}
|
||||
|
||||
// In order for our delegate to know of the existence of globals, we need to implement registry
|
||||
// handling for the program. This trait will forward events to the RegistryHandler trait
|
||||
// implementations.
|
||||
delegate_registry!(Environment);
|
||||
|
||||
// In order for delegate_registry to work, our application data type needs to provide a way for the
|
||||
// implementation to access the registry state.
|
||||
//
|
||||
// We also need to indicate which delegates will get told about globals being created. We specify
|
||||
// the types of the delegates inside the array.
|
||||
impl ProvidesRegistryState for Environment {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![OutputState, SeatState];
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Mutex<WaylandClient>> = Arc::new(Mutex::new(WaylandClient::new()));
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Mutex<WaylandClient>> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tracing::trace;
|
||||
use wayland_client::{DispatchData, Main};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1};
|
||||
use crate::write_lock;
|
||||
|
||||
const STATE_ACTIVE: u32 = 2;
|
||||
const STATE_FULLSCREEN: u32 = 3;
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
fn get_id() -> usize {
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ToplevelInfo {
|
||||
pub id: usize,
|
||||
pub app_id: String,
|
||||
pub title: String,
|
||||
pub active: bool,
|
||||
pub fullscreen: bool,
|
||||
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl ToplevelInfo {
|
||||
fn new() -> Self {
|
||||
let id = get_id();
|
||||
Self {
|
||||
id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toplevel;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelEvent {
|
||||
pub toplevel: ToplevelInfo,
|
||||
pub change: ToplevelChange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ToplevelChange {
|
||||
New,
|
||||
Close,
|
||||
Title(String),
|
||||
Focus(bool),
|
||||
Fullscreen(bool),
|
||||
}
|
||||
|
||||
fn toplevel_implem<F>(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData)
|
||||
where
|
||||
F: FnMut(ToplevelEvent, DispatchData),
|
||||
{
|
||||
trace!("event: {event:?} (info: {info:?})");
|
||||
|
||||
let change = match event {
|
||||
Event::AppId { app_id } => {
|
||||
info.app_id = app_id;
|
||||
None
|
||||
}
|
||||
Event::Title { title } => {
|
||||
info.title = title.clone();
|
||||
|
||||
if info.ready {
|
||||
Some(ToplevelChange::Title(title))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Event::State { state } => {
|
||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||
// the u32 then represents a value in the `State` enum.
|
||||
assert_eq!(state.len() % 4, 0);
|
||||
|
||||
let state = (0..state.len() / 4)
|
||||
.map(|i| {
|
||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||
.try_into()
|
||||
.expect("Received invalid state length");
|
||||
u32::from_le_bytes(slice)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let new_active = state.contains(&STATE_ACTIVE);
|
||||
let new_fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||
|
||||
let change = if info.ready && new_active != info.active {
|
||||
Some(ToplevelChange::Focus(new_active))
|
||||
} else if info.ready && new_fullscreen != info.fullscreen {
|
||||
Some(ToplevelChange::Fullscreen(new_fullscreen))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
info.active = new_active;
|
||||
info.fullscreen = new_fullscreen;
|
||||
|
||||
change
|
||||
}
|
||||
Event::Closed => {
|
||||
if info.ready {
|
||||
Some(ToplevelChange::Close)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Event::OutputEnter { output: _ }
|
||||
| Event::OutputLeave { output: _ }
|
||||
| Event::Parent { parent: _ } => None,
|
||||
Event::Done => {
|
||||
if info.ready || info.app_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
info.ready = true;
|
||||
Some(ToplevelChange::New)
|
||||
}
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(change) = change {
|
||||
let event = ToplevelEvent {
|
||||
change,
|
||||
toplevel: info.clone(),
|
||||
};
|
||||
|
||||
implem(event, ddata);
|
||||
}
|
||||
}
|
||||
|
||||
impl Toplevel {
|
||||
pub fn init<F>(handle: &Main<ZwlrForeignToplevelHandleV1>, mut callback: F) -> Self
|
||||
where
|
||||
F: FnMut(ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
let inner = Arc::new(RwLock::new(ToplevelInfo::new()));
|
||||
|
||||
handle.quick_assign(move |_handle, event, ddata| {
|
||||
let mut inner = write_lock!(inner);
|
||||
toplevel_implem(event, &mut inner, &mut callback, ddata);
|
||||
});
|
||||
|
||||
Self
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use super::toplevel::{Toplevel, ToplevelEvent};
|
||||
use super::LazyGlobal;
|
||||
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
|
||||
use std::cell::RefCell;
|
||||
use std::rc;
|
||||
use std::rc::Rc;
|
||||
use tracing::warn;
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use wayland_client::{Attached, DispatchData};
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
|
||||
struct ToplevelHandlerInner {
|
||||
manager: LazyGlobal<ZwlrForeignToplevelManagerV1>,
|
||||
registry: Option<Attached<WlRegistry>>,
|
||||
toplevels: Vec<Toplevel>,
|
||||
}
|
||||
|
||||
impl ToplevelHandlerInner {
|
||||
const fn new() -> Self {
|
||||
let toplevels = vec![];
|
||||
|
||||
Self {
|
||||
registry: None,
|
||||
manager: LazyGlobal::Unknown,
|
||||
toplevels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToplevelHandler {
|
||||
inner: Rc<RefCell<ToplevelHandlerInner>>,
|
||||
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>>,
|
||||
}
|
||||
|
||||
impl ToplevelHandler {
|
||||
pub fn init() -> Self {
|
||||
let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new()));
|
||||
|
||||
Self {
|
||||
inner,
|
||||
status_listeners: Rc::new(RefCell::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalHandler<ZwlrForeignToplevelManagerV1> for ToplevelHandler {
|
||||
fn created(
|
||||
&mut self,
|
||||
registry: Attached<WlRegistry>,
|
||||
id: u32,
|
||||
version: u32,
|
||||
_ddata: DispatchData,
|
||||
) {
|
||||
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||
if inner.registry.is_none() {
|
||||
inner.registry = Some(registry);
|
||||
}
|
||||
if matches!(inner.manager, LazyGlobal::Unknown) {
|
||||
inner.manager = LazyGlobal::Seen { id, version }
|
||||
} else {
|
||||
warn!(
|
||||
"Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self) -> Option<Attached<ZwlrForeignToplevelManagerV1>> {
|
||||
let mut inner = RefCell::borrow_mut(&self.inner);
|
||||
|
||||
match inner.manager {
|
||||
LazyGlobal::Bound(ref mgr) => Some(mgr.clone()),
|
||||
LazyGlobal::Unknown => None,
|
||||
LazyGlobal::Seen { id, version } => {
|
||||
let registry = inner.registry.as_ref().expect("Failed to get registry");
|
||||
// current max protocol version = 3
|
||||
let version = std::cmp::min(version, 3);
|
||||
let manager = registry.bind::<ZwlrForeignToplevelManagerV1>(version, id);
|
||||
|
||||
{
|
||||
let inner = self.inner.clone();
|
||||
let status_listeners = self.status_listeners.clone();
|
||||
|
||||
manager.quick_assign(move |_, event, _ddata| {
|
||||
let mut inner = RefCell::borrow_mut(&inner);
|
||||
let status_listeners = status_listeners.clone();
|
||||
|
||||
match event {
|
||||
zwlr_foreign_toplevel_manager_v1::Event::Toplevel {
|
||||
toplevel: handle,
|
||||
} => {
|
||||
let toplevel =
|
||||
Toplevel::init(&handle.clone(), move |event, ddata| {
|
||||
notify_status_listeners(
|
||||
&handle,
|
||||
&event,
|
||||
ddata,
|
||||
&status_listeners,
|
||||
);
|
||||
});
|
||||
|
||||
inner.toplevels.push(toplevel);
|
||||
}
|
||||
zwlr_foreign_toplevel_manager_v1::Event::Finished => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inner.manager = LazyGlobal::Bound((*manager).clone());
|
||||
Some((*manager).clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ToplevelStatusCallback =
|
||||
dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||
|
||||
/// Notifies the callbacks of an event on the toplevel
|
||||
fn notify_status_listeners(
|
||||
toplevel: &ZwlrForeignToplevelHandleV1,
|
||||
event: &ToplevelEvent,
|
||||
mut ddata: DispatchData,
|
||||
listeners: &RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>,
|
||||
) {
|
||||
listeners.borrow_mut().retain(|lst| {
|
||||
rc::Weak::upgrade(lst).map_or(false, |cb| {
|
||||
(cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow());
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub struct ToplevelStatusListener {
|
||||
_cb: Rc<RefCell<ToplevelStatusCallback>>,
|
||||
}
|
||||
|
||||
pub trait ToplevelHandling {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
|
||||
}
|
||||
|
||||
impl ToplevelHandling for ToplevelHandler {
|
||||
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
|
||||
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
|
||||
ToplevelStatusListener { _cb: rc }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_toplevels<E, F>(env: Environment<E>, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
E: ToplevelHandling,
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
env.with_inner(move |inner| ToplevelHandling::listen(inner, f))
|
||||
}
|
||||
55
src/clients/wayland/wl_output.rs
Normal file
55
src/clients/wayland/wl_output.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::Environment;
|
||||
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_output;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl Environment {
|
||||
pub fn output_info(&mut self) -> Vec<OutputInfo> {
|
||||
self.output_state
|
||||
.outputs()
|
||||
.filter_map(|output| self.output_state.info(&output))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
|
||||
// output and to provide an instance of the output state to the delegate when dispatching events.
|
||||
impl OutputHandler for Environment {
|
||||
// First we need to provide a way to access the delegate.
|
||||
//
|
||||
// This is needed because delegate implementations for handling events use the application data type in
|
||||
// their function signatures. This allows the implementation to access an instance of the type.
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
// Then there exist these functions that indicate the lifecycle of an output.
|
||||
// These will be called as appropriate by the delegate implementation.
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handler received new output");
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Handle received output destruction");
|
||||
}
|
||||
}
|
||||
63
src/clients/wayland/wl_seat.rs
Normal file
63
src/clients/wayland/wl_seat.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::Environment;
|
||||
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
|
||||
use tracing::debug;
|
||||
use wayland_client::protocol::wl_seat;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
impl SeatHandler for Environment {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received new seat");
|
||||
self.seats.push(seat);
|
||||
}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received new capability");
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
if !self
|
||||
.data_control_devices
|
||||
.iter_mut()
|
||||
.any(|entry| entry.seat == seat)
|
||||
{
|
||||
debug!("Adding new data control device");
|
||||
// create the data device here for this seat
|
||||
let data_control_device_manager = &self.data_control_device_manager_state;
|
||||
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
|
||||
self.data_control_devices
|
||||
.push(super::DataControlDeviceEntry {
|
||||
seat: seat.clone(),
|
||||
device: data_control_device,
|
||||
});
|
||||
}
|
||||
|
||||
if !self.seats.iter().any(|s| s == &seat) {
|
||||
self.seats.push(seat);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: wl_seat::WlSeat,
|
||||
_: Capability,
|
||||
) {
|
||||
debug!("Handler received capability removal");
|
||||
// Not applicable
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
|
||||
debug!("Handler received seat removal");
|
||||
self.seats.retain(|s| s != &seat);
|
||||
}
|
||||
}
|
||||
166
src/clients/wayland/wlr_data_control/device.rs
Normal file
166
src/clients/wayland/wlr_data_control/device.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use super::offer::{
|
||||
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
|
||||
};
|
||||
use crate::error::ERR_WAYLAND_DATA;
|
||||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::warn;
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
||||
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
||||
};
|
||||
|
||||
pub struct DataControlDevice {
|
||||
pub device: ZwlrDataControlDeviceV1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceInner {
|
||||
/// the active selection offer and its data
|
||||
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
|
||||
/// the active undetermined offers and their data
|
||||
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceData {
|
||||
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
|
||||
}
|
||||
|
||||
pub trait DataControlDeviceDataExt: Send + Sync {
|
||||
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
|
||||
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData;
|
||||
|
||||
fn selection_mime_types(&self) -> Vec<String> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.map(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.mime_types()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the active selection offer if it exists.
|
||||
fn selection_offer(&self) -> Option<SelectionOffer> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.and_then(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.as_selection_offer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDevice {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self.device.data().expect(ERR_WAYLAND_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceDataExt for DataControlDeviceData {
|
||||
type DataControlOfferInner = DataControlOfferData;
|
||||
fn data_control_device_data(&self) -> &DataControlDeviceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataDevice` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
|
||||
pub trait DataControlDeviceHandler: Sized {
|
||||
/// Advertises a new selection.
|
||||
fn selection(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U>
|
||||
+ Dispatch<ZwlrDataControlOfferV1, V>
|
||||
+ DataControlDeviceHandler
|
||||
+ DataControlOfferHandler
|
||||
+ 'static,
|
||||
U: DataControlDeviceDataExt,
|
||||
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrDataControlDeviceV1, [
|
||||
0 => (ZwlrDataControlOfferV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
data_device: &ZwlrDataControlDeviceV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_device_data();
|
||||
let inner = lock!(data.inner);
|
||||
|
||||
match event {
|
||||
Event::DataOffer { id } => {
|
||||
// XXX Drop done here to prevent Mutex deadlocks.S
|
||||
|
||||
lock!(inner.undetermined_offers).push(id.clone());
|
||||
let data = id
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.init_undetermined_offer(&id);
|
||||
|
||||
// Append the data offer to our list of offers.
|
||||
drop(inner);
|
||||
}
|
||||
Event::Selection { id } => {
|
||||
let mut selection_offer = lock!(inner.selection_offer);
|
||||
|
||||
if let Some(offer) = id {
|
||||
let mut undetermined = lock!(inner.undetermined_offers);
|
||||
if let Some(i) = undetermined.iter().position(|o| o == &offer) {
|
||||
undetermined.remove(i);
|
||||
}
|
||||
drop(undetermined);
|
||||
|
||||
let data = offer
|
||||
.data::<V>()
|
||||
.expect(ERR_WAYLAND_DATA)
|
||||
.data_control_offer_data();
|
||||
data.to_selection_offer();
|
||||
// XXX Drop done here to prevent Mutex deadlocks.
|
||||
*selection_offer = Some(offer.clone());
|
||||
drop(selection_offer);
|
||||
drop(inner);
|
||||
state.selection(
|
||||
conn,
|
||||
qh,
|
||||
DataControlDevice {
|
||||
device: data_device.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
*selection_offer = None;
|
||||
}
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/clients/wayland/wlr_data_control/manager.rs
Normal file
132
src/clients/wayland/wlr_data_control/manager.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
|
||||
use super::offer::DataControlOfferData;
|
||||
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::debug;
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
|
||||
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
|
||||
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
|
||||
manager: ZwlrDataControlManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl DataControlDeviceManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let manager = globals.bind(qh, 1..=2, GlobalData)?;
|
||||
debug!("Bound to ZwlDataControlManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// creates a data source for copy paste
|
||||
pub fn create_copy_paste_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> CopyPasteSource
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
CopyPasteSource {
|
||||
inner: self.create_data_control_source(qh, mime_types),
|
||||
}
|
||||
}
|
||||
|
||||
/// creates a data source
|
||||
fn create_data_control_source<'s, D, I>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
mime_types: I,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
|
||||
I: IntoIterator<Item = &'s str>,
|
||||
{
|
||||
let source =
|
||||
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
|
||||
|
||||
for mime in mime_types {
|
||||
source.offer(mime.to_string());
|
||||
}
|
||||
|
||||
source
|
||||
}
|
||||
|
||||
/// create a new data source for a given seat with some user data
|
||||
pub fn create_data_control_source_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
data: U,
|
||||
) -> ZwlrDataControlSourceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
|
||||
U: DataControlSourceDataExt + 'static,
|
||||
{
|
||||
self.manager.create_data_source(qh, data)
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat
|
||||
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
|
||||
{
|
||||
DataControlDevice {
|
||||
device: self.get_data_control_device_with_data(
|
||||
qh,
|
||||
seat,
|
||||
DataControlDeviceData::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// create a new data device for a given seat with some user data
|
||||
pub fn get_data_control_device_with_data<D, U>(
|
||||
&self,
|
||||
qh: &QueueHandle<D>,
|
||||
seat: &WlSeat,
|
||||
data: U,
|
||||
) -> ZwlrDataControlDeviceV1
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
|
||||
U: DataControlDeviceDataExt + 'static,
|
||||
{
|
||||
self.manager.get_data_device(seat, qh, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>,
|
||||
{
|
||||
fn event(
|
||||
_state: &mut D,
|
||||
_proxy: &ZwlrDataControlManagerV1,
|
||||
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
|
||||
_data: &GlobalData,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
389
src/clients/wayland/wlr_data_control/mod.rs
Normal file
389
src/clients/wayland/wlr_data_control/mod.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
pub mod device;
|
||||
pub mod manager;
|
||||
pub mod offer;
|
||||
pub mod source;
|
||||
|
||||
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||
use self::source::DataControlSourceHandler;
|
||||
use crate::clients::wayland::Environment;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::{lock, send};
|
||||
use device::DataControlDevice;
|
||||
use glib::Bytes;
|
||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
||||
use nix::sys::epoll::{epoll_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||
use std::cmp::min;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
use std::sync::Arc;
|
||||
use std::{fs, io};
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
||||
|
||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||
|
||||
pub struct SelectionOfferItem {
|
||||
offer: SelectionOffer,
|
||||
token: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct ClipboardItem {
|
||||
pub id: usize,
|
||||
pub value: ClipboardValue,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for ClipboardItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardValue {
|
||||
Text(String),
|
||||
Image(Bytes),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Debug for ClipboardValue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Text(text) => text.clone(),
|
||||
Self::Image(bytes) => {
|
||||
format!("[{} Bytes]", bytes.len())
|
||||
}
|
||||
Self::Other => "[Unknown]".to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MimeType {
|
||||
value: String,
|
||||
category: MimeTypeCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MimeTypeCategory {
|
||||
Text,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
fn parse(mime_type: &str) -> Option<Self> {
|
||||
match mime_type.to_lowercase().as_str() {
|
||||
"text"
|
||||
| "string"
|
||||
| "utf8_string"
|
||||
| "text/plain"
|
||||
| "text/plain;charset=utf-8"
|
||||
| "text/plain;charset=iso-8859-1"
|
||||
| "text/plain;charset=us-ascii"
|
||||
| "text/plain;charset=unicode" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Text,
|
||||
}),
|
||||
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
||||
| "image/x-bmp" | "image/icon" => Some(Self {
|
||||
value: mime_type.to_string(),
|
||||
category: MimeTypeCategory::Image,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
|
||||
mime_types.iter().find_map(|mime| Self::parse(mime))
|
||||
}
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
|
||||
debug!("Copying item to clipboard: {item:?}");
|
||||
|
||||
// TODO: Proper device tracking
|
||||
let device = self.data_control_devices.first();
|
||||
if let Some(device) = device {
|
||||
let source = self
|
||||
.data_control_device_manager_state
|
||||
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
|
||||
|
||||
source.set_selection(&device.device);
|
||||
self.copy_paste_sources.push(source);
|
||||
|
||||
lock!(self.clipboard).replace(item);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||
let value = match mime_type.category {
|
||||
MimeTypeCategory::Text => {
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)?;
|
||||
|
||||
ClipboardValue::Text(txt)
|
||||
}
|
||||
MimeTypeCategory::Image => {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)?;
|
||||
let bytes = Bytes::from(&bytes);
|
||||
|
||||
ClipboardValue::Image(bytes)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ClipboardItem {
|
||||
id: get_unique_usize(),
|
||||
value,
|
||||
mime_type: mime_type.value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlDeviceHandler for Environment {
|
||||
fn selection(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
data_device: DataControlDevice,
|
||||
) {
|
||||
debug!("Handler received selection event");
|
||||
|
||||
let mime_types = data_device.selection_mime_types();
|
||||
|
||||
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(offer) = data_device.selection_offer() {
|
||||
self.selection_offers
|
||||
.push(SelectionOfferItem { offer, token: None });
|
||||
|
||||
let cur_offer = self
|
||||
.selection_offers
|
||||
.last_mut()
|
||||
.expect("Failed to get current offer");
|
||||
|
||||
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
||||
lock!(self.clipboard).take();
|
||||
// send an event so the clipboard module is aware it's changed
|
||||
send!(
|
||||
self.clipboard_tx,
|
||||
Arc::new(ClipboardItem {
|
||||
id: usize::MAX,
|
||||
mime_type: String::new(),
|
||||
value: ClipboardValue::Other
|
||||
})
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
||||
let offer_clone = cur_offer.offer.clone();
|
||||
|
||||
let tx = self.clipboard_tx.clone();
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
let token = self
|
||||
.loop_handle
|
||||
.insert_source(read_pipe, move |_, file, state| {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
|
||||
match Self::read_file(&mime_type, file) {
|
||||
Ok(item) => {
|
||||
let item = Arc::new(item);
|
||||
lock!(clipboard).replace(item.clone());
|
||||
send!(tx, item);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
});
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
cur_offer.token.replace(token);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlOfferHandler for Environment {
|
||||
fn offer(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_offer: &mut DataControlDeviceOffer,
|
||||
_mime_type: String,
|
||||
) {
|
||||
debug!("Handler received offer");
|
||||
}
|
||||
}
|
||||
|
||||
impl DataControlSourceHandler for Environment {
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
) {
|
||||
debug!("Accepted mime type: {mime:?}");
|
||||
}
|
||||
|
||||
fn send_request(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
debug!("Handler received source send request event ({mime})");
|
||||
|
||||
if let Some(item) = lock!(self.clipboard).clone() {
|
||||
let fd = OwnedFd::from(write_pipe);
|
||||
if self
|
||||
.copy_paste_sources
|
||||
.iter_mut()
|
||||
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
|
||||
{
|
||||
trace!("Source found, writing to file");
|
||||
|
||||
let mut bytes = match &item.value {
|
||||
ClipboardValue::Text(text) => text.as_bytes(),
|
||||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||
ClipboardValue::Other => panic!(
|
||||
"{:?}",
|
||||
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",)
|
||||
),
|
||||
};
|
||||
|
||||
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
|
||||
.expect("Failed to increase pipe size");
|
||||
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
|
||||
|
||||
trace!("Num bytes: {}", bytes.len());
|
||||
|
||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
|
||||
let epoll_fd = epoll_create().expect("to get valid file descriptor");
|
||||
epoll_ctl(
|
||||
epoll_fd,
|
||||
EpollOp::EpollCtlAdd,
|
||||
fd.as_raw_fd(),
|
||||
&mut epoll_event,
|
||||
)
|
||||
.expect("to send valid epoll operation");
|
||||
|
||||
while !bytes.is_empty() {
|
||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||
|
||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
||||
|
||||
epoll_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
|
||||
|
||||
match file.write(chunk) {
|
||||
Ok(_) => bytes = &bytes[chunk.len()..],
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for chunk in bytes.chunks(pipe_size as usize) {
|
||||
// trace!("Writing chunk");
|
||||
// file.write(chunk).expect("Failed to write chunk to buffer");
|
||||
// file.flush().expect("Failed to flush to file");
|
||||
// }
|
||||
|
||||
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
|
||||
// Ok(_) => debug!("Copied item"),
|
||||
// Err(err) => error!("{err:?}"),
|
||||
// }
|
||||
|
||||
// match file.write_all(bytes) {
|
||||
// Ok(_) => debug!("Copied item"),
|
||||
// Err(err) => error!("{err:?}"),
|
||||
// }
|
||||
} else {
|
||||
error!("Failed to find source");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
) {
|
||||
debug!("Handler received source cancelled event");
|
||||
|
||||
self.copy_paste_sources
|
||||
.iter()
|
||||
.position(|s| s.inner() == source)
|
||||
.map(|pos| self.copy_paste_sources.remove(pos));
|
||||
source.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to increase the fd pipe size to the requested number of bytes.
|
||||
/// The kernel will automatically round this up to the nearest page size.
|
||||
/// If the requested size is larger than the kernel max (normally 1MB),
|
||||
/// it will be clamped at this.
|
||||
///
|
||||
/// Returns the new size if succeeded
|
||||
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||
// clamp size at kernel max
|
||||
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
||||
.expect("Failed to find pipe-max-size virtual kernel file")
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.expect("Failed to parse pipe-max-size contents");
|
||||
|
||||
let size = min(size, max_pipe_size);
|
||||
|
||||
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
|
||||
|
||||
trace!("Current pipe size: {curr_size}");
|
||||
|
||||
let new_size = if size > curr_size {
|
||||
trace!("Requesting pipe size increase to (at least): {size}");
|
||||
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
|
||||
trace!("New pipe size: {res}");
|
||||
if res < size as i32 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
res
|
||||
} else {
|
||||
size as i32
|
||||
};
|
||||
|
||||
Ok(new_size)
|
||||
}
|
||||
183
src/clients/wayland/wlr_data_control/offer.rs
Normal file
183
src/clients/wayland/wlr_data_control/offer.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::unistd::{close, pipe2};
|
||||
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
|
||||
use smithay_client_toolkit::data_device_manager::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
Event, ZwlrDataControlOfferV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UndeterminedOffer {
|
||||
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>,
|
||||
}
|
||||
|
||||
impl PartialEq for UndeterminedOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionOffer {
|
||||
pub data_offer: ZwlrDataControlOfferV1,
|
||||
}
|
||||
|
||||
impl PartialEq for SelectionOffer {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.data_offer == other.data_offer
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionOffer {
|
||||
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
|
||||
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DataControlDeviceOffer {
|
||||
Selection(SelectionOffer),
|
||||
Undetermined(UndeterminedOffer),
|
||||
}
|
||||
|
||||
impl Default for DataControlDeviceOffer {
|
||||
fn default() -> Self {
|
||||
Self::Undetermined(UndeterminedOffer { data_offer: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlOfferData {
|
||||
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlDeviceOfferInner {
|
||||
pub(crate) offer: DataControlDeviceOffer,
|
||||
pub(crate) mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl DataControlOfferData {
|
||||
pub(crate) fn push_mime_type(&self, mime_type: String) {
|
||||
lock!(self.inner).mime_types.push(mime_type);
|
||||
}
|
||||
|
||||
pub(crate) fn to_selection_offer(&self) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
|
||||
data_offer: o.data_offer.clone().expect("Missing current data offer"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
|
||||
let mut inner = lock!(self.inner);
|
||||
match &mut inner.deref_mut().offer {
|
||||
DataControlDeviceOffer::Selection(_) => {
|
||||
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
|
||||
data_offer: Some(offer.clone()),
|
||||
});
|
||||
}
|
||||
DataControlDeviceOffer::Undetermined(o) => {
|
||||
o.data_offer = Some(offer.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataControlOfferDataExt {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData;
|
||||
fn mime_types(&self) -> Vec<String>;
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer>;
|
||||
}
|
||||
|
||||
impl DataControlOfferDataExt for DataControlOfferData {
|
||||
fn data_control_offer_data(&self) -> &DataControlOfferData {
|
||||
self
|
||||
}
|
||||
|
||||
fn mime_types(&self) -> Vec<String> {
|
||||
lock!(self.inner).mime_types.clone()
|
||||
}
|
||||
|
||||
fn as_selection_offer(&self) -> Option<SelectionOffer> {
|
||||
match &lock!(self.inner).offer {
|
||||
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
|
||||
DataControlDeviceOffer::Undetermined(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataOffer` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
|
||||
pub trait DataControlOfferHandler: Sized {
|
||||
// Called for each mime type the data offer advertises.
|
||||
fn offer(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
offer: &mut DataControlDeviceOffer,
|
||||
mime_type: String,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
|
||||
U: DataControlOfferDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
_offer: &ZwlrDataControlOfferV1,
|
||||
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
let data = data.data_control_offer_data();
|
||||
|
||||
if let Event::Offer { mime_type } = event {
|
||||
debug!("Adding new offer with type '{mime_type}'");
|
||||
data.push_mime_type(mime_type.clone());
|
||||
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to receive the data of a given mime type.
|
||||
///
|
||||
/// You can do this several times, as a reaction to motion of
|
||||
/// the dnd cursor, or to inspect the data in order to choose your
|
||||
/// response.
|
||||
///
|
||||
/// Note that you should *not* read the contents right away in a
|
||||
/// blocking way, as you may deadlock your application doing so.
|
||||
/// At least make sure you flush your events to the server before
|
||||
/// doing so.
|
||||
///
|
||||
/// Fails if too many file descriptors were already open and a pipe
|
||||
/// could not be created.
|
||||
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
|
||||
offer.receive(mime_type, writefd);
|
||||
|
||||
if let Err(err) = close(writefd) {
|
||||
warn!("Failed to close write pipe: {}", err);
|
||||
}
|
||||
|
||||
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||
}
|
||||
101
src/clients/wayland/wlr_data_control/source.rs
Normal file
101
src/clients/wayland/wlr_data_control/source.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use super::device::DataControlDevice;
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
|
||||
Event, ZwlrDataControlSourceV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DataControlSourceData {}
|
||||
|
||||
pub trait DataControlSourceDataExt: Send + Sync {
|
||||
fn data_source_data(&self) -> &DataControlSourceData;
|
||||
}
|
||||
|
||||
impl DataControlSourceDataExt for DataControlSourceData {
|
||||
fn data_source_data(&self) -> &DataControlSourceData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler trait for `DataSource` events.
|
||||
///
|
||||
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
|
||||
pub trait DataControlSourceHandler: Sized {
|
||||
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: Option<String>,
|
||||
);
|
||||
|
||||
/// The client has requested the data for this source to be sent.
|
||||
/// Send the data, then close the fd.
|
||||
fn send_request(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
fd: WritePipe,
|
||||
);
|
||||
|
||||
/// The data source is no longer valid
|
||||
/// Cleanup & destroy this resource
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
|
||||
U: DataControlSourceDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
source: &ZwlrDataControlSourceV1,
|
||||
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
|
||||
_data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Send { mime_type, fd } => {
|
||||
state.send_request(conn, qh, source, mime_type, fd.into());
|
||||
}
|
||||
Event::Cancelled => {
|
||||
state.cancelled(conn, qh, source);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CopyPasteSource {
|
||||
pub(crate) inner: ZwlrDataControlSourceV1,
|
||||
}
|
||||
|
||||
impl CopyPasteSource {
|
||||
/// Set the selection of the provided data device as a response to the event with with provided serial.
|
||||
pub fn set_selection(&self, device: &DataControlDevice) {
|
||||
device.device.set_selection(Some(&self.inner));
|
||||
}
|
||||
|
||||
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CopyPasteSource {
|
||||
fn drop(&mut self) {
|
||||
self.inner.destroy();
|
||||
}
|
||||
}
|
||||
178
src/clients/wayland/wlr_foreign_toplevel/handle.rs
Normal file
178
src/clients/wayland/wlr_foreign_toplevel/handle.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use super::manager::ToplevelManagerState;
|
||||
use crate::lock;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::trace;
|
||||
use wayland_client::protocol::wl_output::WlOutput;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
|
||||
Event, ZwlrForeignToplevelHandleV1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelHandle {
|
||||
pub handle: ZwlrForeignToplevelHandleV1,
|
||||
}
|
||||
|
||||
impl PartialEq for ToplevelHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.handle == other.handle
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelHandle {
|
||||
pub fn info(&self) -> Option<ToplevelInfo> {
|
||||
trace!("Retrieving handle info");
|
||||
|
||||
let data = self.handle.data::<ToplevelHandleData>()?;
|
||||
data.info()
|
||||
}
|
||||
|
||||
pub fn focus(&self, seat: &WlSeat) {
|
||||
trace!("Activating handle");
|
||||
self.handle.activate(seat);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleData {
|
||||
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
|
||||
}
|
||||
|
||||
impl ToplevelHandleData {
|
||||
fn info(&self) -> Option<ToplevelInfo> {
|
||||
lock!(self.inner).current_info.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleDataInner {
|
||||
initial_done: bool,
|
||||
output: Option<WlOutput>,
|
||||
|
||||
current_info: Option<ToplevelInfo>,
|
||||
pending_info: ToplevelInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToplevelInfo {
|
||||
pub id: usize,
|
||||
pub app_id: String,
|
||||
pub title: String,
|
||||
pub fullscreen: bool,
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl Default for ToplevelInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: get_unique_usize(),
|
||||
app_id: String::new(),
|
||||
title: String::new(),
|
||||
fullscreen: false,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelHandleDataExt {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
|
||||
}
|
||||
|
||||
impl ToplevelHandleDataExt for ToplevelHandleData {
|
||||
fn toplevel_handle_data(&self) -> &ToplevelHandleData {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelHandleHandler: Sized {
|
||||
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
|
||||
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
|
||||
}
|
||||
|
||||
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
|
||||
U: ToplevelHandleDataExt,
|
||||
{
|
||||
fn event(
|
||||
state: &mut D,
|
||||
handle: &ZwlrForeignToplevelHandleV1,
|
||||
event: Event,
|
||||
data: &U,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<D>,
|
||||
) {
|
||||
const STATE_ACTIVE: u32 = 2;
|
||||
const STATE_FULLSCREEN: u32 = 3;
|
||||
|
||||
let data = data.toplevel_handle_data();
|
||||
|
||||
trace!("Processing handle event: {event:?}");
|
||||
|
||||
match event {
|
||||
Event::Title { title } => {
|
||||
lock!(data.inner).pending_info.title = title;
|
||||
}
|
||||
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
|
||||
Event::State { state } => {
|
||||
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
|
||||
// the u32 then represents a value in the `State` enum.
|
||||
assert_eq!(state.len() % 4, 0);
|
||||
let state = (0..state.len() / 4)
|
||||
.map(|i| {
|
||||
let slice: [u8; 4] = state[i * 4..i * 4 + 4]
|
||||
.try_into()
|
||||
.expect("Received invalid state length");
|
||||
u32::from_le_bytes(slice)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
|
||||
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN);
|
||||
}
|
||||
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
|
||||
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
|
||||
Event::Closed => state.remove_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
),
|
||||
Event::Done => {
|
||||
{
|
||||
let pending_info = lock!(data.inner).pending_info.clone();
|
||||
lock!(data.inner).current_info = Some(pending_info);
|
||||
}
|
||||
|
||||
if lock!(data.inner).initial_done {
|
||||
state.update_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
lock!(data.inner).initial_done = true;
|
||||
state.new_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
trace!("Event processed");
|
||||
}
|
||||
}
|
||||
86
src/clients/wayland/wlr_foreign_toplevel/manager.rs
Normal file
86
src/clients/wayland/wlr_foreign_toplevel/manager.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
|
||||
use smithay_client_toolkit::error::GlobalError;
|
||||
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
||||
use std::marker::PhantomData;
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
|
||||
pub struct ToplevelManagerState<V = ToplevelHandleData> {
|
||||
manager: ZwlrForeignToplevelManagerV1,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl ToplevelManagerState {
|
||||
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
|
||||
where
|
||||
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
|
||||
{
|
||||
let manager = globals.bind(qh, 1..=3, GlobalData)?;
|
||||
debug!("Bound to ZwlForeignToplevelManagerV1 global");
|
||||
Ok(Self {
|
||||
manager,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToplevelManagerHandler: Sized {
|
||||
/// Advertises a new toplevel.
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
manager: ToplevelManagerState,
|
||||
);
|
||||
}
|
||||
|
||||
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
|
||||
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
|
||||
Ok(self.manager.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
|
||||
where
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
|
||||
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
|
||||
+ ToplevelManagerHandler
|
||||
+ ToplevelHandleHandler
|
||||
+ 'static,
|
||||
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
|
||||
{
|
||||
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
|
||||
0 => (ZwlrForeignToplevelHandleV1, V::default())
|
||||
]);
|
||||
|
||||
fn event(
|
||||
state: &mut D,
|
||||
toplevel_manager: &ZwlrForeignToplevelManagerV1,
|
||||
event: Event,
|
||||
_data: &GlobalData,
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<D>,
|
||||
) {
|
||||
match event {
|
||||
Event::Toplevel { toplevel: _ } => {
|
||||
state.toplevel(
|
||||
conn,
|
||||
qhandle,
|
||||
ToplevelManagerState {
|
||||
manager: toplevel_manager.clone(),
|
||||
_phantom: PhantomData,
|
||||
},
|
||||
);
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
84
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
pub mod handle;
|
||||
pub mod manager;
|
||||
|
||||
use self::handle::ToplevelHandleHandler;
|
||||
use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
|
||||
use crate::clients::wayland::Environment;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
use crate::send;
|
||||
pub use handle::{ToplevelHandle, ToplevelInfo};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelEvent {
|
||||
New(ToplevelHandle),
|
||||
Update(ToplevelHandle),
|
||||
Remove(ToplevelHandle),
|
||||
}
|
||||
|
||||
impl ToplevelManagerHandler for Environment {
|
||||
fn toplevel(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_manager: ToplevelManagerState,
|
||||
) {
|
||||
debug!("Manager received new handle");
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelHandleHandler for Environment {
|
||||
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
|
||||
debug!("Handler received new handle");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Adding new handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::New(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
debug!("Handler received handle update");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
trace!("Updating handle: {info:?}");
|
||||
self.handles.insert(info.id, handle.clone());
|
||||
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_handle(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
debug!("Handler received handle close");
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
self.handles.remove(&info.id);
|
||||
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
|
||||
}
|
||||
None => {
|
||||
error!("Handle is missing information!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/config/common.rs
Normal file
151
src/config/common.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
/// Common configuration options
|
||||
/// which can be set on every module.
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
pub struct CommonConfig {
|
||||
pub class: Option<String>,
|
||||
pub name: Option<String>,
|
||||
|
||||
pub show_if: Option<DynamicBool>,
|
||||
pub transition_type: Option<TransitionType>,
|
||||
pub transition_duration: Option<u32>,
|
||||
|
||||
pub on_click_left: Option<ScriptInput>,
|
||||
pub on_click_right: Option<ScriptInput>,
|
||||
pub on_click_middle: Option<ScriptInput>,
|
||||
pub on_scroll_up: Option<ScriptInput>,
|
||||
pub on_scroll_down: Option<ScriptInput>,
|
||||
pub on_mouse_enter: Option<ScriptInput>,
|
||||
pub on_mouse_exit: Option<ScriptInput>,
|
||||
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TransitionType {
|
||||
None,
|
||||
Crossfade,
|
||||
SlideStart,
|
||||
SlideEnd,
|
||||
}
|
||||
|
||||
impl TransitionType {
|
||||
pub const fn to_revealer_transition_type(
|
||||
&self,
|
||||
orientation: Orientation,
|
||||
) -> RevealerTransitionType {
|
||||
match (self, orientation) {
|
||||
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft,
|
||||
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
||||
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight,
|
||||
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
||||
(Self::Crossfade, _) => RevealerTransitionType::Crossfade,
|
||||
_ => RevealerTransitionType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonConfig {
|
||||
/// Configures the module's container according to the common config options.
|
||||
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.install_show_if(container, revealer);
|
||||
|
||||
let left_click_script = self.on_click_left.map(Script::new_polling);
|
||||
let middle_click_script = self.on_click_middle.map(Script::new_polling);
|
||||
let right_click_script = self.on_click_right.map(Script::new_polling);
|
||||
|
||||
container.connect_button_press_event(move |_, event| {
|
||||
let script = match event.button() {
|
||||
1 => left_click_script.as_ref(),
|
||||
2 => middle_click_script.as_ref(),
|
||||
3 => right_click_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-click script: {}", event.button());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
||||
|
||||
container.connect_scroll_event(move |_, event| {
|
||||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(script) = script {
|
||||
trace!("Running on-scroll script: {}", event.direction());
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
macro_rules! install_oneshot {
|
||||
($option:expr, $method:ident) => {
|
||||
$option.map(Script::new_polling).map(|script| {
|
||||
container.$method(move |_, _| {
|
||||
script.run_as_oneshot(None);
|
||||
Inhibit(false)
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
install_oneshot!(self.on_mouse_enter, connect_enter_notify_event);
|
||||
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
let container = container.clone();
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) {
|
||||
self.show_if.take().map_or_else(
|
||||
|| {
|
||||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
let container = container.clone();
|
||||
|
||||
{
|
||||
let revealer = revealer.clone();
|
||||
let container = container.clone();
|
||||
|
||||
show_if.subscribe(move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
revealer.connect_child_revealed_notify(move |revealer| {
|
||||
if !revealer.reveals_child() {
|
||||
container.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
use super::{BarPosition, Config, MonitorConfig};
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::{ContextCompat, WrapErr};
|
||||
use color_eyre::{Help, Report};
|
||||
use dirs::config_dir;
|
||||
use gtk::Orientation;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
use tracing::instrument;
|
||||
|
||||
// Manually implement for better untagged enum error handling:
|
||||
// currently open pr: https://github.com/serde-rs/serde/pull/1544
|
||||
impl<'de> Deserialize<'de> for MonitorConfig {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
@@ -62,79 +56,3 @@ impl BarPosition {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Attempts to load the config file from file,
|
||||
/// parse it and return a new instance of `Self`.
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = env::var("IRONBAR_CONFIG").map_or_else(
|
||||
|_| Self::try_find_config(),
|
||||
|config_path| {
|
||||
let path = PathBuf::from(config_path);
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Specified config file does not exist: {}",
|
||||
path.display()
|
||||
))
|
||||
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
Self::load_file(&config_path)
|
||||
}
|
||||
|
||||
/// Attempts to discover the location of the config file
|
||||
/// by checking each valid format's extension.
|
||||
///
|
||||
/// Returns the path of the first valid match, if any.
|
||||
#[instrument]
|
||||
fn try_find_config() -> Result<PathBuf> {
|
||||
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
||||
|
||||
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
|
||||
|
||||
let file = extensions.into_iter().find_map(|extension| {
|
||||
let full_path = config_dir
|
||||
.join("ironbar")
|
||||
.join(format!("config.{extension}"));
|
||||
|
||||
if Path::exists(&full_path) {
|
||||
Some(full_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
file.map_or_else(
|
||||
|| {
|
||||
Err(Report::msg("Could not find config file")
|
||||
.suggestion("Ironbar does not include a configuration out of the box")
|
||||
.suggestion("A guide on writing a config can be found on the wiki:")
|
||||
.suggestion("https://github.com/JakeStanger/ironbar/wiki/configuration-guide"))
|
||||
},
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads the config file at the specified path
|
||||
/// and parses it into `Self` based on its extension.
|
||||
fn load_file(path: &Path) -> Result<Self> {
|
||||
let file = fs::read(path).wrap_err("Failed to read config file")?;
|
||||
let extension = path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
match extension {
|
||||
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
|
||||
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
|
||||
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
|
||||
"corn" => libcorn::from_slice(&file).wrap_err("Invalid Corn config"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
mod common;
|
||||
mod r#impl;
|
||||
mod truncate;
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
use crate::modules::clipboard::ClipboardModule;
|
||||
#[cfg(feature = "clock")]
|
||||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::label::LabelModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
#[cfg(feature = "music")]
|
||||
use crate::modules::music::MusicModule;
|
||||
use crate::modules::script::ScriptModule;
|
||||
#[cfg(feature = "sys_info")]
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
#[cfg(feature = "tray")]
|
||||
use crate::modules::tray::TrayModule;
|
||||
#[cfg(feature = "upower")]
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use crate::script::ScriptInput;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CommonConfig {
|
||||
pub show_if: Option<ScriptInput>,
|
||||
|
||||
pub on_click_left: Option<ScriptInput>,
|
||||
pub on_click_right: Option<ScriptInput>,
|
||||
pub on_click_middle: Option<ScriptInput>,
|
||||
pub on_scroll_up: Option<ScriptInput>,
|
||||
pub on_scroll_down: Option<ScriptInput>,
|
||||
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
pub use self::common::{CommonConfig, TransitionType};
|
||||
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ModuleConfig {
|
||||
Clock(ClockModule),
|
||||
Music(MusicModule),
|
||||
Tray(TrayModule),
|
||||
Workspaces(WorkspacesModule),
|
||||
SysInfo(SysInfoModule),
|
||||
Launcher(LauncherModule),
|
||||
Script(ScriptModule),
|
||||
Focused(FocusedModule),
|
||||
Custom(CustomModule),
|
||||
#[cfg(feature = "clock")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
Custom(Box<CustomModule>),
|
||||
Focused(Box<FocusedModule>),
|
||||
Label(Box<LabelModule>),
|
||||
Launcher(Box<LauncherModule>),
|
||||
#[cfg(feature = "music")]
|
||||
Music(Box<MusicModule>),
|
||||
Script(Box<ScriptModule>),
|
||||
#[cfg(feature = "sys_info")]
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
#[cfg(feature = "tray")]
|
||||
Tray(Box<TrayModule>),
|
||||
#[cfg(feature = "upower")]
|
||||
Upower(Box<UpowerModule>),
|
||||
#[cfg(feature = "workspaces")]
|
||||
Workspaces(Box<WorkspacesModule>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -61,14 +72,35 @@ impl Default for BarPosition {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct MarginConfig {
|
||||
#[serde(default)]
|
||||
pub bottom: i32,
|
||||
#[serde(default)]
|
||||
pub left: i32,
|
||||
#[serde(default)]
|
||||
pub right: i32,
|
||||
#[serde(default)]
|
||||
pub top: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_bar_position")]
|
||||
#[serde(default)]
|
||||
pub position: BarPosition,
|
||||
#[serde(default = "default_true")]
|
||||
pub anchor_to_edges: bool,
|
||||
#[serde(default = "default_bar_height")]
|
||||
pub height: i32,
|
||||
#[serde(default)]
|
||||
pub margin: MarginConfig,
|
||||
#[serde(default = "default_popup_gap")]
|
||||
pub popup_gap: i32,
|
||||
|
||||
/// GTK icon theme to use.
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
||||
|
||||
pub start: Option<Vec<ModuleConfig>>,
|
||||
pub center: Option<Vec<ModuleConfig>>,
|
||||
@@ -77,14 +109,14 @@ pub struct Config {
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
}
|
||||
|
||||
const fn default_bar_position() -> BarPosition {
|
||||
BarPosition::Bottom
|
||||
}
|
||||
|
||||
const fn default_bar_height() -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
const fn default_popup_gap() -> i32 {
|
||||
5
|
||||
}
|
||||
|
||||
pub const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
66
src/config/truncate.rs
Normal file
66
src/config/truncate.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EllipsizeMode {
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
|
||||
impl From<EllipsizeMode> for GtkEllipsizeMode {
|
||||
fn from(value: EllipsizeMode) -> Self {
|
||||
match value {
|
||||
EllipsizeMode::Start => Self::Start,
|
||||
EllipsizeMode::Middle => Self::Middle,
|
||||
EllipsizeMode::End => Self::End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(untagged)]
|
||||
pub enum TruncateMode {
|
||||
Auto(EllipsizeMode),
|
||||
Length {
|
||||
mode: EllipsizeMode,
|
||||
length: Option<i32>,
|
||||
max_length: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TruncateMode {
|
||||
const fn mode(&self) -> EllipsizeMode {
|
||||
match self {
|
||||
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
|
||||
}
|
||||
}
|
||||
|
||||
const fn length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Length { length, .. } => *length,
|
||||
}
|
||||
}
|
||||
|
||||
const fn max_length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Length { max_length, .. } => *max_length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate_label(&self, label: >k::Label) {
|
||||
label.set_ellipsize(self.mode().into());
|
||||
|
||||
if let Some(length) = self.length() {
|
||||
label.set_width_chars(length);
|
||||
}
|
||||
|
||||
if let Some(length) = self.max_length() {
|
||||
label.set_max_width_chars(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/desktop_file.rs
Normal file
163
src/desktop_file.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::lock;
|
||||
|
||||
type DesktopFile = HashMap<String, Vec<String>>;
|
||||
|
||||
lazy_static! {
|
||||
static ref DESKTOP_FILES: Mutex<HashMap<PathBuf, DesktopFile>> =
|
||||
Mutex::new(HashMap::new());
|
||||
|
||||
/// These are the keys that in the cache
|
||||
static ref DESKTOP_FILES_LOOK_OUT_KEYS: HashSet<&'static str> =
|
||||
HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]);
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![
|
||||
PathBuf::from("/usr/share/applications"), // 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 {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Finds all the desktop files
|
||||
fn find_desktop_files() -> Vec<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
dirs.into_iter()
|
||||
.flat_map(|dir| {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
// this is necessary to invalidate the cache
|
||||
let files = find_desktop_files();
|
||||
|
||||
if let Some(path) = find_desktop_file_by_filename(app_id, &files) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
let Some(path) = find_desktop_file(app_id) else { return None };
|
||||
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let desktop_file = match desktop_files_cache.get(&path) {
|
||||
Some(desktop_file) => desktop_file,
|
||||
_ => desktop_files_cache
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||
};
|
||||
|
||||
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||
|
||||
icons.next().map(std::string::ToString::to_string)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{lock, send};
|
||||
use gtk::prelude::*;
|
||||
use indexmap::IndexMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DynamicStringSegment {
|
||||
Static(String),
|
||||
Dynamic(Script),
|
||||
}
|
||||
|
||||
pub struct DynamicString;
|
||||
|
||||
impl DynamicString {
|
||||
pub fn new<F>(input: &str, f: F) -> Self
|
||||
where
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let mut segments = vec![];
|
||||
|
||||
let mut chars = input.chars().collect::<Vec<_>>();
|
||||
while !chars.is_empty() {
|
||||
let char = &chars[..=1];
|
||||
|
||||
let (token, skip) = if let ['{', '{'] = char {
|
||||
const SKIP_BRACKETS: usize = 4;
|
||||
|
||||
let str = chars
|
||||
.iter()
|
||||
.skip(2)
|
||||
.enumerate()
|
||||
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
|
||||
.map(|(_, c)| c)
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(
|
||||
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
||||
len + SKIP_BRACKETS,
|
||||
)
|
||||
} else {
|
||||
let str = chars
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
|
||||
.map(|(_, c)| c)
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.len();
|
||||
|
||||
(DynamicStringSegment::Static(str), len)
|
||||
};
|
||||
|
||||
assert_ne!(skip, 0);
|
||||
|
||||
segments.push(token);
|
||||
chars.drain(..skip);
|
||||
}
|
||||
|
||||
let label_parts = Arc::new(Mutex::new(IndexMap::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).insert(i, str);
|
||||
}
|
||||
DynamicStringSegment::Dynamic(script) => {
|
||||
let tx = tx.clone();
|
||||
let label_parts = label_parts.clone();
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(|(out, _)| {
|
||||
if let OutputStream::Stdout(out) = out {
|
||||
let mut label_parts = lock!(label_parts);
|
||||
|
||||
label_parts.insert(i, out);
|
||||
|
||||
let string = label_parts
|
||||
.iter()
|
||||
.map(|(_, part)| part.as_str())
|
||||
.collect::<String>();
|
||||
|
||||
send!(tx, string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize
|
||||
{
|
||||
let label_parts = lock!(label_parts)
|
||||
.iter()
|
||||
.map(|(_, part)| part.as_str())
|
||||
.collect::<String>();
|
||||
|
||||
send!(tx, label_parts);
|
||||
}
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test() {
|
||||
// TODO: see if we can run gtk tests in ci
|
||||
if gtk::init().is_ok() {
|
||||
let label = gtk::Label::new(None);
|
||||
DynamicString::new(
|
||||
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
|
||||
move |string| {
|
||||
label.set_label(&string);
|
||||
Continue(true)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/dynamic_value/dynamic_bool.rs
Normal file
78
src/dynamic_value/dynamic_bool.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::Script;
|
||||
use crate::send;
|
||||
use cfg_if::cfg_if;
|
||||
use glib::Continue;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum DynamicBool {
|
||||
/// Either a script or variable, to be determined.
|
||||
Unknown(String),
|
||||
Script(Script),
|
||||
#[cfg(feature = "ipc")]
|
||||
Variable(Box<str>),
|
||||
}
|
||||
|
||||
impl DynamicBool {
|
||||
pub fn subscribe<F>(self, f: F)
|
||||
where
|
||||
F: FnMut(bool) -> Continue + 'static,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown(input) => {
|
||||
if input.starts_with('#') {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
Self::Variable(input.into())
|
||||
} else {
|
||||
Self::Unknown(input)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let script = Script::from(input.as_str());
|
||||
Self::Script(script)
|
||||
}
|
||||
}
|
||||
_ => self,
|
||||
};
|
||||
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
rx.attach(None, f);
|
||||
|
||||
spawn(async move {
|
||||
match value {
|
||||
DynamicBool::Script(script) => {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicBool::Variable(variable) => {
|
||||
let variable_manager = get_variable_manager();
|
||||
|
||||
let variable_name = variable[1..].into(); // remove hash
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||
send!(tx, has_value);
|
||||
}
|
||||
}
|
||||
DynamicBool::Unknown(_) => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a string ironvar is 'truthy'
|
||||
#[cfg(feature = "ipc")]
|
||||
fn is_truthy(string: &str) -> bool {
|
||||
!(string.is_empty() || string == "0" || string == "false")
|
||||
}
|
||||
321
src/dynamic_value/dynamic_string.rs
Normal file
321
src/dynamic_value/dynamic_string.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{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;
|
||||
@@ -5,9 +5,11 @@ pub enum ExitCode {
|
||||
Config = 3,
|
||||
}
|
||||
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";
|
||||
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
||||
pub const ERR_READ_LOCK: &str = "Failed to get read lock";
|
||||
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
|
||||
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
|
||||
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
|
||||
|
||||
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";
|
||||
|
||||
8
src/gtk_helpers.rs
Normal file
8
src/gtk_helpers.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use glib::IsA;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Widget;
|
||||
|
||||
/// Adds a new CSS class to a widget.
|
||||
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
|
||||
widget.style_context().add_class(class);
|
||||
}
|
||||
139
src/icon.rs
139
src/icon.rs
@@ -1,139 +0,0 @@
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Gets directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![PathBuf::from("/usr/share/applications")];
|
||||
let user_dir = dirs::data_local_dir();
|
||||
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
user_dir.push("applications");
|
||||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
/// (or app class).
|
||||
///
|
||||
/// A simple case-insensitive check is performed on filename == `app_id`.
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
|
||||
for dir in dirs {
|
||||
let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
|
||||
|
||||
let entry = walker.find(|entry| match entry {
|
||||
Ok(entry) => {
|
||||
let file_name = entry.file_name().to_string_lossy().to_lowercase();
|
||||
let test_name = format!("{}.desktop", app_id.to_lowercase());
|
||||
file_name == test_name
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(Ok(entry)) = entry {
|
||||
let path = entry.path().to_owned();
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a flat hashmap of keys/values.
|
||||
fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
|
||||
let file = File::open(path)?;
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for line in lines.flatten() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
find_desktop_file(app_id).and_then(|file| {
|
||||
let map = parse_desktop_file(file);
|
||||
map.map_or(None, |map| {
|
||||
map.get("Icon").map(std::string::ToString::to_string)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
enum IconLocation {
|
||||
Theme(String),
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
/// Attempts to get the location of an icon.
|
||||
///
|
||||
/// Handles icons that are part of a GTK theme, icons specified as path
|
||||
/// and icons for steam games.
|
||||
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
|
||||
let has_icon = theme
|
||||
.lookup_icon(app_id, size, IconLookupFlags::empty())
|
||||
.is_some();
|
||||
|
||||
if has_icon {
|
||||
return Some(IconLocation::Theme(app_id.to_string()));
|
||||
}
|
||||
|
||||
let is_steam_game = app_id.starts_with("steam_app_");
|
||||
if is_steam_game {
|
||||
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
|
||||
|
||||
return match dirs::data_dir() {
|
||||
Some(dir) => {
|
||||
let path = dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
));
|
||||
|
||||
return Some(IconLocation::File(path));
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
let icon_name = get_desktop_icon_name(app_id);
|
||||
if let Some(icon_name) = icon_name {
|
||||
let is_path = PathBuf::from(&icon_name).exists();
|
||||
|
||||
return if is_path {
|
||||
Some(IconLocation::File(PathBuf::from(icon_name)))
|
||||
} else {
|
||||
return Some(IconLocation::Theme(icon_name));
|
||||
};
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Gets the icon associated with an app.
|
||||
pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
|
||||
let icon_location = get_icon_location(theme, app_id, size);
|
||||
|
||||
match icon_location {
|
||||
Some(IconLocation::Theme(icon_name)) => {
|
||||
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
|
||||
icon.map_or(None, |icon| icon)
|
||||
}
|
||||
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
55
src/image/gtk.rs
Normal file
55
src/image/gtk.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
let button = Button::new();
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
add_class(&image, "image");
|
||||
add_class(&image, "icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, size)
|
||||
.map(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Some(_) => {
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
None => {
|
||||
button.set_label(input);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button.set_label(input);
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
#[cfg(feature = "music")]
|
||||
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
add_class(&image, "icon");
|
||||
add_class(&image, "image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
ImageProvider::parse(input, icon_theme, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
add_class(&label, "icon");
|
||||
add_class(&label, "text-icon");
|
||||
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
7
src/image/mod.rs
Normal file
7
src/image/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
mod gtk;
|
||||
mod provider;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
pub use self::gtk::*;
|
||||
pub use provider::ImageProvider;
|
||||
251
src/image/provider.rs
Normal file
251
src/image/provider.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::desktop_file::get_desktop_icon_name;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::warn;
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use crate::send;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ImageLocation<'a> {
|
||||
Icon {
|
||||
name: String,
|
||||
theme: &'a IconTheme,
|
||||
},
|
||||
Local(PathBuf),
|
||||
Steam(String),
|
||||
#[cfg(feature = "http")]
|
||||
Remote(reqwest::Url),
|
||||
}
|
||||
|
||||
pub struct ImageProvider<'a> {
|
||||
location: ImageLocation<'a>,
|
||||
size: i32,
|
||||
}
|
||||
|
||||
impl<'a> ImageProvider<'a> {
|
||||
/// Attempts to parse the image input to find its location.
|
||||
/// Errors if no valid location type can be found.
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size)?;
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
}
|
||||
|
||||
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
}),
|
||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
||||
input_name[2..].to_string(),
|
||||
))),
|
||||
#[cfg(feature = "http")]
|
||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||
input.parse().ok().map(ImageLocation::Remote)
|
||||
}
|
||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
})
|
||||
}
|
||||
Some(input_type) => {
|
||||
warn!(
|
||||
"{:?}",
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to fetch the image from the location
|
||||
/// and load it into the provided `GTK::Image` widget.
|
||||
pub fn load_into_image(&self, image: gtk::Image) -> Result<()> {
|
||||
// handle remote locations async to avoid blocking UI thread while downloading
|
||||
#[cfg(feature = "http")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
send!(tx, bytes);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let size = self.size;
|
||||
rx.attach(None, move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
);
|
||||
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
||||
{
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Continue(false)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.load_into_image_sync(&image)?;
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
self.load_into_image_sync(&image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||
let scale = image.scale_factor();
|
||||
|
||||
let pixbuf = match &self.location {
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
||||
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
||||
#[cfg(feature = "http")]
|
||||
_ => unreachable!(), // handled above
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image, scale)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image, scale: i32) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr =
|
||||
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||
let pixbuf =
|
||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||
let scaled_size = self.size * scale;
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||
Ok(pixbuf)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file,
|
||||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| {
|
||||
Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
|
||||
self.get_from_file(&path, scale)
|
||||
}
|
||||
|
||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||
#[cfg(feature = "http")]
|
||||
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
|
||||
let res = reqwest::get(url).await?;
|
||||
|
||||
let status = res.status();
|
||||
if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ fn install_tracing() -> Result<WorkerGuard> {
|
||||
const DEFAULT_LOG: &str = "info";
|
||||
const DEFAULT_FILE_LOG: &str = "warn";
|
||||
|
||||
let fmt_layer = fmt::layer().with_target(true);
|
||||
let fmt_layer = fmt::layer().with_target(true).with_line_number(true);
|
||||
let filter_layer =
|
||||
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Sends a message on an asynchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// send_async!(tx, "my message");
|
||||
@@ -16,7 +16,7 @@ macro_rules! send_async {
|
||||
/// Sends a message on an synchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// send!(tx, "my message");
|
||||
@@ -31,7 +31,7 @@ macro_rules! send {
|
||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// try_send!(tx, "my message");
|
||||
@@ -46,22 +46,23 @@ macro_rules! try_send {
|
||||
/// Locks a `Mutex`.
|
||||
/// Panics if the `Mutex` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut val = lock!(my_mutex);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! lock {
|
||||
($mutex:expr) => {
|
||||
($mutex:expr) => {{
|
||||
tracing::trace!("Locking {}", std::stringify!($mutex));
|
||||
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
|
||||
};
|
||||
}};
|
||||
}
|
||||
|
||||
/// Gets a read lock on a `RwLock`.
|
||||
/// Panics if the `RwLock` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = read_lock!(my_rwlock);
|
||||
@@ -76,7 +77,7 @@ macro_rules! read_lock {
|
||||
/// Gets a write lock on a `RwLock`.
|
||||
/// Panics if the `RwLock` cannot be locked.
|
||||
///
|
||||
/// Usage:
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let mut val = write_lock!(my_rwlock);
|
||||
@@ -87,3 +88,33 @@ macro_rules! write_lock {
|
||||
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
||||
};
|
||||
}
|
||||
|
||||
/// Wraps `val` in a new `Arc<Mutex<T>>`.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = arc_mut!(MyService::new());
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! arc_mut {
|
||||
($val:expr) => {
|
||||
std::sync::Arc::new(std::Sync::Mutex::new($val))
|
||||
};
|
||||
}
|
||||
|
||||
/// Wraps `val` in a new `Arc<RwLock<T>>`.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = arc_rw!(MyService::new());
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! arc_rw {
|
||||
($val:expr) => {
|
||||
std::sync::Arc::new(std::sync::RwLock::new($val))
|
||||
};
|
||||
}
|
||||
|
||||
124
src/main.rs
124
src/main.rs
@@ -1,52 +1,111 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod bar;
|
||||
mod bridge_channel;
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli;
|
||||
mod clients;
|
||||
mod config;
|
||||
mod dynamic_string;
|
||||
mod desktop_file;
|
||||
mod dynamic_value;
|
||||
mod error;
|
||||
mod icon;
|
||||
mod gtk_helpers;
|
||||
mod image;
|
||||
#[cfg(feature = "ipc")]
|
||||
mod ipc;
|
||||
#[cfg(feature = "ipc")]
|
||||
mod ironvar;
|
||||
mod logging;
|
||||
mod macros;
|
||||
mod modules;
|
||||
mod popup;
|
||||
mod script;
|
||||
mod style;
|
||||
mod unique_id;
|
||||
|
||||
use crate::bar::create_bar;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::style::load_css;
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use dirs::config_dir;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use std::cell::Cell;
|
||||
use std::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::block_in_place;
|
||||
use tokio::task::{block_in_place, spawn_blocking};
|
||||
|
||||
use crate::error::ExitCode;
|
||||
use clients::wayland::{self, WaylandClient};
|
||||
use clients::wayland;
|
||||
use tracing::{debug, error, info};
|
||||
use universal_config::ConfigLoader;
|
||||
|
||||
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
async fn main() {
|
||||
let _guard = logging::install_logging();
|
||||
|
||||
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!("Starting application");
|
||||
|
||||
let wayland_client = wayland::get_client().await;
|
||||
|
||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||
let _ = wayland::get_client(); // force-init
|
||||
|
||||
let running = Rc::new(Cell::new(false));
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
if running.get() {
|
||||
info!("Ironbar already running, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
running.set(true);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
let ipc = ipc::Ipc::new();
|
||||
ipc.start();
|
||||
}
|
||||
}
|
||||
|
||||
let display = Display::default().map_or_else(
|
||||
|| {
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
@@ -56,16 +115,32 @@ async fn main() -> Result<()> {
|
||||
|display| display,
|
||||
);
|
||||
|
||||
let config = match Config::load() {
|
||||
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|
||||
|_| ConfigLoader::new("ironbar").find_and_load(),
|
||||
ConfigLoader::load,
|
||||
);
|
||||
|
||||
let mut config: Config = match config_res {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::Config as i32)
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Loaded config file");
|
||||
|
||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||
#[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);
|
||||
exit(ExitCode::CreateBars as i32);
|
||||
}
|
||||
@@ -89,23 +164,33 @@ async fn main() -> Result<()> {
|
||||
if style_path.exists() {
|
||||
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
|
||||
// Some are provided by swaybar_config but not currently supported
|
||||
app.run_with_args(&Vec::<&str>::new());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates each of the bars across each of the (configured) outputs.
|
||||
fn create_bars(
|
||||
app: &Application,
|
||||
display: &Display,
|
||||
wl: &WaylandClient,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
let outputs = wl.outputs.as_slice();
|
||||
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
|
||||
let wl = wayland::get_client();
|
||||
let outputs = lock!(wl).get_outputs();
|
||||
|
||||
debug!("Received {} outputs from Wayland", outputs.len());
|
||||
debug!("Outputs: {:?}", outputs);
|
||||
@@ -119,7 +204,8 @@ fn create_bars(
|
||||
let output = outputs
|
||||
.get(i as usize)
|
||||
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
||||
let monitor_name = &output.name;
|
||||
|
||||
let Some(monitor_name) = &output.name else { continue };
|
||||
|
||||
config.monitors.as_ref().map_or_else(
|
||||
|| {
|
||||
|
||||
318
src/modules/clipboard.rs
Normal file
318
src/modules/clipboard.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use crate::clients::clipboard::{self, ClipboardEvent};
|
||||
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::try_send;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ClipboardModule {
|
||||
#[serde(default = "default_icon")]
|
||||
icon: String,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
#[serde(default = "default_max_items")]
|
||||
max_items: usize,
|
||||
|
||||
// -- Common --
|
||||
truncate: Option<TruncateMode>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
fn default_icon() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
const fn default_max_items() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ControllerEvent {
|
||||
Add(usize, Arc<ClipboardItem>),
|
||||
Remove(usize),
|
||||
Activate(usize),
|
||||
Deactivate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UIEvent {
|
||||
Copy(usize),
|
||||
Remove(usize),
|
||||
}
|
||||
|
||||
impl Module<Button> for ClipboardModule {
|
||||
type SendMessage = ControllerEvent;
|
||||
type ReceiveMessage = UIEvent;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"clipboard"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> color_eyre::Result<()> {
|
||||
let max_items = self.max_items;
|
||||
|
||||
// listen to clipboard events
|
||||
spawn(async move {
|
||||
let mut rx = {
|
||||
let client = clipboard::get_client();
|
||||
client.subscribe(max_items)
|
||||
};
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
ClipboardEvent::Add(item) => {
|
||||
let msg = match &item.value {
|
||||
ClipboardValue::Other => {
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
|
||||
}
|
||||
_ => ModuleUpdateEvent::Update(ControllerEvent::Add(item.id, item)),
|
||||
};
|
||||
try_send!(tx, msg);
|
||||
}
|
||||
ClipboardEvent::Remove(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Remove(id)));
|
||||
}
|
||||
ClipboardEvent::Activate(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Activate(id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("Clipboard client unexpectedly closed");
|
||||
});
|
||||
|
||||
// listen to ui events
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let client = clipboard::get_client();
|
||||
match event {
|
||||
UIEvent::Copy(id) => client.copy(id),
|
||||
UIEvent::Remove(id) => client.remove(id),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> color_eyre::Result<ModuleWidget<Button>> {
|
||||
let position = info.bar_position;
|
||||
|
||||
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
||||
button.style_context().add_class("btn");
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
let pos = Popup::widget_geometry(button, position.get_orientation());
|
||||
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
||||
});
|
||||
|
||||
// we need to bind to the receiver as the channel does not open
|
||||
// until the popup is first opened.
|
||||
context.widget_rx.attach(None, |_| Continue(true));
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||
|
||||
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
||||
container.add(&entries);
|
||||
|
||||
let hidden_option = RadioButton::new();
|
||||
entries.add(&hidden_option);
|
||||
|
||||
let mut items = HashMap::new();
|
||||
|
||||
{
|
||||
let hidden_option = hidden_option.clone();
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
ControllerEvent::Add(id, item) => {
|
||||
debug!("Adding new value with ID {}", id);
|
||||
|
||||
let row = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
row.style_context().add_class("item");
|
||||
|
||||
let button = match &item.value {
|
||||
ClipboardValue::Text(value) => {
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
|
||||
let label = Label::new(Some(value));
|
||||
button.add(&label);
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
}
|
||||
|
||||
button.style_context().add_class("text");
|
||||
button
|
||||
}
|
||||
ClipboardValue::Image(bytes) => {
|
||||
let stream = MemoryInputStream::from_bytes(bytes);
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
128,
|
||||
64,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
)
|
||||
.expect("Failed to read Pixbuf from stream");
|
||||
let image = Image::from_pixbuf(Some(&pixbuf));
|
||||
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
button.style_context().add_class("image");
|
||||
|
||||
button
|
||||
}
|
||||
ClipboardValue::Other => unreachable!(),
|
||||
};
|
||||
|
||||
button.style_context().add_class("btn");
|
||||
button.set_active(true); // if just added, should be on clipboard
|
||||
|
||||
let button_wrapper = EventBox::new();
|
||||
button_wrapper.add(&button);
|
||||
|
||||
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
||||
button_wrapper.set_above_child(true);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
button_wrapper.connect_button_press_event(
|
||||
move |button_wrapper, event| {
|
||||
// left click
|
||||
if event.button() == 1 {
|
||||
let id = get_button_id(button_wrapper)
|
||||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Copying item with id: {id}");
|
||||
try_send!(tx, UIEvent::Copy(id));
|
||||
}
|
||||
|
||||
Inhibit(true)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let remove_button = Button::with_label("x");
|
||||
remove_button.set_widget_name(&format!("remove-{id}"));
|
||||
remove_button.style_context().add_class("btn-remove");
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let entries = entries.clone();
|
||||
let row = row.clone();
|
||||
|
||||
remove_button.connect_clicked(move |button| {
|
||||
let id = get_button_id(button)
|
||||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Removing item with id: {id}");
|
||||
try_send!(tx, UIEvent::Remove(id));
|
||||
|
||||
entries.remove(&row);
|
||||
});
|
||||
}
|
||||
|
||||
row.add(&button_wrapper);
|
||||
row.pack_end(&remove_button, false, false, 0);
|
||||
|
||||
entries.add(&row);
|
||||
entries.reorder_child(&row, 0);
|
||||
row.show_all();
|
||||
|
||||
items.insert(id, (row, button));
|
||||
}
|
||||
ControllerEvent::Remove(id) => {
|
||||
debug!("Removing option with ID {id}");
|
||||
let row = items.remove(&id);
|
||||
if let Some((row, button)) = row {
|
||||
if button.is_active() {
|
||||
hidden_option.set_active(true);
|
||||
}
|
||||
|
||||
entries.remove(&row);
|
||||
}
|
||||
}
|
||||
ControllerEvent::Activate(id) => {
|
||||
debug!("Activating option with ID {id}");
|
||||
|
||||
hidden_option.set_active(false);
|
||||
let row = items.get(&id);
|
||||
if let Some((_, button)) = row {
|
||||
button.set_active(true);
|
||||
}
|
||||
}
|
||||
ControllerEvent::Deactivate => {
|
||||
debug!("Deactivating current option");
|
||||
hidden_option.set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
hidden_option.hide();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the ID from a widget's name.
|
||||
///
|
||||
/// This expects the button name to be
|
||||
/// in the format `<purpose>-<id>`.
|
||||
fn get_button_id<W>(button_wrapper: &W) -> Option<usize>
|
||||
where
|
||||
W: IsA<Widget>,
|
||||
{
|
||||
button_wrapper
|
||||
.widget_name()
|
||||
.split_once('-')
|
||||
.and_then(|(_, id)| id.parse().ok())
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{send_async, try_send};
|
||||
@@ -69,7 +70,7 @@ impl Module<Button> for ClockModule {
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation))
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -82,7 +83,7 @@ impl Module<Button> for ClockModule {
|
||||
});
|
||||
}
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
@@ -94,21 +95,18 @@ impl Module<Button> for ClockModule {
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.name("popup-clock")
|
||||
.build();
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
let clock = Label::builder()
|
||||
.name("calendar-clock")
|
||||
.halign(Align::Center)
|
||||
.build();
|
||||
let clock = Label::builder().halign(Align::Center).build();
|
||||
add_class(&clock, "calendar-clock");
|
||||
let format = "%H:%M:%S";
|
||||
|
||||
container.add(&clock);
|
||||
|
||||
let calendar = Calendar::builder().name("calendar").build();
|
||||
let calendar = Calendar::new();
|
||||
add_class(&calendar, "calendar");
|
||||
container.add(&calendar);
|
||||
|
||||
{
|
||||
@@ -119,6 +117,8 @@ impl Module<Button> for ClockModule {
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::dynamic_string::DynamicString;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::{ButtonGeometry, Popup};
|
||||
use crate::script::Script;
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CustomModule {
|
||||
/// Container class name
|
||||
class: Option<String>,
|
||||
/// Widgets to add to the bar container
|
||||
bar: Vec<Widget>,
|
||||
/// Widgets to add to the popup container
|
||||
popup: Option<Vec<Widget>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
/// Attempts to parse an `Orientation` from `String`
|
||||
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||
match orientation.to_lowercase().as_str() {
|
||||
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget attributes
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Widget {
|
||||
/// Type of GTK widget to add
|
||||
#[serde(rename = "type")]
|
||||
widget_type: WidgetType,
|
||||
widgets: Option<Vec<Widget>>,
|
||||
label: Option<String>,
|
||||
name: Option<String>,
|
||||
class: Option<String>,
|
||||
on_click: Option<String>,
|
||||
orientation: Option<String>,
|
||||
}
|
||||
|
||||
/// Supported GTK widget types
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WidgetType {
|
||||
Box,
|
||||
Label,
|
||||
Button,
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(self, parent: >k::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
|
||||
match self.widget_type {
|
||||
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
|
||||
WidgetType::Label => parent.add(&self.into_label()),
|
||||
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Box` from this widget
|
||||
fn into_box(self, tx: &Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
|
||||
let mut builder = gtk::Box::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
builder = builder
|
||||
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
|
||||
}
|
||||
|
||||
let container = builder.build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
widgets
|
||||
.into_iter()
|
||||
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation));
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Label` from this widget
|
||||
fn into_label(self) -> Label {
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
let label = builder.build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
label.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
let text = self.label.map_or_else(String::new, |text| text);
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
DynamicString::new(&text, move |string| {
|
||||
label.set_label(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Button` from this widget
|
||||
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
|
||||
let mut builder = Button::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
let button = builder.build();
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
label.set_markup(&text);
|
||||
button.add(&label);
|
||||
}
|
||||
|
||||
if let Some(class) = self.class {
|
||||
button.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(exec) = self.on_click {
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
geometry: Popup::button_pos(button, bar_orientation),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
geometry: ButtonGeometry,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
type SendMessage = ();
|
||||
type ReceiveMessage = ExecEvent;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if event.cmd.starts_with('!') {
|
||||
let script = Script::from(&event.cmd[1..]);
|
||||
|
||||
debug!("executing command: '{}'", script.cmd);
|
||||
// TODO: Migrate to use script.run
|
||||
if let Err(err) = script.get_output().await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
|
||||
} else if event.cmd == "popup:open" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
} else {
|
||||
error!("Received invalid command: '{}'", event.cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let container = gtk::Box::builder().orientation(orientation).build();
|
||||
|
||||
if let Some(ref class) = self.class {
|
||||
container.style_context().add_class(class);
|
||||
}
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
widget.add_to(&container, context.controller_tx.clone(), orientation);
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder().name("popup-custom").build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container
|
||||
.style_context()
|
||||
.add_class(format!("popup-{class}").as_str());
|
||||
}
|
||||
|
||||
if let Some(popup) = self.popup {
|
||||
popup
|
||||
.into_iter()
|
||||
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
|
||||
}
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user