86 Commits

Author SHA1 Message Date
Jake Stanger
6836abefd1 chore(release): v0.12.0 2023-05-06 13:37:22 +01:00
Jake Stanger
91e766d6ab Merge pull request #140 from JakeStanger/feat/better-styles
[BREAKING] module-level `name` and `class` options
2023-05-06 13:36:27 +01:00
Jake Stanger
dea66415c2 feat: module-level name and class options
BREAKING CHANGE: 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.

Resolves #75.
2023-05-06 13:22:35 +01:00
Jake Stanger
528a8d6dd6 Merge pull request #130 from JakeStanger/refactor/wayland-0.30
Update Wayland libraries
2023-05-05 22:41:31 +01:00
Jake Stanger
e1abadcf39 fix(clipboard): copying large images filling write pipe
Fixes partially #86
2023-05-05 22:30:16 +01:00
Jake Stanger
cf32870f8a docs(compiling): add ron feature flag 2023-05-05 22:29:26 +01:00
Jake Stanger
139bc5d23f docs(compiling): improve requirements list 2023-05-05 22:29:16 +01:00
Jake Stanger
735f5cc9f1 fix(launcher): crash when focusing window
Fixes #41 🎉
2023-05-04 20:07:46 +01:00
Jake Stanger
aed04c1ccf chore: add trace logging for mutex locks 2023-05-04 20:07:46 +01:00
Jake Stanger
c1ea5fad7e feat(logging): include line numbers 2023-05-04 20:07:46 +01:00
Jake Stanger
38da59cd41 refactor: fix a few pedantic clippy warnings 2023-05-04 20:07:46 +01:00
Jake Stanger
7f46cb4976 refactor(wayland): update to 0.30.0
This is pretty much a rewrite of the Wayland client code for `wayland-client` and `wayland-protocols` v0.30.0, and `smithay-client-toolkit` v0.17.0
2023-05-04 20:07:42 +01:00
Jake Stanger
5c18ec8ba0 Merge pull request #138 from JakeStanger/build/ron-support
build: enable support for `ron` config lang
2023-05-03 21:49:39 +01:00
Jake Stanger
81acc176ed build: enable support for ron config lang 2023-05-03 20:15:37 +01:00
Jake Stanger
618b7ef552 docs: improve example css 2023-05-02 23:08:49 +01:00
Jake Stanger
2a155b9aa8 feat(music): add css selector for button contents 2023-05-02 23:08:13 +01:00
Jake Stanger
bc87c7f0d4 chore: fix docs typo 2023-05-01 14:17:21 +01:00
Jake Stanger
bde469816a Merge pull request #135 from JakeStanger/oknozor/master
fix: fallback to default icon theme for notifier items
2023-05-01 14:11:05 +01:00
Paul Delafosse
98aaaa0d14 fix: fallback to default icon theme for notifier items 2023-05-01 14:10:43 +01:00
Jake Stanger
51e95d9e01 Merge remote-tracking branch 'origin/master' 2023-05-01 13:31:31 +01:00
Jake Stanger
ea9f7caaf7 docs: add missing upower feature flag 2023-05-01 13:30:57 +01:00
Jake Stanger
338829e275 Merge pull request #133 from JakeStanger/update_flake_lock_action
Update flake.lock
2023-05-01 13:26:08 +01:00
Jake Stanger
610c3528af docs: add missing link to upower page 2023-05-01 13:24:38 +01:00
github-actions[bot]
f95e1e8f74 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/e3652e0735fbec227f342712f180f4f21f0594f2' (2023-03-30)
  → 'github:nixos/nixpkgs/08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b' (2023-04-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/aa480d799023141e1b9e5d6108700de63d9ad002' (2023-03-31)
  → 'github:oxalica/rust-overlay/1be440e9119e69b68151cd9c84876ff3063a2e45' (2023-04-30)
• Updated input 'rust-overlay/flake-utils':
    'github:numtide/flake-utils/c0e246b9b83f637f4681389ecabcb2681b4f3af0' (2022-08-07)
  → 'github:numtide/flake-utils/cfacdce06f30d2b68473a46042957675eebb3401' (2023-04-11)
• Added input 'rust-overlay/flake-utils/systems':
    'github:nix-systems/default/da67096a3b9bf56a91d16901293e51ba5b49a27e' (2023-04-09)
2023-05-01 00:58:57 +00:00
Jake Stanger
35dfbbf91d Merge pull request #132 from JakeStanger/fix/multiple-instances
fix: bars duplicate when starting second instance
2023-04-30 19:55:25 +01:00
Jake Stanger
14b6c1a69f fix: bars duplicate when starting second instance
This ensures that starting `ironbar` while an instance already running causes the 2nd instance to cleanly exit, and avoids launching the init code a second time.
2023-04-30 19:43:58 +01:00
Jake Stanger
0e3102de8c Merge pull request #83 from p00f/upower-string
implement upower module
2023-04-30 00:26:34 +01:00
Chinmay Dalal
ad3c171eca feat: implement upower module 2023-04-30 00:15:04 +01:00
Jake Stanger
e5bc44168f Merge pull request #125 from JakeStanger/feat/custom-slider-label
feat(custom): option to toggle slider label
2023-04-23 17:35:40 +01:00
Jake Stanger
cc62927f15 Merge pull request #124 from JakeStanger/feat/music-status-icon
feat(music): option to hide status icon on widget
2023-04-23 17:35:19 +01:00
Jake Stanger
76e2b7ba3e feat(music): option to hide status icon on widget
Adds new `show_status_icon` option.

Resolves #97.
2023-04-23 13:00:37 +01:00
Jake Stanger
033d0f7e6e feat(custom): option to toggle slider label
Adds new `show_label` option.

Resolves #115 (for real this time).
2023-04-23 12:59:55 +01:00
Jake Stanger
dc16b1e15a Merge pull request #122 from yavko/fix-nix-pixbuf-loader
Attempt to fix image blurriness on nix
2023-04-23 11:10:04 +01:00
Jake Stanger
03cd263095 Merge pull request #121 from JakeStanger/fix/icon-scale
fix(image): not scaling images for hidpi
2023-04-23 11:07:49 +01:00
Jake Stanger
db0868a3fc fix(image): not scaling icons for hidpi 2023-04-23 11:07:19 +01:00
Yavor Kolev
0382b50cf4 Merge branch 'JakeStanger:master' into fix-nix-pixbuf-loader 2023-04-22 16:49:19 -07:00
yavko
338f5a0e1b fix(nix): Attempt to fix image blurriness 2023-04-22 16:47:04 -07:00
Jake Stanger
20949a7744 Merge pull request #120 from JakeStanger/feat/icon-sizes
feat: ability to configure image icon sizes
2023-04-22 22:35:21 +01:00
Jake Stanger
2da28b9bf5 feat: ability to configure image icon sizes
Adds `icon_size` option to following widgets:

- `clipboard`
- `launcher`
- `music`
- `workspaces`

Also adds `cover_image_size` option to `music`.
2023-04-22 22:22:49 +01:00
Jake Stanger
618e97f1e8 Merge pull request #119 from JakeStanger/feat/slider-step
Custom slider widget step option
2023-04-22 21:39:50 +01:00
Jake Stanger
dd7c9f30db docs: add transition module-level options 2023-04-22 21:29:47 +01:00
Jake Stanger
1fa0c0e977 feat(custom): support mouse wheel on slider 2023-04-22 21:29:47 +01:00
Jake Stanger
74d18aedfb Merge pull request #118 from JakeStanger/fix/dynamic-string
fix(dynamic string): crash when last segment is static and a single char
2023-04-22 16:40:33 +01:00
Jake Stanger
2c88c99cb6 fix(dynamic string): crash when last segment is static and a single char
Resolves #117.
2023-04-22 16:29:54 +01:00
Jake Stanger
236bb09170 Merge pull request #116 from JakeStanger/feat/revealer
feat: wrap modules in a revealer to support animated show/hide
2023-04-22 15:24:20 +01:00
Jake Stanger
83f44fd92f feat: wrap modules in a revealer to support animated show/hide
Resolves #72.
2023-04-22 14:49:15 +01:00
Jake Stanger
1855416db4 Merge pull request #114 from JakeStanger/feat/common-in-custom
feat(custom): support common options in widgets
2023-04-22 13:48:08 +01:00
Jake Stanger
e63509a3a7 refactor: fix a few new clippy warnings 2023-04-22 13:45:44 +01:00
Jake Stanger
4a09b70854 feat(custom): support common options in widgets 2023-04-22 13:34:39 +01:00
Jake Stanger
9d09855fce Merge pull request #109 from JakeStanger/fix/tray-icons
fix(tray): icons sometimes not showing
2023-04-22 11:01:06 +01:00
Jake Stanger
e9d0273176 Merge pull request #113 from yavko/fix-nix-run
Fix `nix run` support
2023-04-22 10:47:37 +01:00
yavko
7926bb07eb fix(nix): Fix nix run support 2023-04-21 21:43:55 -07:00
Jake Stanger
6fd69d657c refactor: move module creation code to module module 2023-04-21 23:51:54 +01:00
Jake Stanger
27d11de661 refactor(config): split common code into separate file 2023-04-21 23:51:29 +01:00
Jake Stanger
07df51c249 docs: include readme in rust docs 2023-04-21 23:50:49 +01:00
Jake Stanger
b038e7671a fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-21 23:02:53 +01:00
Jake Stanger
e5ab9f33b5 Merge remote-tracking branch 'origin/fix/tray-icons' into fix/tray-icons 2023-04-21 22:33:59 +01:00
Jake Stanger
68bc8230dd fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-21 22:31:09 +01:00
Jake Stanger
246313136f Merge pull request #111 from JakeStanger/fix/script-parsing
fix(script): parser incorrectly handling colons
2023-04-21 20:37:13 +01:00
Jake Stanger
15a9d8d42c fix(script): parser incorrectly handling colons
The short input parser was previously splitting colons, and incorrectly handling situations where the `cmd` section contained colons. The parser now properly checks input in the `mode:interval:cmd` format, moving onto the next section regardless of whether the previous was found.

This means unless your script literally starts with `poll:` or `5000:` you won't hit this issue anymore.
2023-04-20 21:59:23 +01:00
Jake Stanger
a87d8d5c30 fix(tray): icons sometimes not showing
Previously icons were only loaded from the theme based on the provided icon name. Sometimes no icon name was provided, and sometimes the name is just missing from the theme.

This falls back to using the provided pixbuf, and then falls back to just displaying the name as text if that is not available.
2023-04-16 21:37:47 +01:00
Jake Stanger
8e99fd4d0f chore(system tray): add debug logging 2023-04-16 19:55:17 +01:00
Jake Stanger
1e1d65ae49 chore(script): add debug logging 2023-04-13 12:47:26 +01:00
Jake Stanger
2815cef440 Merge pull request #106 from JakeStanger/feat/custom-dynamic-image
Custom image dynamic src support
2023-04-10 20:17:58 +01:00
Jake Stanger
138b5b3903 docs(custom): fix potential error in progress example 2023-04-10 20:05:37 +01:00
Jake Stanger
7355db74ec fix(image): http provider not handling non-success codes 2023-04-10 20:05:13 +01:00
Jake Stanger
c214f65ecb refactor: fix strict clippy warnings 2023-04-10 20:04:59 +01:00
Jake Stanger
3d308ab572 feat(custom): support dynamic string in image source
Resolves #94.
2023-04-10 20:04:36 +01:00
Jake Stanger
b770ae716c Merge pull request #104 from JakeStanger/feat/custom-widgets
Custom module improvements
2023-04-10 14:02:42 +01:00
Jake Stanger
3613aef5c5 refactor(custom): reduce a lot of repeated code 2023-04-10 13:51:07 +01:00
Jake Stanger
a9d1233909 feat(custom): support dynamic strings on buttons 2023-04-10 13:49:09 +01:00
Jake Stanger
72b14b6c4e feat(custom): progress bar widget.
Resolves partially #68.
2023-04-10 12:59:24 +01:00
Jake Stanger
910945306c fix(dynamic string): parser issue related to incorrectly matching braces 2023-04-10 00:17:09 +01:00
Jake Stanger
dfe1964abf feat(custom): slider widget
Resolves partially #68.
2023-04-10 00:17:09 +01:00
Jake Stanger
e928b30f99 docs(custom): rewrite widget options to be clearer 2023-04-10 00:16:44 +01:00
Jake Stanger
2ab06f044e refactor(custom): split into enum with separate file per widget 2023-04-07 20:24:41 +01:00
Jake Stanger
4b4f1ffc21 Merge pull request #103 from JakeStanger/feat/popup-gap-config
feat: ability to configure popup gap
2023-04-07 15:02:58 +01:00
Jake Stanger
0691db3b87 Merge pull request #102 from JakeStanger/feat/labels
feat: new label module
2023-04-07 14:53:56 +01:00
Jake Stanger
cac064f479 feat: ability to configure popup gap 2023-04-07 14:53:18 +01:00
Jake Stanger
6c622864b3 feat: new label module
Takes a text label, with the ability to include embedded scripts.

Resolves #80.
2023-04-07 14:29:07 +01:00
Jake Stanger
55c06c4766 chore: bash script for regenerating examples 2023-04-07 14:26:17 +01:00
JakeStanger
1b0287becc docs: update CHANGELOG.md for v0.11.0 [skip ci] 2023-04-01 17:44:26 +00:00
Jake Stanger
7bf44ca75d chore(release): v0.11.0 2023-04-01 18:36:24 +01:00
Jake Stanger
fb04ceab7d Merge pull request #95 from JakeStanger/feat/module-hover
feat: module hover options
2023-04-01 18:17:52 +01:00
Jake Stanger
102d2478a9 feat: module hover options
Resolves #70.
2023-04-01 13:29:40 +01:00
Jake Stanger
80a414ab67 build: update deps
Resolves #93
2023-04-01 13:12:26 +01:00
93 changed files with 5090 additions and 3852 deletions

View File

@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.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 ## [v0.10.0] - 2023-02-01
### :boom: BREAKING CHANGES ### :boom: BREAKING CHANGES
- due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*: - due to [`3cf9be8`](https://github.com/JakeStanger/ironbar/commit/3cf9be89fd74face31806165f66b68052b093bab) - global icon theme setting *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
@@ -234,3 +271,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.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.9.0]: https://github.com/JakeStanger/ironbar/compare/v0.8.0...v0.9.0
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0 [v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0

985
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.10.0" version = "0.12.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar" description = "Customisable GTK Layer Shell wlroots/sway bar"
@@ -14,17 +14,18 @@ default = [
"music+all", "music+all",
"sys_info", "sys_info",
"tray", "tray",
"volume+all", "upower",
"workspaces+all" "workspaces+all"
] ]
http = ["dep:reqwest"] http = ["dep:reqwest"]
upower = ["upower_dbus", "zbus", "futures-lite"]
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn"] "config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
"config+json" = ["universal-config/json"] "config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"] "config+yaml" = ["universal-config/yaml"]
"config+toml" = ["universal-config/toml"] "config+toml" = ["universal-config/toml"]
"config+corn" = ["universal-config/corn"] "config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
clipboard = ["nix"] clipboard = ["nix"]
@@ -39,10 +40,6 @@ sys_info = ["sysinfo", "regex"]
tray = ["stray"] tray = ["stray"]
volume = []
"volume+all" = ["volume", "volume+pulse"]
"volume+pulse" = ["libpulse-binding", "libpulse-glib-binding"]
workspaces = ["futures-util"] workspaces = ["futures-util"]
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
"workspaces+sway" = ["workspaces", "swayipc-async"] "workspaces+sway" = ["workspaces", "swayipc-async"]
@@ -62,13 +59,14 @@ strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2" color-eyre = "0.6.2"
serde = { version = "1.0.141", features = ["derive"] } serde = { version = "1.0.141", features = ["derive"] }
indexmap = "1.9.1" indexmap = "1.9.1"
dirs = "4.0.0" dirs = "5.0.0"
walkdir = "2.3.2" walkdir = "2.3.2"
notify = { version = "5.0.0", default-features = false } notify = { version = "5.0.0", default-features = false }
wayland-client = "0.29.5" wayland-client = "0.30.0"
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] } wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] } wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
universal-config = { version = "0.2.1", default_features = false } smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] }
universal-config = { version = "0.4.0", default_features = false }
lazy_static = "1.4.0" lazy_static = "1.4.0"
async_once = "0.2.6" async_once = "0.2.6"
@@ -78,7 +76,7 @@ cfg-if = "1.0.0"
reqwest = { version = "0.11.14", optional = true } reqwest = { version = "0.11.14", optional = true }
# clipboard # clipboard
nix = { version = "0.26.2", optional = true } nix = { version = "0.26.2", optional = true, features = ["event"] }
# clock # clock
chrono = { version = "0.4.19", optional = true } chrono = { version = "0.4.19", optional = true }
@@ -88,18 +86,19 @@ mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.0", optional = true } mpris = { version = "2.0.0", optional = true }
# sys_info # sys_info
sysinfo = { version = "0.27.0", optional = true } sysinfo = { version = "0.28.4", optional = true }
# tray # tray
stray = { version = "0.1.3", optional = true } stray = { version = "0.1.3", optional = true }
# volume # upower
libpulse-binding = { version = "2.27.1", optional = true } upower_dbus = { version = "0.3.2", optional = true }
libpulse-glib-binding = { version = "2.27.1", optional = true } futures-lite = { version = "1.12.0", optional = true }
zbus = { version = "3.11.0", optional = true }
# workspaces # workspaces
swayipc-async = { version = "2.0.1", optional = true } swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.0", optional = true } hyprland = { version = "0.3.1", optional = true }
futures-util = { version = "0.3.21", optional = true } futures-util = { version = "0.3.21", optional = true }
# shared # shared

View File

@@ -12,6 +12,7 @@ install target/release/ironbar ~/.local/bin/ironbar
## Build requirements ## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed. 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 ### Arch
@@ -22,7 +23,9 @@ pacman -S gtk3 gtk-layer-shell
### Ubuntu/Debian ### Ubuntu/Debian
```shell ```shell
apt install libgtk-3-dev libgtk-layer-shell-dev apt install build-essential libgtk-3-dev libgtk-layer-shell-dev
# for http support
apt install libssl-dev
``` ```
### Fedora ### Fedora
@@ -59,7 +62,8 @@ cargo build --release --no-default-features \
| config+json | Enables configuration support for JSON. | | config+json | Enables configuration support for JSON. |
| config+yaml | Enables configuration support for YAML. | | config+yaml | Enables configuration support for YAML. |
| config+toml | Enables configuration support for TOML. | | config+toml | Enables configuration support for TOML. |
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). | | config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | | | **Modules** | |
| clipboard | Enables the `clipboard` module. | | clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. | | clock | Enables the `clock` module. |
@@ -68,6 +72,7 @@ cargo build --release --no-default-features \
| music+mpd | Enables the `music` module with MPD support. | | music+mpd | Enables the `music` module with MPD support. |
| sys_info | Enables the `sys_info` module. | | sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. | | tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |
| workspaces+all | Enables the `workspaces` module with support for all compositors. | | workspaces+all | Enables the `workspaces` module with support for all compositors. |
| workspaces+sway | Enables the `workspaces` module with support for Sway. | | workspaces+sway | Enables the `workspaces` module with support for Sway. |
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. | | workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |

View File

@@ -272,6 +272,7 @@ The following table lists each of the top-level bar config options:
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | | `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | | `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. | | `height` | `integer` | `42` | The bar's height in pixels. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `margin.top` | `integer` | `0` | The margin on the top of the bar | | `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | | `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar | | `margin.left` | `integer` | `0` | The margin on the left of the bar |
@@ -288,12 +289,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). For information on the `Script` type, and embedding scripts in strings, see [here](script).
#### Events
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------| |-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
| `on_click_left` | `Script [oneshot]` | `null` | Runs the script when the module is left clicked. | | `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_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_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_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. | | `on_scroll_down` | `Script [oneshot]` | `null` | Runs the script when the module is scrolled down on. |
| `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` | `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. |
| `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}}`. | | `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).

View File

@@ -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. 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). 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. 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. Information on styling individual modules can be found on their pages in the sidebar.
| Selector | Description | | Selector | Description |
|----------------|-------------------------------------------| |----------------|--------------------------------------------|
| `.background` | Top-level window | | `.background` | Top-level window. |
| `#bar` | Bar root box | | `#bar` | Bar root box. |
| `#bar #start` | Bar left or top modules container box | | `#bar #start` | Bar left or top modules container box. |
| `#bar #center` | Bar center modules container box | | `#bar #center` | Bar center modules container box. |
| `#bar #end` | Bar right or bottom modules container box | | `#bar #end` | Bar right or bottom modules container box. |
| `.container` | All of the above | | `.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;
}
```

View File

@@ -21,9 +21,11 @@
- [Clock](clock) - [Clock](clock)
- [Custom](custom) - [Custom](custom)
- [Focused](focused) - [Focused](focused)
- [Label](label)
- [Launcher](launcher) - [Launcher](launcher)
- [Music](music) - [Music](music)
- [Script](script) - [Script](script)
- [Sys_Info](sys-info) - [Sys_Info](sys-info)
- [Tray](tray) - [Tray](tray)
- [Upower](upower)
- [Workspaces](workspaces) - [Workspaces](workspaces)

View File

@@ -12,6 +12,7 @@ Supports plain text and images.
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `icon` | `string/image` | `󰨸` | Icon to show on the widget button. | | `icon` | `string/image` | `󰨸` | 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. | | `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. | | `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. | | `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
@@ -83,11 +84,13 @@ end:
| Selector | Description | | Selector | Description |
|--------------------------------------|------------------------------------------------------| |--------------------------------------|------------------------------------------------------|
| `#clipboard` | Clipboard widget. | | `.clipboard` | Clipboard widget. |
| `#clipboard .btn` | Clipboard widget button. | | `.clipboard .btn` | Clipboard widget button. |
| `#popup-clipboard` | Clipboard popup box. | | `.popup-clipboard` | Clipboard popup box. |
| `#popup-clipboard .item` | Clipboard row item inside the popup. | | `.popup-clipboard .item` | Clipboard row item inside the popup. |
| `#popup-clipboard .item .btn` | Clipboard row item radio button. | | `.popup-clipboard .item .btn` | Clipboard row item radio button. |
| `#popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). | | `.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.image` | Clipboard row item radio button (image values only). |
| `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. | | `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -71,7 +71,9 @@ end:
| Selector | Description | | Selector | Description |
|--------------------------------|------------------------------------------------------------------------------------| |--------------------------------|------------------------------------------------------------------------------------|
| `#clock` | Clock widget button | | `.clock` | Clock widget button |
| `#popup-clock` | Clock popup box | | `.popup-clock` | Clock popup box |
| `#popup-clock #calendar-clock` | Clock inside the popup | | `.popup-clock .calendar-clock` | Clock inside the popup |
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. | | `.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).

View File

@@ -1,7 +1,7 @@
Allows you to compose custom modules consisting of multiple widgets, including popups. Allows you to compose custom modules consisting of multiple widgets, including popups.
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click. Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://f.jstanger.dev/github/ironbar/custom-power-menu.png) ![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://f.jstanger.dev/github/ironbar/custom-power-menu.png?raw)
## Configuration ## Configuration
@@ -10,29 +10,144 @@ 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. 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. 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` ### `Widget`
There are many widget types, each with their own config options.
You can think of these like HTML elements and their attributes.
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 | | Name | Type | Default | Description |
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------| |---------|-------------------------------------------------------------------|---------|-------------------------------|
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. | | `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. | | `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. | | `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
### Labels #### 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 and embedded scripts are supported.
> Type `label`
| Name | Type | Default | Description |
|---------|----------|--------------|---------------------------------------------------------------------|
| `label` | `string` | `horizontal` | 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` | `string` | `horizontal` | 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` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
| `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 |
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
| `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 |
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
| `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. 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. For example, the following label would output your system uptime, updated every 30 seconds.
@@ -52,6 +167,9 @@ To execute shell commands, prefix them with an `!`.
For example, if you want to run `~/.local/bin/my-script.sh` on click, 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`. 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: The following bar commands are supported:
- `popup:toggle` - `popup:toggle`
@@ -238,13 +356,9 @@ end:
```corn ```corn
let { let {
$power_menu = { $button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ] $popup = {
popup = [ {
type = "box" type = "box"
orientation = "vertical" orientation = "vertical"
widgets = [ widgets = [
@@ -258,7 +372,16 @@ let {
} }
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" } { type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
] ]
} ] }
$power_menu = {
type = "custom"
class = "power-menu"
bar = [ $button ]
popup = [ $popup ]
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
} }
} in { } in {
end = [ $power_menu ] end = [ $power_menu ]
@@ -269,8 +392,13 @@ let {
## Styling ## 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`.
The following top-level selectors are always available:
| Selector | Description | | Selector | Description |
|-----------|-------------------------| |-----------------|--------------------------------|
| `#custom` | Custom widget container | | `.custom` | Custom widget container. |
| `.popup-custom` | Custom widget popup container. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -87,7 +87,9 @@ end:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|--------------------------|--------------------| |-------------------|--------------------|
| `#focused` | Focused widget box | | `.focused` | Focused widget box |
| `#focused #icon` | App icon | | `.focused .icon` | App icon |
| `#focused #label` | App name | | `.focused .label` | App name |
For more information on styling, please see the [styling guide](styling-guide).

72
docs/modules/Label.md Normal file
View File

@@ -0,0 +1,72 @@
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
## Configuration
> Type: `label`
| Name | Type | Default | Description |
|---------|----------|---------|-----------------------------------------|
| `label` | `string` | `null` | Text, optionally with embedded scripts. |
<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).

View File

@@ -14,6 +14,7 @@ Optionally displays a launchable set of favourites.
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher | | `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_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. | | `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>
@@ -89,10 +90,12 @@ start:
| Selector | Description | | Selector | Description |
|-------------------------------|--------------------------| |-------------------------------|--------------------------|
| `#launcher` | Launcher widget box | | `.launcher` | Launcher widget box |
| `#launcher .item` | App button | | `.launcher .item` | App button |
| `#launcher .item.open` | App button (open app) | | `.launcher .item.open` | App button (open app) |
| `#launcher .item.focused` | App button (focused app) | | `.launcher .item.focused` | App button (focused app) |
| `#launcher .item.urgent` | App button (urgent app) | | `.launcher .item.urgent` | App button (urgent app) |
| `#launcher-popup` | Popup container | | `.popup-launcher` | Popup container |
| `#launcher-popup .popup-item` | Window button in popup | | `.popup-launcher .popup-item` | Window button in popup |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -27,6 +27,9 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
| `icons.track` | `string/image` | `` | Icon to show next to track title. | | `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. | | `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. | | `icons.artist` | `string/image` | `ﴁ` | 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/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. | | `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. | | `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
@@ -132,23 +135,26 @@ and will be replaced with values from the currently playing track:
| Selector | Description | | Selector | Description |
|-------------------------------------|------------------------------------------| |-------------------------------------|------------------------------------------|
| `#music` | Tray widget button | | `.music` | Tray widget button |
| `#popup-music` | Popup box | | `.music .contents` | Tray widget button contents box |
| `#popup-music #album-art` | Album art image inside popup box | | `.popup-music` | Popup box |
| `#popup-music #title` | Track title container inside popup box | | `.popup-music .album-art` | Album art image inside popup box |
| `#popup-music #title .icon` | Track title icon label inside popup box | | `.popup-music .title` | Track title container inside popup box |
| `#popup-music #title .label` | Track title label inside popup box | | `.popup-music .title .icon` | Track title icon label inside popup box |
| `#popup-music #album` | Track album container inside popup box | | `.popup-music .title .label` | Track title label inside popup box |
| `#popup-music #album .icon` | Track album icon label inside popup box | | `.popup-music .album` | Track album container inside popup box |
| `#popup-music #album .label` | Track album label inside popup box | | `.popup-music .album .icon` | Track album icon label inside popup box |
| `#popup-music #artist` | Track artist container inside popup box | | `.popup-music .album .label` | Track album label inside popup box |
| `#popup-music #artist .icon` | Track artist icon label inside popup box | | `.popup-music .artist` | Track artist container inside popup box |
| `#popup-music #artist .label` | Track artist label inside popup box | | `.popup-music .artist .icon` | Track artist icon label inside popup box |
| `#popup-music #controls` | Controls container inside popup box | | `.popup-music .artist .label` | Track artist label inside popup box |
| `#popup-music #controls #btn-prev` | Previous button inside popup box | | `.popup-music .controls` | Controls container inside popup box |
| `#popup-music #controls #btn-play` | Play button inside popup box | | `.popup-music .controls .btn-prev` | Previous button inside popup box |
| `#popup-music #controls #btn-pause` | Pause button inside popup box | | `.popup-music .controls .btn-play` | Play button inside popup box |
| `#popup-music #controls #btn-next` | Next button inside popup box | | `.popup-music .controls .btn-pause` | Pause button inside popup box |
| `#popup-music #volume` | Volume container inside popup box | | `.popup-music .controls .btn-next` | Next button inside popup box |
| `#popup-music #volume #slider` | Volume slider popup box | | `.popup-music .volume` | Volume container inside popup box |
| `#popup-music #volume .icon` | Volume icon label 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).

View File

@@ -83,5 +83,7 @@ end:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|---------------|---------------------| |-----------|---------------------|
| `#script` | Script widget label | | `.script` | Script widget label |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -172,5 +172,7 @@ The following tokens can be used in the `format` configuration option:
| Selector | Description | | Selector | Description |
|------------------|------------------------------| |------------------|------------------------------|
| `#sysinfo` | Sysinfo widget box | | `.sysinfo` | Sysinfo widget box |
| `#sysinfo #item` | Individual information label | | `.sysinfo .item` | Individual information label |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -60,5 +60,7 @@ end:
| Selector | Description | | Selector | Description |
|---------------|------------------| |---------------|------------------|
| `#tray` | Tray widget box | | `.tray` | Tray widget box |
| `#tray .item` | Tray icon button | | `.tray .item` | Tray icon button |
For more information on styling, please see the [styling guide](styling-guide).

82
docs/modules/Upower.md Normal file
View File

@@ -0,0 +1,82 @@
Displays system power information such as the battery percentage, and estimated time to empty.
`TODO: ADD SCREENSHOT`
[//]: # (![Screenshot]&#40;https://user-images.githubusercontent.com/5057870/184540521-2278bdec-9742-46f0-9ac2-58a7b6f6ea1d.png&#41;)
## Configuration
> Type: `upower`
| Name | Type | Default | Description |
|----------|----------|-----------------|---------------------------------------------------|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
<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 .icon` | Upower widget battery icon. |
| `.upower .button` | Upower widget button. |
| `.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).

View File

@@ -11,6 +11,7 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
| Name | Type | Default | Description | | Name | Type | Default | Description |
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | | `name_map` | `Map<string, string/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. | | `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. | | `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
@@ -90,6 +91,8 @@ end:
| Selector | Description | | Selector | Description |
|-----------------------------|--------------------------------------| |-----------------------------|--------------------------------------|
| `#workspaces` | Workspaces widget box | | `.workspaces` | Workspaces widget box |
| `#workspaces .item` | Workspace button | | `.workspaces .item` | Workspace button |
| `#workspaces .item.focused` | Workspace button (workspace focused) | | `.workspaces .item.focused` | Workspace button (workspace focused) |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -67,6 +67,8 @@ let {
$clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 } $clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
$label = { type = "label" label = "random num: {{500:echo $RANDOM}}" }
// -- begin custom -- // -- begin custom --
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } $button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
@@ -97,7 +99,7 @@ let {
} }
// -- end custom -- // -- end custom --
$left = [ $workspaces $launcher ] $left = [ $workspaces $launcher $label ]
$right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ] $right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
} }
in { in {

View File

@@ -126,6 +126,10 @@
"show_icons": true, "show_icons": true,
"show_names": false, "show_names": false,
"type": "launcher" "type": "launcher"
},
{
"label": "random num: {{500:echo $RANDOM}}",
"type": "label"
} }
] ]
} }

View File

@@ -116,3 +116,7 @@ favorites = [
'Steam', 'Steam',
] ]
[[start]]
label = 'random num: {{500:echo $RANDOM}}'
type = 'label'

View File

@@ -82,4 +82,6 @@ start:
show_icons: true show_icons: true
show_names: false show_names: false
type: launcher type: launcher
- label: 'random num: {{500:echo $RANDOM}}'
type: label

View File

@@ -1,240 +1,194 @@
@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: Noto Sans Nerd Font, sans-serif;
font-size: 16px; font-size: 16px;
border: none; border: none;
border-radius: 0;
}
box, menubar, button {
background-color: @color_bg;
}
button, label {
color: @color_text;
}
button:hover {
background-color: @color_bg_dark;
} }
#bar { #bar {
border-top: 1px solid #424242; border-top: 1px solid @color_border;
} }
.background, .container { .popup {
background-color: #2d2d2d; border: 1px solid @color_border;
padding: 1em;
} }
#workspaces .item {
color: white; /* -- clipboard -- */
background-color: #2d2d2d;
border-radius: 0; .clipboard {
margin-left: 5px;
font-size: 1.1em;
} }
#workspaces .item.focused { .popup-clipboard .item {
box-shadow: inset 0 -3px; padding-bottom: 0.3em;
background-color: #1c1c1c; border-bottom: 1px solid @color_border;
} }
#workspaces *:not(.focused):hover {
box-shadow: inset 0 -3px;
}
#launcher .item { /* -- clock -- */
border-radius: 0;
background-color: #2d2d2d;
margin-right: 4px;
}
#launcher .item:not(.focused):hover { .clock {
background-color: #1c1c1c;
}
#launcher .open {
border-bottom: 2px solid #6699cc;
}
#launcher .focused {
color: white;
background-color: #1c1c1c;
border-bottom: 4px solid #6699cc;
}
#launcher .urgent {
color: white;
background-color: #8f0a0a;
}
#popup-launcher .popup-item {
color: white;
background-color: #2d2d2d;
border-radius: 0;
}
#popup-launcher .popup-item:hover {
background-color: #1c1c1c;
}
#popup-launcher .popup-item:not(:first-child) {
border-top: 1px solid white;
}
#clock {
color: white;
background-color: #2d2d2d;
font-weight: bold; font-weight: bold;
margin-left: 5px; margin-left: 5px;
} }
#clock:hover { .popup-clock .calendar-clock {
background-color: #1c1c1c; color: @color_text;
}
#script {
padding-left: 10px;
color: white;
}
#sysinfo {
margin-left: 10px;
color: white;
}
#sysinfo #item {
margin-left: 5px;
}
#tray {
margin-left: 10px;
}
#tray .item {
background-color: #2d2d2d;
}
#music {
background-color: #2d2d2d;
color: white;
}
.popup {
background-color: #2d2d2d;
border: 1px solid #424242;
}
#popup-clock {
padding: 1em;
}
#calendar-clock {
color: white;
font-size: 2.5em; font-size: 2.5em;
padding-bottom: 0.1em; padding-bottom: 0.1em;
} }
#calendar { .popup-clock .calendar {
background-color: #2d2d2d; background-color: @color_bg;
color: white; color: @color_text;
} }
#calendar .header { .popup-clock .calendar .header {
padding-top: 1em; padding-top: 1em;
border-top: 1px solid #424242; border-top: 1px solid @color_border;
font-size: 1.5em; font-size: 1.5em;
} }
#calendar:selected { .popup-clock .calendar:selected {
background-color: #6699cc; background-color: @color_border_active;
} }
#music:hover {
background-color: #1c1c1c; /* -- launcher -- */
.launcher .item {
margin-right: 4px;
} }
#popup-music { .launcher .item:not(.focused):hover {
color: white; background-color: @color_bg_dark;
padding: 1em;
} }
#popup-music #album-art { .launcher .open {
border-bottom: 1px solid @color_text;
}
.launcher .focused {
border-bottom: 2px solid @color_border_active;
}
.launcher .urgent {
border-bottom-color: @color_urgent;
}
.popup-launcher {
padding: 0;
}
.popup-launcher .popup-item:not(:first-child) {
border-top: 1px solid @color_border;
}
/* -- music -- */
.music:hover * {
background-color: @color_bg_dark;
}
.popup-music .album-art {
margin-right: 1em; margin-right: 1em;
} }
#popup-music #title .icon *, #popup-music #title .label { .popup-music .title .icon *, .popup-music .title .label {
font-size: 1.7em; font-size: 1.7em;
} }
#popup-music #controls * { .popup-music .controls *:disabled {
border-radius: 0; color: @color_border;
background-color: #2d2d2d;
color: white;
} }
#popup-music #controls *:disabled { .popup-music .volume scale slider {
color: #424242; border-radius: 100%;
} }
#popup-music #volume > box:last-child label { /* volume icon */
.popup-music .volume > box:last-child label {
margin-left: 6px; margin-left: 6px;
} }
#focused {
color: white; /* -- script -- */
.script {
padding-left: 10px;
} }
.power-menu {
/* -- sys_info -- */
.sysinfo {
margin-left: 10px; margin-left: 10px;
} }
.power-menu #power-btn { .sysinfo .item {
color: white; margin-left: 5px;
background-color: #2d2d2d;
} }
.power-menu #power-btn:hover {
background-color: #1c1c1c; /* -- tray -- */
.tray {
margin-left: 10px;
} }
.popup-power-menu {
padding: 1em; /* -- workspaces -- */
.workspaces .item.focused {
box-shadow: inset 0 -3px;
background-color: @color_bg_dark;
} }
.workspaces .item:hover {
box-shadow: inset 0 -3px;
}
/* -- custom: power menu -- */
.popup-power-menu #header { .popup-power-menu #header {
color: white;
font-size: 1.4em; font-size: 1.4em;
border-bottom: 1px solid white;
padding-bottom: 0.4em; padding-bottom: 0.4em;
margin-bottom: 0.8em; margin-bottom: 0.6em;
border-bottom: 1px solid @color_border;
} }
.popup-power-menu .power-btn { .popup-power-menu .power-btn {
color: white; border: 1px solid @color_border;
background-color: #2d2d2d;
border: 1px solid white;
padding: 0.6em 1em; padding: 0.6em 1em;
} }
.popup-power-menu .power-btn + .power-btn { .popup-power-menu #buttons > *:nth-child(1) .power-btn {
margin-left: 1em; margin-right: 1em;
} }
.popup-power-menu .power-btn:hover {
background-color: #1c1c1c;
}
#clipboard * {
font-size: 1.8em;
}
#popup-clipboard {
padding: 1em;
color: white;
}
#popup-clipboard .item {
border-bottom: 1px solid #424242;
}
#popup-clipboard .btn > *:nth-child(2) {
padding: 10px;
}
#popup-clipboard .btn-remove {
background-color: #2d2d2d;
color: white;
font-size: 1.2em;
border-left: 1px solid #424242;
}
#popup-clipboard .btn-remove:hover {
color: #fcc;
}

36
flake.lock generated
View File

@@ -1,12 +1,15 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1681202837,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1680213900, "lastModified": 1682786779,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", "narHash": "sha256-m7QFzPS/CE8hbkbIVK4UStihAQMtczr0vSpOgETOM1g=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2", "rev": "08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -45,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1680229280, "lastModified": 1682821181,
"narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=", "narHash": "sha256-7MYRqO9Ge46sULbQwJbcH/IMDNBdxCGUO9w7bEOc3CI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "aa480d799023141e1b9e5d6108700de63d9ad002", "rev": "1be440e9119e69b68151cd9c84876ff3063a2e45",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -57,6 +60,21 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "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", "root": "root",

View File

@@ -57,6 +57,18 @@
default = self.packages.${system}.ironbar; 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 devShells = genSystems (system: let
pkgs = pkgsFor system; pkgs = pkgsFor system;
rust = mkRustToolchain pkgs; rust = mkRustToolchain pkgs;

View File

@@ -1,16 +1,26 @@
{ {
gtk3, gtk3,
gdk-pixbuf, gdk-pixbuf,
librsvg,
webp-pixbuf-loader,
gobject-introspection,
glib-networking,
glib,
shared-mime-info,
gsettings-desktop-schemas,
wrapGAppsHook,
gtk-layer-shell, gtk-layer-shell,
gnome,
libxkbcommon, libxkbcommon,
openssl, openssl,
pkg-config, pkg-config,
hicolor-icon-theme,
rustPlatform, rustPlatform,
lib, lib,
version ? "git", version ? "git",
features ? [], features ? [],
}: }:
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage rec {
inherit version; inherit version;
pname = "ironbar"; pname = "ironbar";
src = builtins.path { src = builtins.path {
@@ -24,13 +34,31 @@ rustPlatform.buildRustPackage {
buildFeatures = features; buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;}; cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock; cargoLock.lockFile = ../Cargo.lock;
nativeBuildInputs = [pkg-config]; nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
buildInputs = [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl]; buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
propagatedBuildInputs = [
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; { meta = with lib; {
homepage = "https://github.com/JakeStanger/ironbar"; homepage = "https://github.com/JakeStanger/ironbar";
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust."; description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
license = licenses.mit; license = licenses.mit;
platforms = platforms.linux; platforms = platforms.linux;
mainProgram = "Hyprland"; mainProgram = "ironbar";
}; };
} }

5
scripts/generate-examples.sh Executable file
View 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
View 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"

View File

@@ -1,18 +1,15 @@
use crate::bridge_channel::BridgeChannel; use crate::config::{BarPosition, MarginConfig, ModuleConfig};
use crate::config::{BarPosition, CommonConfig, MarginConfig, ModuleConfig}; use crate::modules::{
use crate::dynamic_string::DynamicString; create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
use crate::modules::{Module, ModuleInfo, ModuleLocation, ModuleUpdateEvent, WidgetContext}; };
use crate::popup::Popup; use crate::popup::Popup;
use crate::script::{OutputStream, Script}; use crate::Config;
use crate::{await_sync, read_lock, send, write_lock, Config};
use color_eyre::Result; use color_eyre::Result;
use gtk::gdk::{EventMask, Monitor, ScrollDirection}; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, EventBox, IconTheme, Orientation, Widget}; use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use tokio::spawn; use tracing::{debug, info};
use tokio::sync::mpsc;
use tracing::{debug, error, info, trace};
/// Creates a new window for a bar, /// Creates a new window for a bar,
/// sets it up and adds its widgets. /// sets it up and adds its widgets.
@@ -168,17 +165,17 @@ fn load_modules(
if let Some(modules) = config.start { if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left); let info = info!(ModuleLocation::Left);
add_modules(left, modules, &info)?; add_modules(left, modules, &info, config.popup_gap)?;
} }
if let Some(modules) = config.center { if let Some(modules) = config.center {
let info = info!(ModuleLocation::Center); let info = info!(ModuleLocation::Center);
add_modules(center, modules, &info)?; add_modules(center, modules, &info, config.popup_gap)?;
} }
if let Some(modules) = config.end { if let Some(modules) = config.end {
let info = info!(ModuleLocation::Right); let info = info!(ModuleLocation::Right);
add_modules(right, modules, &info)?; add_modules(right, modules, &info, config.popup_gap)?;
} }
Ok(()) Ok(())
@@ -186,18 +183,25 @@ fn load_modules(
/// Adds modules into a provided GTK box, /// Adds modules into a provided GTK box,
/// which should be one of its left, center or right containers. /// which should be one of its left, center or right containers.
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> { fn add_modules(
let popup = Popup::new(info); content: &gtk::Box,
modules: Vec<ModuleConfig>,
info: &ModuleInfo,
popup_gap: i32,
) -> Result<()> {
let popup = Popup::new(info, popup_gap);
let popup = Arc::new(RwLock::new(popup)); let popup = Arc::new(RwLock::new(popup));
let orientation = info.bar_position.get_orientation();
macro_rules! add_module { macro_rules! add_module {
($module:expr, $id:expr) => {{ ($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist"); 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); content.add(&container);
setup_module_common_options(container, common);
}}; }};
} }
@@ -209,6 +213,7 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
ModuleConfig::Clock(mut module) => add_module!(module, id), ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id), ModuleConfig::Custom(mut module) => add_module!(module, id),
ModuleConfig::Focused(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), ModuleConfig::Launcher(mut module) => add_module!(module, id),
#[cfg(feature = "music")] #[cfg(feature = "music")]
ModuleConfig::Music(mut module) => add_module!(module, id), ModuleConfig::Music(mut module) => add_module!(module, id),
@@ -217,6 +222,8 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
ModuleConfig::SysInfo(mut module) => add_module!(module, id), ModuleConfig::SysInfo(mut module) => add_module!(module, id),
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
ModuleConfig::Tray(mut module) => add_module!(module, id), ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id), ModuleConfig::Workspaces(mut module) => add_module!(module, id),
} }
@@ -224,217 +231,3 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
Ok(()) Ok(())
} }
/// Creates a module and sets it up.
/// This setup includes widget/popup content and event channels.
fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>,
) -> Result<TWidget>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
TSend: Clone + Send + 'static,
{
let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
let channel = BridgeChannel::<ModuleUpdateEvent<TSend>>::new();
let (ui_tx, ui_rx) = mpsc::channel::<TRec>(16);
module.spawn_controller(info, channel.create_sender(), ui_rx)?;
let context = WidgetContext {
id,
widget_rx: w_rx,
popup_rx: p_rx,
tx: channel.create_sender(),
controller_tx: ui_tx,
};
let name = TModule::name();
let module_parts = module.into_widget(context, info)?;
module_parts.widget.set_widget_name(name);
let mut has_popup = false;
if let Some(popup_content) = module_parts.popup {
register_popup_content(popup, id, popup_content);
has_popup = true;
}
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
Ok(module_parts.widget)
}
/// Registers the popup content with the popup.
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
write_lock!(popup).register_content(id, popup_content);
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
/// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>(
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
w_tx: glib::Sender<TSend>,
p_tx: glib::Sender<TSend>,
popup: Arc<RwLock<Popup>>,
name: &'static str,
id: usize,
has_popup: bool,
) where
TSend: Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
channel.recv(move |ev| {
match ev {
ModuleUpdateEvent::Update(update) => {
if has_popup {
send!(p_tx, update.clone());
}
send!(w_tx, update);
}
ModuleUpdateEvent::TogglePopup(geometry) => {
debug!("Toggling popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
if popup.is_visible() {
popup.hide();
} else {
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
popup.hide();
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
popup.hide();
}
}
Continue(true)
});
}
/// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned.
fn wrap_widget<W: IsA<Widget>>(widget: &W) -> EventBox {
let container = EventBox::new();
container.add_events(EventMask::SCROLL_MASK);
container.add(widget);
container
}
/// Configures the module's container according to the common config options.
fn setup_module_common_options(container: EventBox, common: CommonConfig) {
common.show_if.map_or_else(
|| {
container.show_all();
},
|show_if| {
let script = Script::new_polling(show_if);
let container = container.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
script
.run(|(_, success)| {
send!(tx, success);
})
.await;
});
rx.attach(None, move |success| {
if success {
container.show_all();
} else {
container.hide();
};
Continue(true)
});
},
);
let left_click_script = common.on_click_left.map(Script::new_polling);
let middle_click_script = common.on_click_middle.map(Script::new_polling);
let right_click_script = common.on_click_right.map(Script::new_polling);
container.connect_button_press_event(move |_, event| {
let script = match event.button() {
1 => left_click_script.as_ref(),
2 => middle_click_script.as_ref(),
3 => right_click_script.as_ref(),
_ => None,
};
if let Some(script) = script {
trace!("Running on-click script: {}", event.button());
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
}
Inhibit(false)
});
let scroll_up_script = common.on_scroll_up.map(Script::new_polling);
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
container.connect_scroll_event(move |_, event| {
let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(),
_ => None,
};
if let Some(script) = script {
trace!("Running on-scroll script: {}", event.direction());
match await_sync(async { script.get_output().await }) {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
}
Inhibit(false)
});
if let Some(tooltip) = common.tooltip {
DynamicString::new(&tooltip, move |string| {
container.set_tooltip_text(Some(&string));
Continue(true)
});
}
}

View File

@@ -6,7 +6,7 @@ use lazy_static::lazy_static;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::debug; use tracing::{debug, trace};
#[derive(Debug)] #[derive(Debug)]
pub enum ClipboardEvent { pub enum ClipboardEvent {
@@ -26,6 +26,8 @@ pub struct ClipboardClient {
impl ClipboardClient { impl ClipboardClient {
fn new() -> Self { fn new() -> Self {
trace!("Initializing clipboard client");
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new())); let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
let cache = Arc::new(Mutex::new(ClipboardCache::new())); let cache = Arc::new(Mutex::new(ClipboardCache::new()));
@@ -35,11 +37,21 @@ impl ClipboardClient {
let cache = cache.clone(); let cache = cache.clone();
spawn(async move { spawn(async move {
let mut rx = { let (mut rx, item) = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_clipboard() 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 { while let Ok(item) = rx.recv().await {
debug!("Received clipboard item (ID: {})", item.id); debug!("Received clipboard item (ID: {})", item.id);
@@ -59,8 +71,7 @@ impl ClipboardClient {
let iter = senders.iter(); let iter = senders.iter();
for (tx, sender_cache_size) in iter { for (tx, sender_cache_size) in iter {
if cache_size == *sender_cache_size { if cache_size == *sender_cache_size {
let mut cache = lock!(cache); let removed_id = lock!(cache)
let removed_id = cache
.remove_ref_first() .remove_ref_first()
.expect("Clipboard cache unexpectedly empty"); .expect("Clipboard cache unexpectedly empty");
try_send!(tx, ClipboardEvent::Remove(removed_id)); try_send!(tx, ClipboardEvent::Remove(removed_id));
@@ -83,18 +94,11 @@ impl ClipboardClient {
Self { senders, cache } Self { senders, cache }
} }
pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> { pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
let (tx, rx) = mpsc::channel(16); let (tx, rx) = mpsc::channel(16);
let wl = wayland::get_client().await;
wl.roundtrip();
{ {
let mut cache = lock!(self.cache); let cache = lock!(self.cache);
if let Some(item) = wl.get_clipboard() {
cache.insert_or_inc_ref(item);
}
let iter = cache.iter(); let iter = cache.iter();
for (_, (item, _)) in iter { for (_, (item, _)) in iter {
@@ -102,10 +106,7 @@ impl ClipboardClient {
} }
} }
{ lock!(self.senders).push((tx, cache_size));
let mut senders = lock!(self.senders);
senders.push((tx, cache_size));
}
rx rx
} }
@@ -131,8 +132,7 @@ impl ClipboardClient {
} }
pub fn remove(&self, id: usize) { pub fn remove(&self, id: usize) {
let mut cache = lock!(self.cache); lock!(self.cache).remove(id);
cache.remove(id);
let senders = lock!(self.senders); let senders = lock!(self.senders);
let iter = senders.iter(); let iter = senders.iter();
@@ -172,13 +172,6 @@ impl ClipboardCache {
.map(|(item, _)| item) .map(|(item, _)| item)
} }
/// Inserts an entry with `ref_count` initial references,
/// or increments the `ref_count` by 1 if it already exists.
fn insert_or_inc_ref(&mut self, item: Arc<ClipboardItem>) {
let mut item = self.cache.entry(item.id).or_insert((item, 0));
item.1 += 1;
}
/// Removes the entry with key `id`. /// Removes the entry with key `id`.
/// This ignores references. /// This ignores references.
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> { fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {

View File

@@ -6,6 +6,6 @@ pub mod compositor;
pub mod music; pub mod music;
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub mod system_tray; pub mod system_tray;
#[cfg(feature = "upower")]
pub mod upower;
pub mod wayland; pub mod wayland;
#[cfg(feature = "volume")]
pub mod volume;

View File

@@ -5,6 +5,7 @@ use color_eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::collections::HashSet; use std::collections::HashSet;
use std::string;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@@ -259,7 +260,7 @@ impl From<Metadata> for Track {
.and_then(mpris::MetadataValue::as_str_array) .and_then(mpris::MetadataValue::as_str_array)
.and_then(|arr| arr.first().map(|val| (*val).to_string())), .and_then(|arr| arr.first().map(|val| (*val).to_string())),
track: value.track_number().map(|track| track as u64), track: value.track_number().map(|track| track as u64),
cover_path: value.art_url().map(|s| s.to_string()), cover_path: value.art_url().map(string::ToString::to_string),
} }
} }
} }

View File

@@ -10,7 +10,7 @@ use stray::message::{NotifierItemCommand, NotifierItemMessage};
use stray::StatusNotifierWatcher; use stray::StatusNotifierWatcher;
use tokio::spawn; use tokio::spawn;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tracing::error; use tracing::{debug, error, trace};
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>; type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
@@ -38,6 +38,8 @@ impl TrayEventReceiver {
spawn(async move { spawn(async move {
while let Ok(message) = host.recv().await { while let Ok(message) = host.recv().await {
trace!("Received message: {message:?} ");
send!(b_tx, message.clone()); send!(b_tx, message.clone());
let mut tray = lock!(tray); let mut tray = lock!(tray);
match message { match message {
@@ -46,9 +48,11 @@ impl TrayEventReceiver {
item, item,
menu, menu,
} => { } => {
debug!("Adding item with address '{address}'");
tray.insert(address, (item, menu)); tray.insert(address, (item, menu));
} }
NotifierItemMessage::Remove { address } => { NotifierItemMessage::Remove { address } => {
debug!("Removing item with address '{address}'");
tray.remove(&address); tray.remove(&address);
} }
} }

40
src/clients/upower.rs Normal file
View 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 = 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
}

View File

@@ -1,9 +0,0 @@
#[cfg(feature = "volume+pulse")]
pub mod pulse_bak;
// #[cfg(feature = "volume+pulse")]
// pub mod pulse;
trait VolumeClient {
// TODO: Write
}

View File

@@ -1,345 +0,0 @@
use libpulse_binding::{
callbacks::ListResult,
context::{
introspect::{CardInfo, SinkInfo, SinkInputInfo, SourceInfo, SourceOutputInfo},
subscribe::{InterestMaskSet, Operation},
},
// def::{SinkState, SourceState},
};
use tracing::{debug, error, info};
use super::{common::*, /*pa_interface::ACTIONS_SX*/};
// use crate::{
// entry::{CardProfile, Entry},
// models::EntryUpdate,
// ui::Rect,
// };
use color_eyre::Result;
use crate::clients::volume::pulse::CardProfile;
pub fn subscribe(
context: &Rc<RefCell<PAContext>>,
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
) -> Result<()> {
info!("[PAInterface] Registering pulseaudio callbacks");
context.borrow_mut().subscribe(
InterestMaskSet::SINK
| InterestMaskSet::SINK_INPUT
| InterestMaskSet::SOURCE
| InterestMaskSet::CARD
| InterestMaskSet::SOURCE_OUTPUT
| InterestMaskSet::CLIENT
| InterestMaskSet::SERVER,
|success: bool| {
assert!(success, "subscription failed");
},
);
context.borrow_mut().set_subscribe_callback(Some(Box::new(
move |facility, operation, index| {
if let Some(facility) = facility {
match facility {
Facility::Server | Facility::Client => {
error!("{:?} {:?}", facility, operation);
return;
}
_ => {}
};
let entry_type: EntryType = facility.into();
match operation {
Some(Operation::New) => {
info!("[PAInterface] New {:?}", entry_type);
info_sx
.send(EntryIdentifier::new(entry_type, index))
.unwrap();
}
Some(Operation::Changed) => {
info!("[PAInterface] {:?} changed", entry_type);
info_sx
.send(EntryIdentifier::new(entry_type, index))
.unwrap();
}
Some(Operation::Removed) => {
info!("[PAInterface] {:?} removed", entry_type);
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryRemoved(EntryIdentifier::new(
// entry_type, index,
// )))
// .unwrap();
}
_ => {}
};
};
},
)));
Ok(())
}
pub fn request_current_state(
context: Rc<RefCell<PAContext>>,
info_sxx: mpsc::UnboundedSender<EntryIdentifier>,
) -> Result<()> {
info!("[PAInterface] Requesting starting state");
let introspector = context.borrow_mut().introspect();
let info_sx = info_sxx.clone();
introspector.get_sink_info_list(move |x: ListResult<&SinkInfo>| {
if let ListResult::Item(e) = x {
let _ = info_sx
.clone()
.send(EntryIdentifier::new(EntryType::Sink, e.index));
}
});
let info_sx = info_sxx.clone();
introspector.get_sink_input_info_list(move |x: ListResult<&SinkInputInfo>| {
if let ListResult::Item(e) = x {
let _ = info_sx.send(EntryIdentifier::new(EntryType::SinkInput, e.index));
}
});
let info_sx = info_sxx.clone();
introspector.get_source_info_list(move |x: ListResult<&SourceInfo>| {
if let ListResult::Item(e) = x {
let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, e.index));
}
});
let info_sx = info_sxx.clone();
introspector.get_source_output_info_list(move |x: ListResult<&SourceOutputInfo>| {
if let ListResult::Item(e) = x {
let _ = info_sx.send(EntryIdentifier::new(EntryType::SourceOutput, e.index));
}
});
introspector.get_card_info_list(move |x: ListResult<&CardInfo>| {
if let ListResult::Item(e) = x {
let _ = info_sxx.send(EntryIdentifier::new(EntryType::Card, e.index));
}
});
Ok(())
}
pub fn request_info(
ident: EntryIdentifier,
context: &Rc<RefCell<PAContext>>,
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
) {
let introspector = context.borrow_mut().introspect();
debug!(
"[PAInterface] Requesting info for {:?} {}",
ident.entry_type, ident.index
);
match ident.entry_type {
EntryType::SinkInput => {
introspector.get_sink_input_info(ident.index, on_sink_input_info(&info_sx));
}
EntryType::Sink => {
introspector.get_sink_info_by_index(ident.index, on_sink_info(&info_sx));
}
EntryType::SourceOutput => {
introspector.get_source_output_info(ident.index, on_source_output_info(&info_sx));
}
EntryType::Source => {
introspector.get_source_info_by_index(ident.index, on_source_info(&info_sx));
}
EntryType::Card => {
introspector.get_card_info_by_index(ident.index, on_card_info);
}
};
}
pub fn on_card_info(res: ListResult<&CardInfo>) {
if let ListResult::Item(i) = res {
let n = match i
.proplist
.get_str(libpulse_binding::proplist::properties::DEVICE_DESCRIPTION)
{
Some(s) => s,
None => String::from(""),
};
let profiles: Vec<CardProfile> = i
.profiles
.iter()
.filter_map(|p| {
p.name.clone().map(|n| CardProfile {
// area: Rect::default(),
is_selected: false,
name: n.to_string(),
description: match &p.description {
Some(s) => s.to_string(),
None => n.to_string(),
},
#[cfg(any(feature = "pa_v13"))]
available: p.available,
})
})
.collect();
let selected_profile = match &i.active_profile {
Some(x) => {
if let Some(n) = &x.name {
profiles.iter().position(|p| p.name == *n)
} else {
None
}
}
None => None,
};
// let ident = EntryIdentifier::new(EntryType::Card, i.index);
// let entry = Entry::new_card_entry(i.index, n, profiles, selected_profile);
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
// .unwrap();
}
}
pub fn on_sink_info(
_sx: &mpsc::UnboundedSender<EntryIdentifier>,
) -> impl Fn(ListResult<&SinkInfo>) {
|res: ListResult<&SinkInfo>| {
if let ListResult::Item(i) = res {
debug!("[PADataInterface] Update {} sink info", i.index);
let name = match &i.description {
Some(name) => name.to_string(),
None => String::new(),
};
// let ident = EntryIdentifier::new(EntryType::Sink, i.index);
// let entry = Entry::new_play_entry(
// EntryType::Sink,
// i.index,
// name,
// None,
// i.mute,
// i.volume,
// Some(i.monitor_source),
// None,
// i.state == SinkState::Suspended,
// );
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
// .unwrap();
}
}
}
pub fn on_sink_input_info(
sx: &mpsc::UnboundedSender<EntryIdentifier>,
) -> impl Fn(ListResult<&SinkInputInfo>) {
let info_sx = sx.clone();
move |res: ListResult<&SinkInputInfo>| {
if let ListResult::Item(i) = res {
debug!("[PADataInterface] Update {} sink input info", i.index);
let n = match i
.proplist
.get_str(libpulse_binding::proplist::properties::APPLICATION_NAME)
{
Some(s) => s,
None => match &i.name {
Some(s) => s.to_string(),
None => String::from(""),
},
};
// let ident = EntryIdentifier::new(EntryType::SinkInput, i.index);
//
// let entry = Entry::new_play_entry(
// EntryType::SinkInput,
// i.index,
// n,
// Some(i.sink),
// i.mute,
// i.volume,
// None,
// Some(i.sink),
// false,
// );
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
// .unwrap();
let _ = info_sx.send(EntryIdentifier::new(EntryType::Sink, i.sink));
}
}
}
pub fn on_source_info(
_sx: &mpsc::UnboundedSender<EntryIdentifier>,
) -> impl Fn(ListResult<&SourceInfo>) {
move |res: ListResult<&SourceInfo>| {
if let ListResult::Item(i) = res {
debug!("[PADataInterface] Update {} source info", i.index);
let name = match &i.description {
Some(name) => name.to_string(),
None => String::new(),
};
// let ident = EntryIdentifier::new(EntryType::Source, i.index);
// let entry = Entry::new_play_entry(
// EntryType::Source,
// i.index,
// name,
// None,
// i.mute,
// i.volume,
// Some(i.index),
// None,
// i.state == SourceState::Suspended,
// );
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
// .unwrap();
}
}
}
pub fn on_source_output_info(
sx: &mpsc::UnboundedSender<EntryIdentifier>,
) -> impl Fn(ListResult<&SourceOutputInfo>) {
let info_sx = sx.clone();
move |res: ListResult<&SourceOutputInfo>| {
if let ListResult::Item(i) = res {
debug!("[PADataInterface] Update {} source output info", i.index);
let n = match i
.proplist
.get_str(libpulse_binding::proplist::properties::APPLICATION_NAME)
{
Some(s) => s,
None => String::from(""),
};
if n == "RsMixerContext" {
return;
}
// let ident = EntryIdentifier::new(EntryType::SourceOutput, i.index);
// let entry = Entry::new_play_entry(
// EntryType::SourceOutput,
// i.index,
// n,
// Some(i.source),
// i.mute,
// i.volume,
// Some(i.source),
// None,
// false,
// );
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryUpdate(ident, Box::new(entry)))
// .unwrap();
let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, i.index));
}
}
}

View File

@@ -1,30 +0,0 @@
pub use std::{cell::RefCell, collections::HashMap, rc::Rc};
pub use libpulse_binding::{
context::{subscribe::Facility, Context as PAContext},
mainloop::{api::Mainloop as MainloopTrait, threaded::Mainloop},
stream::Stream,
};
pub use tokio::sync::mpsc;
pub use super::{monitor::Monitors, PAInternal, SPEC};
// pub use crate::{
// entry::{EntryIdentifier, EntryType},
// models::{EntryUpdate, PulseAudioAction},
// prelude::*,
// };
pub static LOGGING_MODULE: &str = "PAInterface";
impl From<Facility> for EntryType {
fn from(fac: Facility) -> Self {
match fac {
Facility::Sink => EntryType::Sink,
Facility::Source => EntryType::Source,
Facility::SinkInput => EntryType::SinkInput,
Facility::SourceOutput => EntryType::SourceOutput,
Facility::Card => EntryType::Card,
_ => EntryType::Sink,
}
}
}

View File

@@ -1,17 +0,0 @@
// use thiserror::Error;
//
// use super::PAInternal;
//
// #[derive(Debug, Error)]
// pub enum PAError {
// #[error("cannot create pulseaudio mainloop")]
// MainloopCreateError,
// #[error("cannot connect pulseaudio mainloop")]
// MainloopConnectError,
// #[error("cannot create pulseaudio stream")]
// StreamCreateError,
// #[error("internal channel send error")]
// ChannelError(#[from] cb_channel::SendError<PAInternal>),
// #[error("pulseaudio disconnected")]
// PulseAudioDisconnected,
// }

View File

@@ -1,36 +0,0 @@
mod callbacks;
pub mod common;
mod errors;
mod monitor;
mod pa_actions;
mod pa_interface;
use common::*;
use lazy_static::lazy_static;
pub use pa_interface::start;
#[derive(Debug)]
pub enum PAInternal {
Tick,
Command(Box<PulseAudioAction>),
AskInfo(EntryIdentifier),
}
lazy_static! {
pub static ref SPEC: libpulse_binding::sample::Spec = libpulse_binding::sample::Spec {
format: libpulse_binding::sample::Format::FLOAT32NE,
channels: 1,
rate: 1024,
};
}
#[derive(PartialEq, Clone, Debug)]
pub struct CardProfile {
pub name: String,
pub description: String,
#[cfg(any(feature = "pa_v13"))]
pub available: bool,
// pub area: Rect,
pub is_selected: bool,
}
impl Eq for CardProfile {}

View File

@@ -1,278 +0,0 @@
use std::convert::TryInto;
use libpulse_binding::stream::PeekResult;
use tracing::{debug, error, info, warn};
use super::{common::*, /*pa_interface::ACTIONS_SX*/};
// use crate::VARIABLES;
use color_eyre::{Report, Result};
pub struct Monitor {
stream: Rc<RefCell<Stream>>,
exit_sender: mpsc::UnboundedSender<u32>,
}
pub struct Monitors {
monitors: HashMap<EntryIdentifier, Monitor>,
errors: HashMap<EntryIdentifier, usize>,
}
impl Default for Monitors {
fn default() -> Self {
Self {
monitors: HashMap::new(),
errors: HashMap::new(),
}
}
}
impl Monitors {
pub fn filter(
&mut self,
mainloop: &Rc<RefCell<Mainloop>>,
context: &Rc<RefCell<PAContext>>,
targets: &HashMap<EntryIdentifier, Option<u32>>,
) {
// remove failed streams
// then send exit signal if stream is unwanted
self.monitors.retain(|ident, monitor| {
match monitor.stream.borrow_mut().get_state() {
libpulse_binding::stream::State::Terminated
| libpulse_binding::stream::State::Failed => {
info!(
"[PAInterface] Disconnecting {} sink input monitor (failed state)",
ident.index
);
return false;
}
_ => {}
};
if targets.get(ident) == None {
let _ = monitor.exit_sender.send(0);
}
true
});
targets.iter().for_each(|(ident, monitor_src)| {
if self.monitors.get(ident).is_none() {
self.create_monitor(mainloop, context, *ident, *monitor_src);
}
});
}
fn create_monitor(
&mut self,
mainloop: &Rc<RefCell<Mainloop>>,
context: &Rc<RefCell<PAContext>>,
ident: EntryIdentifier,
monitor_src: Option<u32>,
) {
if let Some(count) = self.errors.get(&ident) {
if *count >= 5 {
self.errors.remove(&ident);
// (*ACTIONS_SX)
// .get()
// .send(EntryUpdate::EntryRemoved(ident))
// .unwrap();
}
}
if self.monitors.contains_key(&ident) {
return;
}
let (sx, rx) = mpsc::unbounded_channel();
if let Ok(stream) = create(
&mainloop,
&context,
&libpulse_binding::sample::Spec {
format: libpulse_binding::sample::Format::FLOAT32NE,
channels: 1,
rate: /*(*VARIABLES).get().pa_rate*/ 20,
},
ident,
monitor_src,
rx,
) {
self.monitors.insert(
ident,
Monitor {
stream,
exit_sender: sx,
},
);
self.errors.remove(&ident);
} else {
self.error(&ident);
}
}
fn error(&mut self, ident: &EntryIdentifier) {
let count = match self.errors.get(&ident) {
Some(x) => *x,
None => 0,
};
self.errors.insert(*ident, count + 1);
}
}
fn slice_to_4_bytes(slice: &[u8]) -> [u8; 4] {
slice.try_into().expect("slice with incorrect length")
}
fn create(
p_mainloop: &Rc<RefCell<Mainloop>>,
p_context: &Rc<RefCell<PAContext>>,
p_spec: &libpulse_binding::sample::Spec,
ident: EntryIdentifier,
source_index: Option<u32>,
mut close_rx: mpsc::UnboundedReceiver<u32>,
) -> Result<Rc<RefCell<Stream>>> {
info!("[PADataInterface] Attempting to create new monitor stream");
let stream_index = if ident.entry_type == EntryType::SinkInput {
Some(ident.index)
} else {
None
};
let stream = Rc::new(RefCell::new(
match Stream::new(&mut p_context.borrow_mut(), "RsMixer monitor", p_spec, None) {
Some(stream) => stream,
None => return Err(Report::msg("Error creating stream for monitoring volume")),
},
));
// Stream state change callback
{
debug!("[PADataInterface] Registering stream state change callback");
let ml_ref = Rc::clone(&p_mainloop);
let stream_ref = Rc::downgrade(&stream);
stream
.borrow_mut()
.set_state_callback(Some(Box::new(move || {
let state = unsafe { (*(*stream_ref.as_ptr()).as_ptr()).get_state() };
match state {
libpulse_binding::stream::State::Ready
| libpulse_binding::stream::State::Failed
| libpulse_binding::stream::State::Terminated => {
unsafe { (*ml_ref.as_ptr()).signal(false) };
}
_ => {}
}
})));
}
// for sink inputs we want to set monitor stream to sink
if let Some(index) = stream_index {
stream.borrow_mut().set_monitor_stream(index).unwrap();
}
let x;
let mut s = None;
if let Some(i) = source_index {
x = i.to_string();
s = Some(x.as_str());
}
debug!("[PADataInterface] Connecting stream");
match stream.borrow_mut().connect_record(
s,
Some(&libpulse_binding::def::BufferAttr {
maxlength: std::u32::MAX,
tlength: std::u32::MAX,
prebuf: std::u32::MAX,
minreq: 0,
fragsize: /*(*VARIABLES).get().pa_frag_size*/ 48,
}),
libpulse_binding::stream::FlagSet::PEAK_DETECT
| libpulse_binding::stream::FlagSet::ADJUST_LATENCY,
) {
Ok(_) => {}
Err(err) => {
return Err(Report::new(err).wrap_err("while connecting stream for monitoring volume"));
}
};
debug!("[PADataInterface] Waiting for stream to be ready");
loop {
match stream.borrow_mut().get_state() {
libpulse_binding::stream::State::Ready => {
break;
}
libpulse_binding::stream::State::Failed
| libpulse_binding::stream::State::Terminated => {
error!("[PADataInterface] Stream state failed/terminated");
return Err(Report::msg("Stream terminated"))
}
_ => {
p_mainloop.borrow_mut().wait();
}
}
}
stream.borrow_mut().set_state_callback(None);
{
info!("[PADataInterface] Registering stream read callback");
let ml_ref = Rc::clone(&p_mainloop);
let stream_ref = Rc::downgrade(&stream);
stream.borrow_mut().set_read_callback(Some(Box::new(move |_size: usize| {
let remove_failed = || {
error!("[PADataInterface] Monitor failed or terminated");
};
let disconnect_stream = || {
warn!("[PADataInterface] {:?} Monitor existed while the sink (input)/source (output) was already gone", ident);
unsafe {
(*(*stream_ref.as_ptr()).as_ptr()).disconnect().unwrap();
(*ml_ref.as_ptr()).signal(false);
};
};
if close_rx.try_recv().is_ok() {
disconnect_stream();
return;
}
match unsafe {(*(*stream_ref.as_ptr()).as_ptr()).get_state() }{
libpulse_binding::stream::State::Failed => {
remove_failed();
},
libpulse_binding::stream::State::Terminated => {
remove_failed();
},
libpulse_binding::stream::State::Ready => {
match unsafe{ (*(*stream_ref.as_ptr()).as_ptr()).peek() } {
Ok(res) => match res {
PeekResult::Data(data) => {
let count = data.len() / 4;
let mut peak = 0.0;
for c in 0..count {
let data_slice = slice_to_4_bytes(&data[c * 4 .. (c + 1) * 4]);
peak += f32::from_ne_bytes(data_slice).abs();
}
peak = peak / count as f32;
// if (*ACTIONS_SX).get().send(EntryUpdate::PeakVolumeUpdate(ident, peak)).is_err() {
// disconnect_stream();
// }
unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); };
},
PeekResult::Hole(_) => {
unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); };
},
_ => {},
},
Err(_) => {
remove_failed();
},
}
},
_ => {},
};
})));
}
Ok(stream)
}

View File

@@ -1,148 +0,0 @@
use super::{callbacks, common::*};
pub fn handle_command(
cmd: PulseAudioAction,
context: &Rc<RefCell<PAContext>>,
info_sx: &mpsc::UnboundedSender<EntryIdentifier>,
) -> Option<()> {
match cmd {
PulseAudioAction::RequestPulseAudioState => {
callbacks::request_current_state(Rc::clone(&context), info_sx.clone()).unwrap();
}
PulseAudioAction::MuteEntry(ident, mute) => {
set_mute(ident, mute, &context);
}
PulseAudioAction::MoveEntryToParent(ident, parent) => {
move_entry_to_parent(ident, parent, &context, info_sx.clone());
}
PulseAudioAction::ChangeCardProfile(ident, profile) => {
change_card_profile(ident, profile, &context);
}
PulseAudioAction::SetVolume(ident, vol) => {
set_volume(ident, vol, &context);
}
PulseAudioAction::SetSuspend(ident, suspend) => {
set_suspend(ident, suspend, &context);
}
PulseAudioAction::KillEntry(ident) => {
kill_entry(ident, &context);
}
PulseAudioAction::Shutdown => {
//@TODO disconnect monitors
return None;
}
_ => {}
};
Some(())
}
fn set_volume(
ident: EntryIdentifier,
vol: libpulse_binding::volume::ChannelVolumes,
context: &Rc<RefCell<PAContext>>,
) {
let mut introspector = context.borrow_mut().introspect();
match ident.entry_type {
EntryType::Sink => {
introspector.set_sink_volume_by_index(ident.index, &vol, None);
}
EntryType::SinkInput => {
introspector.set_sink_input_volume(ident.index, &vol, None);
}
EntryType::Source => {
introspector.set_source_volume_by_index(ident.index, &vol, None);
}
EntryType::SourceOutput => {
introspector.set_source_output_volume(ident.index, &vol, None);
}
_ => {}
};
}
fn change_card_profile(ident: EntryIdentifier, profile: String, context: &Rc<RefCell<PAContext>>) {
if ident.entry_type != EntryType::Card {
return;
}
context
.borrow_mut()
.introspect()
.set_card_profile_by_index(ident.index, &profile[..], None);
}
fn move_entry_to_parent(
ident: EntryIdentifier,
parent: EntryIdentifier,
context: &Rc<RefCell<PAContext>>,
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
) {
let mut introspector = context.borrow_mut().introspect();
match ident.entry_type {
EntryType::SinkInput => {
introspector.move_sink_input_by_index(
ident.index,
parent.index,
Some(Box::new(move |_| {
info_sx.send(parent).unwrap();
info_sx.send(ident).unwrap();
})),
);
}
EntryType::SourceOutput => {
introspector.move_source_output_by_index(
ident.index,
parent.index,
Some(Box::new(move |_| {
info_sx.send(parent).unwrap();
info_sx.send(ident).unwrap();
})),
);
}
_ => {}
};
}
fn set_suspend(ident: EntryIdentifier, suspend: bool, context: &Rc<RefCell<PAContext>>) {
let mut introspector = context.borrow_mut().introspect();
match ident.entry_type {
EntryType::Sink => {
introspector.suspend_sink_by_index(ident.index, suspend, None);
}
EntryType::Source => {
introspector.suspend_source_by_index(ident.index, suspend, None);
}
_ => {}
};
}
fn kill_entry(ident: EntryIdentifier, context: &Rc<RefCell<PAContext>>) {
let mut introspector = context.borrow_mut().introspect();
match ident.entry_type {
EntryType::SinkInput => {
introspector.kill_sink_input(ident.index, |_| {});
}
EntryType::SourceOutput => {
introspector.kill_source_output(ident.index, |_| {});
}
_ => {}
};
}
fn set_mute(ident: EntryIdentifier, mute: bool, context: &Rc<RefCell<PAContext>>) {
let mut introspector = context.borrow_mut().introspect();
match ident.entry_type {
EntryType::Sink => {
introspector.set_sink_mute_by_index(ident.index, mute, None);
}
EntryType::SinkInput => {
introspector.set_sink_input_mute(ident.index, mute, None);
}
EntryType::Source => {
introspector.set_source_mute_by_index(ident.index, mute, None);
}
EntryType::SourceOutput => {
introspector.set_source_output_mute(ident.index, mute, None);
}
_ => {}
};
}

View File

@@ -1,162 +0,0 @@
use std::ops::Deref;
// use lazy_static::lazy_static;
use libpulse_binding::proplist::Proplist;
use tracing::{debug, error, info};
// use state::Storage;
use color_eyre::{Report, Result};
use super::{callbacks, common::*, pa_actions};
// lazy_static! {
// pub static ref ACTIONS_SX: Storage<mpsc::UnboundedSender<EntryUpdate>> = Storage::new();
// }
pub async fn start(
mut internal_rx: mpsc::Receiver<PAInternal>,
info_sx: mpsc::UnboundedSender<EntryIdentifier>,
actions_sx: mpsc::UnboundedSender<EntryUpdate>,
) -> Result<()> {
// (*ACTIONS_SX).set(actions_sx);
// Create new mainloop and context
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(libpulse_binding::proplist::properties::APPLICATION_NAME, "RsMixer")
.unwrap();
debug!("[PAInterface] Creating new mainloop");
let mainloop = Rc::new(RefCell::new(match Mainloop::new() {
Some(ml) => ml,
None => {
error!("[PAInterface] Error while creating new mainloop");
return Err(Report::msg("Error while creating new mainloop"));
}
}));
debug!("[PAInterface] Creating new context");
let context = Rc::new(RefCell::new(
match PAContext::new_with_proplist(
mainloop.borrow_mut().deref().deref(),
"RsMixerContext",
&proplist,
) {
Some(ctx) => ctx,
None => {
error!("[PAInterface] Error while creating new context");
return Err(Report::msg("Error while creating new context"));
}
},
));
// PAContext state change callback
{
debug!("[PAInterface] Registering state change callback");
let ml_ref = Rc::clone(&mainloop);
let context_ref = Rc::clone(&context);
context
.borrow_mut()
.set_state_callback(Some(Box::new(move || {
let state = unsafe { (*context_ref.as_ptr()).get_state() };
if matches!(
state,
libpulse_binding::context::State::Ready
| libpulse_binding::context::State::Failed
| libpulse_binding::context::State::Terminated
) {
unsafe { (*ml_ref.as_ptr()).signal(false) };
}
})));
}
// Try to connect to pulseaudio
debug!("[PAInterface] Connecting context");
if context
.borrow_mut()
.connect(None, libpulse_binding::context::FlagSet::NOFLAGS, None)
.is_err()
{
error!("[PAInterface] Error while connecting context");
return Err(Report::msg("Error while connecting context"));
}
info!("[PAInterface] Starting mainloop");
// start mainloop
mainloop.borrow_mut().lock();
if let Err(err) = mainloop.borrow_mut().start() {
return Err(Report::new(err));
}
debug!("[PAInterface] Waiting for context to be ready...");
// wait for context to be ready
loop {
match context.borrow_mut().get_state() {
libpulse_binding::context::State::Ready => {
break;
}
libpulse_binding::context::State::Failed | libpulse_binding::context::State::Terminated => {
mainloop.borrow_mut().unlock();
mainloop.borrow_mut().stop();
error!("[PAInterface] Connection failed or context terminated");
return Err(Report::msg("Connection failed or context terminated"));
}
_ => {
mainloop.borrow_mut().wait();
}
}
}
debug!("[PAInterface] PAContext ready");
context.borrow_mut().set_state_callback(None);
callbacks::subscribe(&context, info_sx.clone())?;
callbacks::request_current_state(context.clone(), info_sx.clone())?;
mainloop.borrow_mut().unlock();
debug!("[PAInterface] Actually starting our mainloop");
let mut monitors = Monitors::default();
let mut last_targets = HashMap::new();
while let Some(msg) = internal_rx.recv().await {
mainloop.borrow_mut().lock();
match context.borrow_mut().get_state() {
libpulse_binding::context::State::Ready => {}
_ => {
mainloop.borrow_mut().unlock();
return Err(Report::msg("Disconnected while working"))
}
}
match msg {
PAInternal::AskInfo(ident) => {
callbacks::request_info(ident, &context, info_sx.clone());
}
PAInternal::Tick => {
// remove failed monitors
monitors.filter(&mainloop, &context, &last_targets);
}
PAInternal::Command(cmd) => {
let cmd = cmd.deref();
if pa_actions::handle_command(cmd.clone(), &context, &info_sx).is_none() {
monitors.filter(&mainloop, &context, &HashMap::new());
mainloop.borrow_mut().unlock();
break;
}
if let PulseAudioAction::CreateMonitors(mons) = cmd.clone() {
last_targets = mons;
monitors.filter(&mainloop, &context, &last_targets);
}
}
};
mainloop.borrow_mut().unlock();
}
Ok(())
}

View File

@@ -1,90 +0,0 @@
use crate::clients::volume::VolumeClient;
use libpulse_binding::context::State;
use libpulse_binding::{
context::{Context, FlagSet},
mainloop::threaded::Mainloop,
proplist::{properties, Proplist},
};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use tracing::{debug, error};
pub fn test() {
let mut prop_list = Proplist::new().unwrap();
prop_list
.set_str(properties::APPLICATION_NAME, "ironbar")
.unwrap();
let mainloop = Rc::new(RefCell::new(Mainloop::new().unwrap()));
let context = Rc::new(RefCell::new(
Context::new_with_proplist(mainloop.borrow().deref(), "ironbar_context", &prop_list)
.unwrap(),
));
// PAContext state change callback
{
debug!("[PAInterface] Registering state change callback");
let ml_ref = Rc::clone(&mainloop);
let context_ref = Rc::clone(&context);
context
.borrow_mut()
.set_state_callback(Some(Box::new(move || {
let state = unsafe { (*context_ref.as_ptr()).get_state() };
if matches!(state, State::Ready | State::Failed | State::Terminated) {
unsafe { (*ml_ref.as_ptr()).signal(false) };
}
})));
}
if let Err(err) = context.borrow_mut().connect(None, FlagSet::NOFLAGS, None) {
error!("{err:?}");
}
println!("{:?}", context.borrow().get_server());
mainloop.borrow_mut().lock();
if let Err(err) = mainloop.borrow_mut().start() {
error!("{err:?}");
}
debug!("[PAInterface] Waiting for context to be ready...");
println!("[PAInterface] Waiting for context to be ready...");
// wait for context to be ready
loop {
match context.borrow().get_state() {
State::Ready => {
break;
}
State::Failed | State::Terminated => {
mainloop.borrow_mut().unlock();
mainloop.borrow_mut().stop();
error!("[PAInterface] Connection failed or context terminated");
}
_ => {
mainloop.borrow_mut().wait();
}
}
}
debug!("[PAInterface] PAContext ready");
println!("[PAInterface] PAContext ready");
context.borrow_mut().set_state_callback(None);
println!("jfgjfgg");
let introspector = context.borrow().introspect();
println!("jfgjfgg2");
introspector.get_sink_info_list(|result| {
println!("boo: {result:?}");
});
println!("fjgjfgf??");
}
struct PulseVolumeClient {}
impl VolumeClient for PulseVolumeClient {}

View File

@@ -1,79 +1,90 @@
use super::wlr_foreign_toplevel::{ use super::wlr_foreign_toplevel::handle::ToplevelHandle;
handle::{ToplevelEvent, ToplevelInfo}, use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
manager::listen_for_toplevels, use super::wlr_foreign_toplevel::ToplevelEvent;
}; use super::Environment;
use super::{DData, Env, ToplevelHandler}; use crate::error::ERR_CHANNEL_RECV;
use crate::{error as err, send}; use crate::send;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::Report; use color_eyre::Report;
use indexmap::IndexMap; use smithay_client_toolkit::output::{OutputInfo, OutputState};
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender}; use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
use smithay_client_toolkit::reexports::calloop::EventLoop; use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::WaylandSource; use smithay_client_toolkit::registry::RegistryState;
use smithay_client_toolkit::seat::SeatState;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, RwLock}; use std::sync::mpsc;
use tokio::sync::{broadcast, oneshot}; use tokio::sync::broadcast;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::{debug, error}; use tracing::{debug, error, trace};
use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{ConnectError, Display, EventQueue}; use wayland_client::{Connection, WaylandSource};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
use super::{ClipboardItem}; use super::ClipboardItem;
use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler}; use super::wlr_data_control::manager::DataControlDeviceManagerState;
use crate::{read_lock, write_lock}; use crate::lock;
use tokio::spawn; use std::sync::{Arc, Mutex};
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Request { 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 /// Copies the value to the clipboard
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
CopyToClipboard(Arc<ClipboardItem>), CopyToClipboard(Arc<ClipboardItem>),
/// Forces a dispatch, flushing any currently queued events /// Forces a dispatch, flushing any currently queued events
Refresh, Roundtrip,
} }
pub struct WaylandClient { pub struct WaylandClient {
pub outputs: Vec<OutputInfo>, // External channels
pub seats: Vec<WlSeat>,
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>, toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>, _toplevel_rx: broadcast::Receiver<ToplevelEvent>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>, clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>, _clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>,
// Internal channels
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
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>, request_tx: Sender<Request>,
} }
impl WaylandClient { impl WaylandClient {
pub(super) async fn new() -> Self { pub(super) async fn new() -> Self {
let (output_tx, output_rx) = oneshot::channel();
let (seat_tx, seat_rx) = oneshot::channel();
let (toplevel_tx, toplevel_rx) = broadcast::channel(32); let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
let toplevels = Arc::new(RwLock::new(IndexMap::new())); let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel();
let toplevels2 = toplevels.clone(); #[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 toplevel_tx2 = toplevel_tx.clone();
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32); let (clipboard_tx, clipboard_rx) = broadcast::channel(32);
let clipboard = Arc::new(RwLock::new(None));
let clipboard_tx2 = clipboard_tx.clone(); let clipboard_tx2 = clipboard_tx.clone();
} }
} }
@@ -82,85 +93,100 @@ impl WaylandClient {
// `queue` is not `Send` so we need to handle everything inside the task // `queue` is not `Send` so we need to handle everything inside the task
spawn_blocking(move || { spawn_blocking(move || {
let toplevels = toplevels2;
let toplevel_tx = toplevel_tx2; let toplevel_tx = toplevel_tx2;
#[cfg(feature = "clipboard")]
let clipboard_tx = clipboard_tx2;
let (env, _display, queue) = let conn =
Self::new_environment().expect("Failed to connect to Wayland compositor"); 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 = let mut event_loop =
EventLoop::<DData>::try_new().expect("Failed to create new event loop"); EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
WaylandSource::new(queue) WaylandSource::new(queue)
.quick_insert(event_loop.handle()) .expect("Failed to create Wayland source from queue")
.insert(event_loop.handle())
.expect("Failed to insert Wayland event queue into event loop"); .expect("Failed to insert Wayland event queue into event loop");
let outputs = Self::get_outputs(&env); let loop_handle = event_loop.handle();
send!(output_tx, outputs);
let seats = env.get_all_seats(); // Initialize the registry handling
// so other parts of Smithay's client toolkit may bind globals.
let registry_state = RegistryState::new(&globals);
let output_delegate = OutputState::new(&globals, &qh);
let seat_delegate = SeatState::new(&globals, &qh);
// TODO: Actually handle seats properly
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
let default_seat = seats[0].detach(); let data_control_device_manager_delegate =
DataControlDeviceManagerState::bind(&globals, &qh)
.expect("data device manager is not available");
send!( let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh)
seat_tx, .expect("foreign toplevel manager is not available");
seats
.into_iter()
.map(|seat| seat.detach())
.collect::<Vec<WlSeat>>()
);
let handle = event_loop.handle(); let mut env = Environment {
handle registry_state,
.insert_source(ev_rx, move |event, _metadata, ddata| { output_state: output_delegate,
// let env = &ddata.env; 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 { match event {
Event::Msg(Request::Refresh) => debug!("Received refresh 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")] #[cfg(feature = "clipboard")]
Event::Msg(Request::CopyToClipboard(value)) => { Event::Msg(Request::CopyToClipboard(value)) => {
super::wlr_data_control::copy_to_clipboard( env.copy_to_clipboard(value, &qh);
&ddata.env,
&default_seat,
&value,
)
.expect("Failed to copy to clipboard");
} }
Event::Closed => panic!("Channel unexpectedly closed"), Event::Closed => panic!("Channel unexpectedly closed"),
} }
}) })
.expect("Failed to insert channel into event queue"); .expect("Failed to insert channel into event queue");
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
super::wlr_foreign_toplevel::update_toplevels(
&toplevels,
handle,
event,
&toplevel_tx,
);
});
cfg_if! {
if #[cfg(feature = "clipboard")] {
let clipboard_tx = clipboard_tx2;
let handle = event_loop.handle();
let _offer_listener = listen_to_devices(&env, move |_seat, event, ddata| {
debug!("Received clipboard event");
super::wlr_data_control::receive_offer(event, &handle, clipboard_tx.clone(), ddata);
});
}
}
let mut data = DData {
env,
offer_tokens: HashMap::new(),
};
loop { loop {
if let Err(err) = event_loop.dispatch(None, &mut data) { trace!("Dispatching event loop");
if let Err(err) = event_loop.dispatch(None, &mut env) {
error!( error!(
"{:?}", "{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events") Report::new(err).wrap_err("Failed to dispatch pending wayland events")
@@ -169,119 +195,76 @@ impl WaylandClient {
} }
}); });
// keep track of current clipboard item
#[cfg(feature = "clipboard")]
{
let clipboard = clipboard.clone();
spawn(async move {
while let Ok(item) = clipboard_rx.recv().await {
let mut clipboard = write_lock!(clipboard);
clipboard.replace(item);
}
});
}
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
Self { Self {
outputs,
seats,
#[cfg(feature = "clipboard")]
clipboard,
toplevels,
toplevel_tx, toplevel_tx,
_toplevel_rx: toplevel_rx, _toplevel_rx: toplevel_rx,
toplevel_init_rx,
#[cfg(feature = "clipboard")]
clipboard_init_rx,
output_rx,
seat_rx,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx, clipboard_tx,
#[cfg(feature = "clipboard")]
_clipboard_rx: clipboard_rx,
request_tx: ev_tx, request_tx: ev_tx,
} }
} }
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> { pub fn subscribe_toplevels(
self.toplevel_tx.subscribe() &self,
) -> (
broadcast::Receiver<ToplevelEvent>,
HashMap<usize, ToplevelHandle>,
) {
let rx = self.toplevel_tx.subscribe();
let receiver = &self.toplevel_init_rx;
send!(self.request_tx, Request::Toplevels);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
} }
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub fn subscribe_clipboard(&self) -> broadcast::Receiver<Arc<ClipboardItem>> { pub fn subscribe_clipboard(
self.clipboard_tx.subscribe() &self,
) -> (
broadcast::Receiver<Arc<ClipboardItem>>,
Option<Arc<ClipboardItem>>,
) {
let rx = self.clipboard_tx.subscribe();
let receiver = &self.clipboard_init_rx;
send!(self.request_tx, Request::Clipboard);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
} }
/// Force a roundtrip on the wayland connection,
/// flushing any queued events and immediately receiving any new ones.
pub fn roundtrip(&self) { pub fn roundtrip(&self) {
send!(self.request_tx, Request::Refresh); trace!("Sending roundtrip request");
send!(self.request_tx, Request::Roundtrip);
} }
#[cfg(feature = "clipboard")] pub fn get_outputs(&self) -> Vec<OutputInfo> {
pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> { trace!("Sending get outputs request");
let clipboard = read_lock!(self.clipboard);
clipboard.as_ref().cloned() 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")] #[cfg(feature = "clipboard")]
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) { pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
send!(self.request_tx, Request::CopyToClipboard(item)); send!(self.request_tx, Request::CopyToClipboard(item));
} }
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
let outputs = env.get_all_outputs();
outputs
.iter()
.filter_map(|output| with_output_info(output, Clone::clone))
.collect()
}
fn new_environment() -> Result<(Environment<Env>, Display, EventQueue), ConnectError> {
Display::connect_to_env().and_then(|display| {
let mut queue = display.create_event_queue();
let ret = {
let mut sctk_seats = smithay_client_toolkit::seat::SeatHandler::new();
let sctk_data_device_manager =
smithay_client_toolkit::data_device::DataDeviceHandler::init(&mut sctk_seats);
#[cfg(feature = "clipboard")]
let data_control_device = DataControlDeviceHandler::init(&mut sctk_seats);
let sctk_primary_selection_manager =
smithay_client_toolkit::primary_selection::PrimarySelectionHandler::init(
&mut sctk_seats,
);
let display = ::smithay_client_toolkit::reexports::client::Proxy::clone(&display);
let env = Environment::new(
&display.attach(queue.token()),
&mut queue,
Env {
sctk_compositor: smithay_client_toolkit::environment::SimpleGlobal::new(),
sctk_subcompositor: smithay_client_toolkit::environment::SimpleGlobal::new(
),
sctk_shm: smithay_client_toolkit::shm::ShmHandler::new(),
sctk_outputs: smithay_client_toolkit::output::OutputHandler::new(),
sctk_seats,
sctk_data_device_manager,
sctk_primary_selection_manager,
toplevel: ToplevelHandler::init(),
#[cfg(feature = "clipboard")]
data_control_device,
},
);
if let Ok(env) = env.as_ref() {
let _psm = env.get_primary_selection_manager();
}
env
};
match ret {
Ok(env) => Ok((env, display, queue)),
Err(_e) => display.protocol_error().map_or_else(
|| Err(ConnectError::NoCompositorListening),
|perr| {
panic!("[SCTK] A protocol error occured during initial setup: {perr}");
},
),
}
})
}
} }

View 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
);
};
}

View File

@@ -1,70 +1,110 @@
mod client; mod client;
mod macros;
mod wl_output;
mod wl_seat;
mod wlr_foreign_toplevel; mod wlr_foreign_toplevel;
use std::collections::HashMap; use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
use async_once::AsyncOnce; use async_once::AsyncOnce;
use lazy_static::lazy_static;
use std::fmt::Debug;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use smithay_client_toolkit::default_environment; use lazy_static::lazy_static;
use smithay_client_toolkit::environment::Environment; use smithay_client_toolkit::output::OutputState;
use smithay_client_toolkit::reexports::calloop::RegistrationToken; use smithay_client_toolkit::reexports::calloop::LoopHandle;
use wayland_client::{Attached, Interface}; use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1; use smithay_client_toolkit::seat::SeatState;
pub use wlr_foreign_toplevel::handle::{ToplevelChange, ToplevelEvent, ToplevelInfo}; use smithay_client_toolkit::{
use wlr_foreign_toplevel::manager::{ToplevelHandler}; delegate_output, delegate_registry, delegate_seat, registry_handlers,
};
use std::collections::HashMap;
use tokio::sync::broadcast;
use wayland_client::protocol::wl_seat::WlSeat;
pub use client::WaylandClient; pub use self::client::WaylandClient;
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
mod wlr_data_control; mod wlr_data_control;
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source};
use wlr_data_control::manager::DataControlDeviceHandler; 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;
use std::sync::{Arc, Mutex};
pub use wlr_data_control::{ClipboardItem, ClipboardValue}; pub use wlr_data_control::{ClipboardItem, ClipboardValue};
pub struct DataControlDeviceEntry {
seat: WlSeat,
device: DataControlDevice,
}
} }
} }
/// A utility for lazy-loading globals. pub struct Environment {
/// Taken from `smithay_client_toolkit` where it's not exposed pub registry_state: RegistryState,
#[derive(Debug)] pub output_state: OutputState,
enum LazyGlobal<I: Interface> { pub seat_state: SeatState,
Unknown, pub foreign_toplevel_manager_state: ToplevelManagerState,
Seen { id: u32, version: u32 }, #[cfg(feature = "clipboard")]
Bound(Attached<I>), 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 struct DData { // Now we need to say we are delegating the responsibility of output related events for our application data
env: Environment<Env>, // type to the requisite delegate.
offer_tokens: HashMap<u128, RegistrationToken>, delegate_output!(Environment);
} delegate_seat!(Environment);
delegate_foreign_toplevel_manager!(Environment);
delegate_foreign_toplevel_handle!(Environment);
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
default_environment!(Env, delegate_data_control_device_manager!(Environment);
fields = [ delegate_data_control_device!(Environment);
toplevel: ToplevelHandler, delegate_data_control_source!(Environment);
data_control_device: DataControlDeviceHandler delegate_data_control_offer!(Environment);
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
ZwlrDataControlManagerV1 => data_control_device
],
);
} else {
default_environment!(Env,
fields = [
toplevel: ToplevelHandler,
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
],
);
} }
} }
// 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! { lazy_static! {
static ref CLIENT: AsyncOnce<WaylandClient> = static ref CLIENT: AsyncOnce<WaylandClient> =
AsyncOnce::new(async { WaylandClient::new().await }); AsyncOnce::new(async { WaylandClient::new().await });

View 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");
}
}

View 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);
}
}

View File

@@ -1,88 +1,166 @@
use super::offer::DataControlOffer; use super::manager::DataControlDeviceManagerState;
use super::source::DataControlSource; use super::offer::{
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
};
use crate::error::ERR_WAYLAND_DATA;
use crate::lock; use crate::lock;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use wayland_client::protocol::wl_seat::WlSeat; use tracing::warn;
use wayland_client::{Attached, DispatchData, Main}; use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols::wlr::unstable::data_control::v1::client::{ use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1}, zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
}; };
#[derive(Debug)] pub struct DataControlDevice {
struct Inner { pub device: ZwlrDataControlDeviceV1,
offer: Option<Arc<DataControlOffer>>,
} }
impl Inner { #[derive(Debug, Default)]
fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) { pub struct DataControlDeviceInner {
self.offer.replace(Arc::new(DataControlOffer::new(offer))); /// 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()
})
} }
} }
#[derive(Debug, Clone)] impl DataControlDeviceDataExt for DataControlDevice {
pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>); type DataControlOfferInner = DataControlOfferData;
fn data_control_device_data(&self) -> &DataControlDeviceData {
self.device.data().expect(ERR_WAYLAND_DATA)
}
}
fn data_control_device_implem<F>( impl DataControlDeviceDataExt for DataControlDeviceData {
event: Event, type DataControlOfferInner = DataControlOfferData;
inner: &mut Inner, fn data_control_device_data(&self) -> &DataControlDeviceData {
implem: &mut F, self
ddata: DispatchData, }
) where }
F: FnMut(DataControlDeviceEvent, DispatchData),
/// 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 { match event {
Event::DataOffer { id } => { Event::DataOffer { id } => {
inner.new_offer(&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: Some(offer) } => { Event::Selection { id } => {
let inner_offer = inner let mut selection_offer = lock!(inner.selection_offer);
.offer
.clone() if let Some(offer) = id {
.expect("Offer should exist at this stage"); let mut undetermined = lock!(inner.undetermined_offers);
if offer == inner_offer.offer { if let Some(i) = undetermined.iter().position(|o| o == &offer) {
implem(DataControlDeviceEvent(inner_offer), ddata); 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.");
} }
_ => {} _ => {}
} }
} }
pub struct DataControlDevice {
device: ZwlrDataControlDeviceV1,
_inner: Arc<Mutex<Inner>>,
}
impl DataControlDevice {
pub fn init_for_seat<F>(
manager: &Attached<ZwlrDataControlManagerV1>,
seat: &WlSeat,
mut callback: F,
) -> Self
where
F: FnMut(DataControlDeviceEvent, DispatchData) + 'static,
{
let inner = Arc::new(Mutex::new(Inner { offer: None }));
let device = manager.get_data_device(seat);
{
let inner = inner.clone();
device.quick_assign(move |_handle, event, ddata| {
let mut inner = lock!(inner);
data_control_device_implem(event, &mut inner, &mut callback, ddata);
});
}
Self {
device: device.detach(),
_inner: inner,
}
}
pub fn set_selection(&self, source: &Option<DataControlSource>) {
self.device
.set_selection(source.as_ref().map(|s| &s.source));
}
} }

View File

@@ -1,253 +1,132 @@
use super::device::{DataControlDevice, DataControlDeviceEvent}; use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt};
use super::source::DataControlSource; use super::offer::DataControlOfferData;
use smithay_client_toolkit::data_device::WritePipe; use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt};
use smithay_client_toolkit::environment::{Environment, GlobalHandler}; use smithay_client_toolkit::error::GlobalError;
use smithay_client_toolkit::seat::{SeatHandling, SeatListener}; use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use smithay_client_toolkit::MissingGlobal; use std::marker::PhantomData;
use std::cell::RefCell; use tracing::debug;
use std::rc::{self, Rc}; use wayland_client::globals::{BindError, GlobalList};
use tracing::warn;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Attached, DispatchData}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
enum DataControlDeviceHandlerInner { zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
Ready { zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
manager: Attached<ZwlrDataControlManagerV1>,
devices: Vec<(WlSeat, DataControlDevice)>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
Pending {
seats: Vec<WlSeat>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
}
impl DataControlDeviceHandlerInner {
fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
let (seats, status_listeners) = if let Self::Pending {
seats,
status_listeners,
} = self
{
(std::mem::take(seats), status_listeners.clone())
} else {
warn!("Ignoring second zwlr_data_control_manager_v1");
return;
}; };
let mut devices = Vec::new(); pub struct DataControlDeviceManagerState<V = DataControlOfferData> {
manager: ZwlrDataControlManagerV1,
for seat in seats { _phantom: PhantomData<V>,
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(&manager, &seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
} }
*self = Self::Ready { impl DataControlDeviceManagerState {
manager, pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
devices,
status_listeners,
};
}
fn get_manager(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
match self {
Self::Ready { manager, .. } => Some(manager.clone()),
Self::Pending { .. } => None,
}
}
fn new_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready {
manager,
devices,
status_listeners,
} => {
if devices.iter().any(|(s, _)| s == seat) {
// the seat already exists, nothing to do
return;
}
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(manager, seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
}
Self::Pending { seats, .. } => {
seats.push(seat.clone());
}
}
}
fn remove_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready { devices, .. } => devices.retain(|(s, _)| s != seat),
Self::Pending { seats, .. } => seats.retain(|s| s != seat),
}
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where where
F: FnMut(String, WritePipe, DispatchData) + 'static, State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static,
{ {
match self { let manager = globals.bind(qh, 1..=2, GlobalData)?;
Self::Ready { manager, .. } => { debug!("Bound to ZwlDataControlManagerV1 global");
let source = DataControlSource::new(manager, mime_types, callback); Ok(Self {
Some(source) manager,
} _phantom: PhantomData,
Self::Pending { .. } => None,
}
}
fn with_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
match self {
Self::Ready { devices, .. } => {
let device = devices
.iter()
.find_map(|(s, device)| if s == seat { Some(device) } else { None });
device.map_or(Err(MissingGlobal), |device| {
f(device);
Ok(())
}) })
} }
Self::Pending { .. } => Err(MissingGlobal),
}
}
}
pub struct DataControlDeviceHandler { /// creates a data source for copy paste
inner: Rc<RefCell<DataControlDeviceHandlerInner>>, pub fn create_copy_paste_source<'s, D, I>(
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>, &self,
_seat_listener: SeatListener, qh: &QueueHandle<D>,
} mime_types: I,
) -> CopyPasteSource
impl DataControlDeviceHandler {
pub fn init<S>(seat_handler: &mut S) -> Self
where where
S: SeatHandling, D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
I: IntoIterator<Item = &'s str>,
{ {
let status_listeners = Rc::new(RefCell::new(Vec::new())); CopyPasteSource {
inner: self.create_data_control_source(qh, mime_types),
let inner = Rc::new(RefCell::new(DataControlDeviceHandlerInner::Pending {
seats: Vec::new(),
status_listeners: status_listeners.clone(),
}));
let seat_inner = inner.clone();
let seat_listener = seat_handler.listen(move |seat, seat_data, _| {
if seat_data.defunct {
seat_inner.borrow_mut().remove_seat(&seat);
} else {
seat_inner.borrow_mut().new_seat(&seat);
}
});
Self {
inner,
_seat_listener: seat_listener,
status_listeners,
}
}
}
impl GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
fn created(
&mut self,
registry: Attached<WlRegistry>,
id: u32,
version: u32,
_ddata: DispatchData,
) {
// data control manager is supported until version 2
let version = std::cmp::min(version, 2);
let manager = registry.bind::<ZwlrDataControlManagerV1>(version, id);
self.inner.borrow_mut().init_manager((*manager).clone());
}
fn get(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
RefCell::borrow(&self.inner).get_manager()
} }
} }
type DataControlDeviceStatusCallback = /// creates a data source
dyn FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static; 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());
/// Notifies the callbacks of an event on the data device for mime in mime_types {
fn notify_status_listeners( 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, seat: &WlSeat,
event: &DataControlDeviceEvent, data: U,
mut ddata: DispatchData, ) -> ZwlrDataControlDeviceV1
listeners: &RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>, 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>,
) { ) {
listeners.borrow_mut().retain(|lst| { unreachable!()
rc::Weak::upgrade(lst).map_or(false, |cb| {
(cb.borrow_mut())(seat.clone(), event.clone(), ddata.reborrow());
true
})
});
}
pub struct DataControlDeviceStatusListener {
_cb: Rc<RefCell<DataControlDeviceStatusCallback>>,
}
pub trait DataControlDeviceHandling {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice);
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static;
}
impl DataControlDeviceHandling for DataControlDeviceHandler {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
DataControlDeviceStatusListener { _cb: rc }
}
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
RefCell::borrow(&self.inner).with_device(seat, f)
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
RefCell::borrow(&self.inner).create_source(mime_types, callback)
} }
} }
pub fn listen_to_devices<E, F>(env: &Environment<E>, f: F) -> DataControlDeviceStatusListener
where
E: DataControlDeviceHandling,
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
}

View File

@@ -3,28 +3,28 @@ pub mod manager;
pub mod offer; pub mod offer;
pub mod source; pub mod source;
use super::Env; use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
use crate::clients::wayland::DData; use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
use crate::send; use self::source::DataControlSourceHandler;
use color_eyre::Report; use crate::clients::wayland::Environment;
use device::{DataControlDevice, DataControlDeviceEvent}; use crate::{lock, send};
use device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener}; use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
use smithay_client_toolkit::data_device::WritePipe; use nix::sys::epoll::{epoll_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp};
use smithay_client_toolkit::environment::Environment; use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::LoopHandle; use smithay_client_toolkit::reexports::calloop::RegistrationToken;
use smithay_client_toolkit::MissingGlobal; use std::cmp::min;
use source::DataControlSource; use std::fmt::{Debug, Formatter};
use std::fs::File; use std::fs::File;
use std::io; use std::io::{ErrorKind, Read, Write};
use std::io::{Read, Write}; use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::UNIX_EPOCH; use std::{fs, io};
use tokio::sync::broadcast;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::{Connection, QueueHandle};
use wayland_client::DispatchData; use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
static COUNTER: AtomicUsize = AtomicUsize::new(1); static COUNTER: AtomicUsize = AtomicUsize::new(1);
@@ -34,6 +34,11 @@ fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)
} }
pub struct SelectionOfferItem {
offer: SelectionOffer,
token: Option<RegistrationToken>,
}
#[derive(Debug, Clone, Eq)] #[derive(Debug, Clone, Eq)]
pub struct ClipboardItem { pub struct ClipboardItem {
pub id: usize, pub id: usize,
@@ -47,77 +52,27 @@ impl PartialEq<Self> for ClipboardItem {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub enum ClipboardValue { pub enum ClipboardValue {
Text(String), Text(String),
Image(Bytes), Image(Bytes),
Other, Other,
} }
impl DataControlDeviceHandling for Env { impl Debug for ClipboardValue {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
where write!(
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static, f,
{ "{}",
self.data_control_device.listen(f) match self {
Self::Text(text) => text.clone(),
Self::Image(bytes) => {
format!("[{} Bytes]", bytes.len())
} }
Self::Other => "[Unknown]".to_string(),
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
self.data_control_device.with_data_control_device(seat, f)
} }
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
self.data_control_device.create_source(mime_types, callback)
}
}
pub fn copy_to_clipboard<E>(
env: &Environment<E>,
seat: &WlSeat,
item: &ClipboardItem,
) -> Result<(), MissingGlobal>
where
E: DataControlDeviceHandling,
{
debug!("Copying item with id {} [{}]", item.id, item.mime_type);
trace!("Copying: {item:?}");
let item = item.clone();
env.with_inner(|env| {
let mime_types = vec![INTERNAL_MIME_TYPE.to_string(), item.mime_type];
let source = env.create_source(mime_types, move |mime_type, mut pipe, _ddata| {
debug!(
"Triggering source callback for item with id {} [{}]",
item.id, mime_type
);
// FIXME: Not working for large (buffered) values in xwayland
let bytes = match &item.value {
ClipboardValue::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(
io::ErrorKind::Other,
"Attempted to copy unsupported mime type",
) )
),
};
if let Err(err) = pipe.write_all(bytes) {
error!("{err:?}");
} }
});
env.with_data_control_device(seat, |device| device.set_selection(&source))
})
} }
#[derive(Debug)] #[derive(Debug)]
@@ -133,11 +88,8 @@ enum MimeTypeCategory {
} }
impl MimeType { impl MimeType {
fn parse(mime_types: &[String]) -> Option<Self> { fn parse(mime_type: &str) -> Option<Self> {
mime_types match mime_type.to_lowercase().as_str() {
.iter()
.map(|s| s.to_lowercase())
.find_map(|mime_type| match mime_type.as_str() {
"text" "text"
| "string" | "string"
| "utf8_string" | "utf8_string"
@@ -146,90 +98,38 @@ impl MimeType {
| "text/plain;charset=iso-8859-1" | "text/plain;charset=iso-8859-1"
| "text/plain;charset=us-ascii" | "text/plain;charset=us-ascii"
| "text/plain;charset=unicode" => Some(Self { | "text/plain;charset=unicode" => Some(Self {
value: mime_type, value: mime_type.to_string(),
category: MimeTypeCategory::Text, category: MimeTypeCategory::Text,
}), }),
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp" "image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
| "image/x-bmp" | "image/icon" => Some(Self { | "image/x-bmp" | "image/icon" => Some(Self {
value: mime_type, value: mime_type.to_string(),
category: MimeTypeCategory::Image, category: MimeTypeCategory::Image,
}), }),
_ => None, _ => None,
})
} }
} }
pub fn receive_offer( fn parse_multiple(mime_types: &[String]) -> Option<Self> {
event: DataControlDeviceEvent, mime_types.iter().find_map(|mime| Self::parse(mime))
handle: &LoopHandle<DData>,
tx: broadcast::Sender<Arc<ClipboardItem>>,
mut ddata: DispatchData,
) {
let timestamp = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Could not get epoch, system time is probably very wrong")
.as_nanos();
let offer = event.0;
let ddata = ddata
.get::<DData>()
.expect("Expected dispatch data to exist");
let handle2 = handle.clone();
let res = offer.with_mime_types(|mime_types| {
debug!("Offer mime types: {mime_types:?}");
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
debug!("Skipping value provided by bar");
return Ok(());
}
let mime_type = MimeType::parse(mime_types);
debug!("Detected mime type: {mime_type:?}");
match mime_type {
Some(mime_type) => {
debug!("[{timestamp}] Sending clipboard read request ({mime_type:?})");
let read_pipe = offer.receive(mime_type.value.clone())?;
let source = handle.insert_source(read_pipe, move |(), file, ddata| {
debug!(
"[{timestamp}] Reading clipboard contents ({:?})",
&mime_type.category
);
match read_file(&mime_type, file) {
Ok(item) => {
send!(tx, Arc::new(item));
}
Err(err) => error!("{err:?}"),
}
if let Some(src) = ddata.offer_tokens.remove(&timestamp) {
handle2.remove(src);
}
})?;
ddata.offer_tokens.insert(timestamp, source);
}
None => {
// send an event so the clipboard module is aware it's changed
send!(
tx,
Arc::new(ClipboardItem {
id: usize::MAX,
mime_type: String::new(),
value: ClipboardValue::Other
})
);
} }
} }
Ok::<(), Report>(()) impl Environment {
}); pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) {
debug!("Copying item to clipboard: {item:?}");
if let Err(err) = res { // TODO: Proper device tracking
error!("{err:?}"); 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);
} }
} }
@@ -256,3 +156,240 @@ fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem>
mime_type: mime_type.value.clone(), 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().unwrap();
epoll_ctl(
epoll_fd,
EpollOp::EpollCtlAdd,
fd.as_raw_fd(),
&mut epoll_event,
)
.unwrap();
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)
}

View File

@@ -1,63 +1,179 @@
use super::manager::DataControlDeviceManagerState;
use crate::lock; use crate::lock;
use nix::fcntl::OFlag; use nix::fcntl::OFlag;
use nix::unistd::{close, pipe2}; use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device::ReadPipe; use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use std::io; use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut;
use std::os::fd::FromRawFd; use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::warn; use tracing::{debug, warn};
use wayland_client::Main; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1, Event, ZwlrDataControlOfferV1,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Inner { pub struct UndeterminedOffer {
mime_types: Vec<String>, 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)] #[derive(Debug, Clone)]
pub struct DataControlOffer { pub struct SelectionOffer {
inner: Arc<Mutex<Inner>>, pub data_offer: ZwlrDataControlOfferV1,
pub(crate) offer: ZwlrDataControlOfferV1,
} }
impl DataControlOffer { impl PartialEq for SelectionOffer {
pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> Self { fn eq(&self, other: &Self) -> bool {
let inner = Arc::new(Mutex::new(Inner { self.data_offer == other.data_offer
mime_types: Vec::new(),
}));
{
let inner = inner.clone();
offer.quick_assign(move |_, event, _| {
let mut inner = lock!(inner);
if let Event::Offer { mime_type } = event {
inner.mime_types.push(mime_type);
} }
}
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"),
}); });
} }
Self {
offer: offer.detach(),
inner,
} }
} }
pub fn with_mime_types<F, T>(&self, f: F) -> T 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 where
F: FnOnce(&[String]) -> T, D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
U: DataControlOfferDataExt,
{ {
let inner = lock!(self.inner); fn event(
f(&inner.mime_types) 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);
}
}
} }
pub fn receive(&self, mime_type: String) -> io::Result<ReadPipe> { /// 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 // create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?; let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
self.offer.receive(mime_type, writefd); offer.receive(mime_type, writefd);
if let Err(err) = close(writefd) { if let Err(err) = close(writefd) {
warn!("Failed to close write pipe: {}", err); warn!("Failed to close write pipe: {}", err);
@@ -65,10 +181,3 @@ impl DataControlOffer {
Ok(unsafe { FromRawFd::from_raw_fd(readfd) }) Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
} }
}
impl Drop for DataControlOffer {
fn drop(&mut self) {
self.offer.destroy();
}
}

View File

@@ -1,54 +1,101 @@
use smithay_client_toolkit::data_device::WritePipe; use super::device::DataControlDevice;
use std::os::fd::FromRawFd; use super::manager::DataControlDeviceManagerState;
use wayland_client::{Attached, DispatchData}; use smithay_client_toolkit::data_device_manager::WritePipe;
use wayland_protocols::wlr::unstable::data_control::v1::client::{ use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1, use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1}, Event, ZwlrDataControlSourceV1,
}; };
fn data_control_source_impl<F>( #[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, source: &ZwlrDataControlSourceV1,
event: Event, mime: Option<String>,
implem: &mut F, );
ddata: DispatchData,
) where /// The client has requested the data for this source to be sent.
F: FnMut(String, WritePipe, DispatchData), /// 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 { match event {
Event::Send { mime_type, fd } => { Event::Send { mime_type, fd } => {
let pipe = unsafe { FromRawFd::from_raw_fd(fd) }; state.send_request(conn, qh, source, mime_type, fd.into());
implem(mime_type, pipe, ddata); }
Event::Cancelled => {
state.cancelled(conn, qh, source);
}
_ => {}
} }
Event::Cancelled => source.destroy(),
_ => unreachable!(),
} }
} }
pub struct DataControlSource { #[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) source: ZwlrDataControlSourceV1, pub struct CopyPasteSource {
pub(crate) inner: ZwlrDataControlSourceV1,
} }
impl DataControlSource { impl CopyPasteSource {
pub fn new<F>( /// Set the selection of the provided data device as a response to the event with with provided serial.
manager: &Attached<ZwlrDataControlManagerV1>, pub fn set_selection(&self, device: &DataControlDevice) {
mime_types: Vec<String>, device.device.set_selection(Some(&self.inner));
mut callback: F,
) -> Self
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
let source = manager.create_data_source();
source.quick_assign(move |source, evt, ddata| {
data_control_source_impl(&source, evt, &mut callback, ddata);
});
for mime_type in mime_types {
source.offer(mime_type);
} }
Self { pub const fn inner(&self) -> &ZwlrDataControlSourceV1 {
source: source.detach(), &self.inner
} }
} }
impl Drop for CopyPasteSource {
fn drop(&mut self) {
self.inner.destroy();
}
} }

View File

@@ -1,13 +1,15 @@
use super::manager::ToplevelManagerState;
use crate::lock;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use tracing::trace; use tracing::trace;
use wayland_client::{DispatchData, Main}; use wayland_client::protocol::wl_output::WlOutput;
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1}; use wayland_client::protocol::wl_seat::WlSeat;
use crate::write_lock; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
const STATE_ACTIVE: u32 = 2; Event, ZwlrForeignToplevelHandleV1,
const STATE_FULLSCREEN: u32 = 3; };
static COUNTER: AtomicUsize = AtomicUsize::new(1); static COUNTER: AtomicUsize = AtomicUsize::new(1);
@@ -15,69 +17,119 @@ fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)
} }
#[derive(Debug, Clone, Default)] #[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 struct ToplevelInfo {
pub id: usize, pub id: usize,
pub app_id: String, pub app_id: String,
pub title: String, pub title: String,
pub active: bool,
pub fullscreen: bool, pub fullscreen: bool,
pub focused: bool,
ready: bool,
} }
impl ToplevelInfo { impl Default for ToplevelInfo {
fn new() -> Self { fn default() -> Self {
let id = get_id();
Self { Self {
id, id: get_id(),
..Default::default() app_id: String::new(),
title: String::new(),
fullscreen: false,
focused: false,
} }
} }
} }
pub struct Toplevel; pub trait ToplevelHandleDataExt {
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
#[derive(Debug, Clone)]
pub struct ToplevelEvent {
pub toplevel: ToplevelInfo,
pub change: ToplevelChange,
} }
#[derive(Debug, Clone, PartialEq, Eq)] impl ToplevelHandleDataExt for ToplevelHandleData {
pub enum ToplevelChange { fn toplevel_handle_data(&self) -> &ToplevelHandleData {
New, self
Close, }
Title(String),
Focus(bool),
Fullscreen(bool),
} }
fn toplevel_implem<F>(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData) pub trait ToplevelHandleHandler: Sized {
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
}
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
where where
F: FnMut(ToplevelEvent, DispatchData), D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler,
U: ToplevelHandleDataExt,
{ {
trace!("event: {event:?} (info: {info:?})"); fn event(
state: &mut D,
handle: &ZwlrForeignToplevelHandleV1,
event: Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
const STATE_ACTIVE: u32 = 2;
const STATE_FULLSCREEN: u32 = 3;
let change = match event { let data = data.toplevel_handle_data();
Event::AppId { app_id } => {
info.app_id = app_id; trace!("Processing handle event: {event:?}");
None
} match event {
Event::Title { title } => { Event::Title { title } => {
info.title = title.clone(); lock!(data.inner).pending_info.title = title;
if info.ready {
Some(ToplevelChange::Title(title))
} else {
None
}
} }
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
Event::State { state } => { Event::State { state } => {
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32` // 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. // the u32 then represents a value in the `State` enum.
assert_eq!(state.len() % 4, 0); assert_eq!(state.len() % 4, 0);
let state = (0..state.len() / 4) let state = (0..state.len() / 4)
.map(|i| { .map(|i| {
let slice: [u8; 4] = state[i * 4..i * 4 + 4] let slice: [u8; 4] = state[i * 4..i * 4 + 4]
@@ -87,66 +139,46 @@ where
}) })
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let new_active = state.contains(&STATE_ACTIVE); lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE);
let new_fullscreen = state.contains(&STATE_FULLSCREEN); lock!(data.inner).pending_info.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 => { Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
if info.ready { Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
Some(ToplevelChange::Close) Event::Closed => state.remove_handle(
} else { conn,
None qh,
} ToplevelHandle {
} handle: handle.clone(),
Event::OutputEnter { output: _ } },
| Event::OutputLeave { output: _ } ),
| Event::Parent { parent: _ } => None,
Event::Done => { 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())); let pending_info = lock!(data.inner).pending_info.clone();
lock!(data.inner).current_info = Some(pending_info);
}
handle.quick_assign(move |_handle, event, ddata| { if !lock!(data.inner).initial_done {
let mut inner = write_lock!(inner); lock!(data.inner).initial_done = true;
toplevel_implem(event, &mut inner, &mut callback, ddata); state.new_handle(
}); conn,
qh,
Self ToplevelHandle {
handle: handle.clone(),
},
);
} else {
state.update_handle(
conn,
qh,
ToplevelHandle {
handle: handle.clone(),
},
);
}
}
_ => {}
}
trace!("Event processed");
} }
} }

View File

@@ -1,163 +1,86 @@
use super::handle::{Toplevel, ToplevelEvent}; use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler};
use crate::wayland::LazyGlobal; use smithay_client_toolkit::error::GlobalError;
use smithay_client_toolkit::environment::{Environment, GlobalHandler}; use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use std::cell::RefCell; use std::marker::PhantomData;
use std::rc::{self, Rc}; use tracing::{debug, warn};
use tracing::warn; use wayland_client::globals::{BindError, GlobalList};
use wayland_client::protocol::wl_registry::WlRegistry; use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
use wayland_client::{Attached, DispatchData}; use wayland_protocols_wlr::foreign_toplevel::v1::client::{
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
}; };
struct ToplevelHandlerInner { pub struct ToplevelManagerState<V = ToplevelHandleData> {
manager: LazyGlobal<ZwlrForeignToplevelManagerV1>, manager: ZwlrForeignToplevelManagerV1,
registry: Option<Attached<WlRegistry>>, _phantom: PhantomData<V>,
toplevels: Vec<Toplevel>,
} }
impl ToplevelHandlerInner { impl ToplevelManagerState {
const fn new() -> Self { pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError>
let toplevels = vec![]; where
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
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 manager = globals.bind(qh, 1..=3, GlobalData)?;
let status_listeners = self.status_listeners.clone(); debug!("Bound to ZwlForeignToplevelManagerV1 global");
Ok(Self {
manager.quick_assign(move |_, event, _ddata| { manager,
let mut inner = RefCell::borrow_mut(&inner); _phantom: PhantomData,
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 { pub trait ToplevelManagerHandler: Sized {
_cb: Rc<RefCell<ToplevelStatusCallback>>, /// Advertises a new toplevel.
fn toplevel(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
manager: ToplevelManagerState,
);
} }
pub trait ToplevelHandling { impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
Ok(self.manager.clone())
}
}
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
where where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static; D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
} + Dispatch<ZwlrForeignToplevelHandleV1, V>
+ ToplevelManagerHandler
impl ToplevelHandling for ToplevelHandler { + ToplevelHandleHandler
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener + 'static,
where V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{ {
let rc = Rc::new(RefCell::new(f)) as Rc<_>; event_created_child!(D, ZwlrForeignToplevelManagerV1, [
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc)); 0 => (ZwlrForeignToplevelHandleV1, V::default())
ToplevelStatusListener { _cb: rc } ]);
}
}
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener fn event(
where state: &mut D,
E: ToplevelHandling, toplevel_manager: &ZwlrForeignToplevelManagerV1,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, event: Event,
{ _data: &GlobalData,
env.with_inner(move |inner| ToplevelHandling::listen(inner, f)) 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.");
}
_ => {}
}
}
} }

View File

@@ -1,39 +1,84 @@
use std::sync::RwLock;
use indexmap::IndexMap;
use tokio::sync::broadcast::Sender;
use tracing::trace;
use super::Env;
use handle::{ToplevelEvent, ToplevelChange, ToplevelInfo};
use manager::{ToplevelHandling, ToplevelStatusListener};
use wayland_client::DispatchData;
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use crate::{send, write_lock};
pub mod handle; pub mod handle;
pub mod manager; pub mod manager;
impl ToplevelHandling for Env { use self::handle::ToplevelHandleHandler;
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener use self::manager::{ToplevelManagerHandler, ToplevelManagerState};
where use crate::clients::wayland::Environment;
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static, use tracing::{debug, error, trace};
{ use wayland_client::{Connection, QueueHandle};
self.toplevel.listen(f)
} use crate::send;
pub use handle::{ToplevelHandle, ToplevelInfo};
#[derive(Debug, Clone)]
pub enum ToplevelEvent {
New(ToplevelHandle),
Update(ToplevelHandle),
Remove(ToplevelHandle),
} }
pub fn update_toplevels( impl ToplevelManagerHandler for Environment {
toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>, fn toplevel(
handle: ZwlrForeignToplevelHandleV1, &mut self,
event: ToplevelEvent, _conn: &Connection,
tx: &Sender<ToplevelEvent>, _qh: &QueueHandle<Self>,
_manager: ToplevelManagerState,
) { ) {
trace!("Received toplevel event: {:?}", event); debug!("Manager received new handle");
}
if event.change == ToplevelChange::Close {
write_lock!(toplevels).remove(&event.toplevel.id);
} else {
write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
} }
send!(tx, event); 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!");
}
}
}
} }

163
src/config/common.rs Normal file
View File

@@ -0,0 +1,163 @@
use crate::dynamic_string::DynamicString;
use crate::script::{Script, ScriptInput};
use crate::send;
use gtk::gdk::ScrollDirection;
use gtk::prelude::*;
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
use serde::Deserialize;
use tokio::spawn;
use tracing::trace;
/// Common configuration options
/// 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<ScriptInput>,
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();
DynamicString::new(&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 script = Script::new_polling(show_if);
let container = container.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
script
.run(None, |_, success| {
send!(tx, success);
})
.await;
});
{
let revealer = revealer.clone();
let container = container.clone();
rx.attach(None, move |success| {
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();
}
});
},
);
}
}

View File

@@ -1,3 +1,4 @@
mod common;
mod r#impl; mod r#impl;
mod truncate; mod truncate;
@@ -7,6 +8,7 @@ use crate::modules::clipboard::ClipboardModule;
use crate::modules::clock::ClockModule; use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule; use crate::modules::custom::CustomModule;
use crate::modules::focused::FocusedModule; use crate::modules::focused::FocusedModule;
use crate::modules::label::LabelModule;
use crate::modules::launcher::LauncherModule; use crate::modules::launcher::LauncherModule;
#[cfg(feature = "music")] #[cfg(feature = "music")]
use crate::modules::music::MusicModule; use crate::modules::music::MusicModule;
@@ -15,27 +17,16 @@ use crate::modules::script::ScriptModule;
use crate::modules::sysinfo::SysInfoModule; use crate::modules::sysinfo::SysInfoModule;
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
use crate::modules::tray::TrayModule; use crate::modules::tray::TrayModule;
#[cfg(feature = "upower")]
use crate::modules::upower::UpowerModule;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule; use crate::modules::workspaces::WorkspacesModule;
use crate::script::ScriptInput;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType};
pub use self::truncate::{EllipsizeMode, TruncateMode}; pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)]
pub struct CommonConfig {
pub show_if: Option<ScriptInput>,
pub on_click_left: Option<ScriptInput>,
pub on_click_right: Option<ScriptInput>,
pub on_click_middle: Option<ScriptInput>,
pub on_scroll_up: Option<ScriptInput>,
pub on_scroll_down: Option<ScriptInput>,
pub tooltip: Option<String>,
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig { pub enum ModuleConfig {
@@ -45,6 +36,7 @@ pub enum ModuleConfig {
Clock(Box<ClockModule>), Clock(Box<ClockModule>),
Custom(Box<CustomModule>), Custom(Box<CustomModule>),
Focused(Box<FocusedModule>), Focused(Box<FocusedModule>),
Label(Box<LabelModule>),
Launcher(Box<LauncherModule>), Launcher(Box<LauncherModule>),
#[cfg(feature = "music")] #[cfg(feature = "music")]
Music(Box<MusicModule>), Music(Box<MusicModule>),
@@ -53,6 +45,8 @@ pub enum ModuleConfig {
SysInfo(Box<SysInfoModule>), SysInfo(Box<SysInfoModule>),
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
Tray(Box<TrayModule>), Tray(Box<TrayModule>),
#[cfg(feature = "upower")]
Upower(Box<UpowerModule>),
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
Workspaces(Box<WorkspacesModule>), Workspaces(Box<WorkspacesModule>),
} }
@@ -100,6 +94,8 @@ pub struct Config {
pub height: i32, pub height: i32,
#[serde(default)] #[serde(default)]
pub margin: MarginConfig, pub margin: MarginConfig,
#[serde(default = "default_popup_gap")]
pub popup_gap: i32,
/// GTK icon theme to use. /// GTK icon theme to use.
pub icon_theme: Option<String>, pub icon_theme: Option<String>,
@@ -115,6 +111,10 @@ const fn default_bar_height() -> i32 {
42 42
} }
const fn default_popup_gap() -> i32 {
5
}
pub const fn default_false() -> bool { pub const fn default_false() -> bool {
false false
} }

View File

@@ -29,13 +29,12 @@ pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
for dir in dirs { for dir in dirs {
let mut walker = WalkDir::new(dir).max_depth(5).into_iter(); let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
let entry = walker.find(|entry| match entry { let entry = walker.find(|entry| {
Ok(entry) => { entry.as_ref().map_or(false, |entry| {
let file_name = entry.file_name().to_string_lossy().to_lowercase(); let file_name = entry.file_name().to_string_lossy().to_lowercase();
let test_name = format!("{}.desktop", app_id.to_lowercase()); let test_name = format!("{}.desktop", app_id.to_lowercase());
file_name == test_name file_name == test_name
} })
_ => false,
}); });
if let Some(Ok(entry)) = entry { if let Some(Ok(entry)) = entry {

View File

@@ -4,60 +4,35 @@ use gtk::prelude::*;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
/// A segment of a dynamic string,
/// containing either a static string
/// or a script.
#[derive(Debug)] #[derive(Debug)]
enum DynamicStringSegment { enum DynamicStringSegment {
Static(String), Static(String),
Dynamic(Script), Dynamic(Script),
} }
/// A string with embedded scripts for dynamic content.
pub struct DynamicString; pub struct DynamicString;
impl DynamicString { impl DynamicString {
/// Creates a new dynamic string, based off the input template.
/// Runs `f` with the compiled string each time one of the scripts updates.
///
/// # Example
///
/// ```rs
/// DynamicString::new(&text, move |string| {
/// label.set_markup(&string);
/// Continue(true)
/// });
/// ```
pub fn new<F>(input: &str, f: F) -> Self pub fn new<F>(input: &str, f: F) -> Self
where where
F: FnMut(String) -> Continue + 'static, F: FnMut(String) -> Continue + 'static,
{ {
let mut segments = vec![]; let segments = Self::parse_input(input);
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char = &chars[..=1];
let (token, skip) = if let ['{', '{'] = char {
const SKIP_BRACKETS: usize = 4;
let str = chars
.iter()
.skip(2)
.enumerate()
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
len + SKIP_BRACKETS,
)
} else {
let str = chars
.iter()
.enumerate()
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(DynamicStringSegment::Static(str), len)
};
assert_ne!(skip, 0);
segments.push(token);
chars.drain(..skip);
}
let label_parts = Arc::new(Mutex::new(Vec::new())); let label_parts = Arc::new(Mutex::new(Vec::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@@ -76,11 +51,11 @@ impl DynamicString {
spawn(async move { spawn(async move {
script script
.run(|(out, _)| { .run(None, |out, _| {
if let OutputStream::Stdout(out) = out { if let OutputStream::Stdout(out) = out {
let mut label_parts = lock!(label_parts); let mut label_parts = lock!(label_parts);
let _ = std::mem::replace(&mut label_parts[i], out); let _: String = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join(""); let string = label_parts.join("");
send!(tx, string); send!(tx, string);
@@ -102,6 +77,66 @@ impl DynamicString {
Self Self
} }
/// Parses the input string into static and dynamic segments
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
if !input.contains("{{") {
return vec![DynamicStringSegment::Static(input.to_string())];
}
let mut segments = vec![];
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char_pair = if chars.len() > 1 {
Some(&chars[..=1])
} else {
None
};
let (token, skip) = if let Some(['{', '{']) = char_pair {
const SKIP_BRACKETS: usize = 4; // two braces either side
let str = chars
.windows(2)
.skip(2)
.take_while(|win| win != &['}', '}'])
.map(|w| w[0])
.collect::<String>();
let len = str.len();
(
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
len + SKIP_BRACKETS,
)
} else {
let mut str = chars
.windows(2)
.take_while(|win| win != &['{', '{'])
.map(|w| w[0])
.collect::<String>();
// if segment is at end of string, last char gets missed above due to uneven window.
if chars.len() == str.len() + 1 {
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
str.push(remaining_char);
}
let len = str.len();
(DynamicStringSegment::Static(str), len)
};
// quick runtime check to make sure the parser is working as expected
assert_ne!(skip, 0);
segments.push(token);
chars.drain(..skip);
}
segments
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -5,9 +5,11 @@ pub enum ExitCode {
Config = 3, 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_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
pub const ERR_READ_LOCK: &str = "Failed to get read lock"; 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_WRITE_LOCK: &str = "Failed to get write lock";
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel"; 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_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
View 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);
}

View File

@@ -1,4 +1,5 @@
use super::ImageProvider; use super::ImageProvider;
use crate::gtk_helpers::add_class;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation}; use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error; use tracing::error;
@@ -9,7 +10,7 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
image.set_widget_name("image"); add_class(&image, "image");
match ImageProvider::parse(input, icon_theme, size) match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone())) .and_then(|provider| provider.load_into_image(image.clone()))
@@ -36,7 +37,7 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
image.set_widget_name("image"); add_class(&image, "image");
container.add(&image); container.add(&image);
@@ -47,7 +48,7 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
} }
} else { } else {
let label = Label::new(Some(input)); let label = Label::new(Some(input));
label.set_widget_name("label"); add_class(&label, "label");
container.add(&label); container.add(&label);
} }

View File

@@ -143,7 +143,9 @@ impl<'a> ImageProvider<'a> {
fn load_into_image_sync(&self, image: &gtk::Image) -> Result<()> { fn load_into_image_sync(&self, image: &gtk::Image) -> Result<()> {
let pixbuf = match &self.location { let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme), ImageLocation::Icon { name, theme } => {
self.get_from_icon(name, theme, image.scale_factor())
}
ImageLocation::Local(path) => self.get_from_file(path), ImageLocation::Local(path) => self.get_from_file(path),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id), ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
#[cfg(feature = "http")] #[cfg(feature = "http")]
@@ -156,8 +158,9 @@ impl<'a> ImageProvider<'a> {
} }
/// Attempts to get a `Pixbuf` from the GTK icon theme. /// Attempts to get a `Pixbuf` from the GTK icon theme.
fn get_from_icon(&self, name: &str, theme: &IconTheme) -> Result<Pixbuf> { fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
let pixbuf = match theme.lookup_icon(name, self.size, IconLookupFlags::empty()) { let pixbuf =
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE), Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
None => Ok(None), None => Ok(None),
}?; }?;
@@ -193,7 +196,16 @@ impl<'a> ImageProvider<'a> {
/// Attempts to get `Bytes` from an HTTP resource asynchronously. /// Attempts to get `Bytes` from an HTTP resource asynchronously.
#[cfg(feature = "http")] #[cfg(feature = "http")]
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> { async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
let bytes = reqwest::get(url).await?.bytes().await?; 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)) Ok(glib::Bytes::from_owned(bytes))
} else {
Err(Report::msg(format!(
"Received non-success HTTP code ({status})"
)))
}
} }
} }

View File

@@ -58,7 +58,7 @@ fn install_tracing() -> Result<WorkerGuard> {
const DEFAULT_LOG: &str = "info"; const DEFAULT_LOG: &str = "info";
const DEFAULT_FILE_LOG: &str = "warn"; 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 = let filter_layer =
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?; EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;

View File

@@ -53,9 +53,10 @@ macro_rules! try_send {
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! lock { macro_rules! lock {
($mutex:expr) => { ($mutex:expr) => {{
tracing::trace!("Locking {}", std::stringify!($mutex));
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK) $mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
}; }};
} }
/// Gets a read lock on a `RwLock`. /// Gets a read lock on a `RwLock`.

View File

@@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
mod bar; mod bar;
mod bridge_channel; mod bridge_channel;
mod clients; mod clients;
@@ -5,6 +7,7 @@ mod config;
mod desktop_file; mod desktop_file;
mod dynamic_string; mod dynamic_string;
mod error; mod error;
mod gtk_helpers;
mod image; mod image;
mod logging; mod logging;
mod macros; mod macros;
@@ -22,10 +25,12 @@ use dirs::config_dir;
use gtk::gdk::Display; use gtk::gdk::Display;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Application; use gtk::Application;
use std::cell::Cell;
use std::env; use std::env;
use std::future::Future; use std::future::Future;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::rc::Rc;
use tokio::runtime::Handle; use tokio::runtime::Handle;
use tokio::task::block_in_place; use tokio::task::block_in_place;
@@ -38,19 +43,26 @@ const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() {
let _guard = logging::install_logging(); let _guard = logging::install_logging();
info!("Ironbar version {}", VERSION); info!("Ironbar version {}", VERSION);
info!("Starting application"); info!("Starting application");
clients::volume::pulse_bak::test();
let wayland_client = wayland::get_client().await; let wayland_client = wayland::get_client().await;
let app = Application::builder().application_id(GTK_APP_ID).build(); let app = Application::builder().application_id(GTK_APP_ID).build();
let running = Rc::new(Cell::new(false));
app.connect_activate(move |app| { app.connect_activate(move |app| {
if running.get() {
info!("Ironbar already running, returning");
return;
}
running.set(true);
let display = Display::default().map_or_else( let display = Display::default().map_or_else(
|| { || {
let report = Report::msg("Failed to get default GTK display"); let report = Report::msg("Failed to get default GTK display");
@@ -60,10 +72,10 @@ async fn main() -> Result<()> {
|display| display, |display| display,
); );
let config_res = match env::var("IRONBAR_CONFIG") { let config_res = env::var("IRONBAR_CONFIG").map_or_else(
Ok(path) => ConfigLoader::load(path), |_| ConfigLoader::new("ironbar").find_and_load(),
Err(_) => ConfigLoader::new("ironbar").find_and_load(), ConfigLoader::load,
}; );
let config = match config_res { let config = match config_res {
Ok(config) => config, Ok(config) => config,
@@ -105,7 +117,8 @@ async fn main() -> Result<()> {
// Some are provided by swaybar_config but not currently supported // Some are provided by swaybar_config but not currently supported
app.run_with_args(&Vec::<&str>::new()); app.run_with_args(&Vec::<&str>::new());
Ok(()) info!("Shutting down");
exit(0);
} }
/// Creates each of the bars across each of the (configured) outputs. /// Creates each of the bars across each of the (configured) outputs.
@@ -115,7 +128,7 @@ fn create_bars(
wl: &WaylandClient, wl: &WaylandClient,
config: &Config, config: &Config,
) -> Result<()> { ) -> Result<()> {
let outputs = wl.outputs.as_slice(); let outputs = wl.get_outputs();
debug!("Received {} outputs from Wayland", outputs.len()); debug!("Received {} outputs from Wayland", outputs.len());
debug!("Outputs: {:?}", outputs); debug!("Outputs: {:?}", outputs);
@@ -129,7 +142,8 @@ fn create_bars(
let output = outputs let output = outputs
.get(i as usize) .get(i as usize)
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?; .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( config.monitors.as_ref().map_or_else(
|| { || {

View File

@@ -21,6 +21,9 @@ pub struct ClipboardModule {
#[serde(default = "default_icon")] #[serde(default = "default_icon")]
icon: String, icon: String,
#[serde(default = "default_icon_size")]
icon_size: i32,
#[serde(default = "default_max_items")] #[serde(default = "default_max_items")]
max_items: usize, max_items: usize,
@@ -35,6 +38,10 @@ fn default_icon() -> String {
String::from("󰨸") String::from("󰨸")
} }
const fn default_icon_size() -> i32 {
32
}
const fn default_max_items() -> usize { const fn default_max_items() -> usize {
10 10
} }
@@ -73,7 +80,7 @@ impl Module<Button> for ClipboardModule {
spawn(async move { spawn(async move {
let mut rx = { let mut rx = {
let client = clipboard::get_client(); let client = clipboard::get_client();
client.subscribe(max_items).await client.subscribe(max_items)
}; };
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
@@ -120,11 +127,11 @@ impl Module<Button> for ClipboardModule {
) -> color_eyre::Result<ModuleWidget<Button>> { ) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position; let position = info.bar_position;
let button = new_icon_button(&self.icon, info.icon_theme, 32); let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
button.style_context().add_class("btn"); button.style_context().add_class("btn");
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
let pos = Popup::button_pos(button, position.get_orientation()); let pos = Popup::widget_geometry(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos)); try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
}); });
@@ -147,11 +154,7 @@ impl Module<Button> for ClipboardModule {
where where
Self: Sized, Self: Sized,
{ {
let container = gtk::Box::builder() let container = gtk::Box::new(Orientation::Vertical, 10);
.orientation(Orientation::Vertical)
.spacing(10)
.name("popup-clipboard")
.build();
let entries = gtk::Box::new(Orientation::Vertical, 5); let entries = gtk::Box::new(Orientation::Vertical, 5);
container.add(&entries); container.add(&entries);

View File

@@ -1,4 +1,5 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{send_async, try_send}; use crate::{send_async, try_send};
@@ -69,7 +70,7 @@ impl Module<Button> for ClockModule {
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!( try_send!(
context.tx, context.tx,
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation)) ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
); );
}); });
@@ -96,20 +97,16 @@ impl Module<Button> for ClockModule {
rx: glib::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let container = gtk::Box::builder() let container = gtk::Box::new(Orientation::Vertical, 0);
.orientation(Orientation::Vertical)
.name("popup-clock")
.build();
let clock = Label::builder() let clock = Label::builder().halign(Align::Center).build();
.name("calendar-clock") add_class(&clock, "calendar-clock");
.halign(Align::Center)
.build();
let format = "%H:%M:%S"; let format = "%H:%M:%S";
container.add(&clock); container.add(&clock);
let calendar = Calendar::builder().name("calendar").build(); let calendar = Calendar::new();
add_class(&calendar, "calendar");
container.add(&calendar); container.add(&calendar);
{ {

View File

@@ -1,309 +0,0 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::{ButtonGeometry, Popup};
use crate::script::Script;
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct CustomModule {
/// Container class name
class: Option<String>,
/// Widgets to add to the bar container
bar: Vec<Widget>,
/// Widgets to add to the popup container
popup: Option<Vec<Widget>>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
/// Attempts to parse an `Orientation` from `String`
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
}
}
/// Widget attributes
#[derive(Debug, Deserialize, Clone)]
pub struct Widget {
/// Type of GTK widget to add
#[serde(rename = "type")]
widget_type: WidgetType,
widgets: Option<Vec<Widget>>,
label: Option<String>,
name: Option<String>,
class: Option<String>,
on_click: Option<String>,
orientation: Option<String>,
src: Option<String>,
size: Option<i32>,
}
/// Supported GTK widget types
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum WidgetType {
Box,
Label,
Button,
Image,
}
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(
self,
parent: &gtk::Box,
tx: Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) {
match self.widget_type {
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)),
WidgetType::Label => parent.add(&self.into_label()),
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
WidgetType::Image => parent.add(&self.into_image(icon_theme)),
}
}
/// Creates a `gtk::Box` from this widget
fn into_box(
self,
tx: &Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) -> gtk::Box {
let mut builder = gtk::Box::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
if let Some(orientation) = self.orientation {
builder = builder
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
}
let container = builder.build();
if let Some(class) = self.class {
container.style_context().add_class(&class);
}
if let Some(widgets) = self.widgets {
for widget in widgets {
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
}
}
container
}
/// Creates a `gtk::Label` from this widget
fn into_label(self) -> Label {
let mut builder = Label::builder().use_markup(true);
if let Some(name) = self.name {
builder = builder.name(name);
}
let label = builder.build();
if let Some(class) = self.class {
label.style_context().add_class(&class);
}
let text = self.label.map_or_else(String::new, |text| text);
{
let label = label.clone();
DynamicString::new(&text, move |string| {
label.set_label(&string);
Continue(true)
});
}
label
}
/// Creates a `gtk::Button` from this widget
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
let mut builder = Button::builder();
if let Some(name) = self.name {
builder = builder.name(name);
}
let button = builder.build();
if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
label.set_markup(&text);
button.add(&label);
}
if let Some(class) = self.class {
button.style_context().add_class(&class);
}
if let Some(exec) = self.on_click {
button.connect_clicked(move |button| {
try_send!(
tx,
ExecEvent {
cmd: exec.clone(),
geometry: Popup::button_pos(button, bar_orientation),
}
);
});
}
button
}
fn into_image(self, icon_theme: &IconTheme) -> gtk::Image {
let mut builder = gtk::Image::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
let gtk_image = builder.build();
if let Some(src) = self.src {
let size = self.size.unwrap_or(32);
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
.and_then(|image| image.load_into_image(gtk_image.clone()))
{
error!("{err:?}");
}
}
if let Some(class) = self.class {
gtk_image.style_context().add_class(&class);
}
gtk_image
}
}
#[derive(Debug)]
pub struct ExecEvent {
cmd: String,
geometry: ButtonGeometry,
}
impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn name() -> &'static str {
"custom"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
spawn(async move {
while let Some(event) = rx.recv().await {
if event.cmd.starts_with('!') {
let script = Script::from(&event.cmd[1..]);
debug!("executing command: '{}'", script.cmd);
// TODO: Migrate to use script.run
if let Err(err) = script.get_output().await {
error!("{err:?}");
}
} else if event.cmd == "popup:toggle" {
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
} else if event.cmd == "popup:open" {
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
} else if event.cmd == "popup:close" {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
} else {
error!("Received invalid command: '{}'", event.cmd);
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build();
if let Some(ref class) = self.class {
container.style_context().add_class(class);
}
self.bar.clone().into_iter().for_each(|widget| {
widget.add_to(
&container,
context.controller_tx.clone(),
orientation,
info.icon_theme,
);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
popup,
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::builder().name("popup-custom").build();
if let Some(class) = self.class {
container
.style_context()
.add_class(format!("popup-{class}").as_str());
}
if let Some(popup) = self.popup {
for widget in popup {
widget.add_to(
&container,
tx.clone(),
Orientation::Horizontal,
info.icon_theme,
);
}
}
container.show_all();
Some(container)
}
}

36
src/modules/custom/box.rs Normal file
View File

@@ -0,0 +1,36 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use crate::build;
use crate::modules::custom::WidgetConfig;
use gtk::prelude::*;
use gtk::Orientation;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct BoxWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
widgets: Option<Vec<WidgetConfig>>,
}
impl CustomWidget for BoxWidget {
type Widget = gtk::Box;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let container = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
container.set_orientation(
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
);
}
if let Some(widgets) = self.widgets {
for widget in widgets {
widget.widget.add_to(&container, context, widget.common);
}
}
container
}
}

View File

@@ -0,0 +1,52 @@
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use crate::dynamic_string::DynamicString;
use crate::popup::Popup;
use crate::{build, try_send};
use gtk::prelude::*;
use gtk::{Button, Label};
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
name: Option<String>,
class: Option<String>,
label: Option<String>,
on_click: Option<String>,
}
impl CustomWidget for ButtonWidget {
type Widget = Button;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let button = build!(self, Self::Widget);
if let Some(text) = self.label {
let label = Label::new(None);
label.set_use_markup(true);
button.add(&label);
DynamicString::new(&text, move |string| {
label.set_markup(&string);
Continue(true)
});
}
if let Some(exec) = self.on_click {
let bar_orientation = context.bar_orientation;
let tx = context.tx.clone();
button.connect_clicked(move |button| {
try_send!(
tx,
ExecEvent {
cmd: exec.clone(),
args: None,
geometry: Popup::widget_geometry(button, bar_orientation),
}
);
});
}
button
}
}

View File

@@ -0,0 +1,47 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_string::DynamicString;
use crate::image::ImageProvider;
use gtk::prelude::*;
use gtk::Image;
use serde::Deserialize;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct ImageWidget {
name: Option<String>,
class: Option<String>,
src: String,
#[serde(default = "default_size")]
size: i32,
}
const fn default_size() -> i32 {
32
}
impl CustomWidget for ImageWidget {
type Widget = Image;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let gtk_image = build!(self, Self::Widget);
{
let gtk_image = gtk_image.clone();
let icon_theme = context.icon_theme.clone();
DynamicString::new(&self.src, move |src| {
let res = ImageProvider::parse(&src, &icon_theme, self.size)
.and_then(|image| image.load_into_image(gtk_image.clone()));
if let Err(err) = res {
error!("{err:?}");
}
Continue(true)
});
}
gtk_image
}
}

View File

@@ -0,0 +1,33 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_string::DynamicString;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget {
name: Option<String>,
class: Option<String>,
label: String,
}
impl CustomWidget for LabelWidget {
type Widget = Label;
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
let label = build!(self, Self::Widget);
label.set_use_markup(true);
{
let label = label.clone();
DynamicString::new(&self.label, move |string| {
label.set_markup(&string);
Continue(true)
});
}
label
}
}

247
src/modules/custom/mod.rs Normal file
View File

@@ -0,0 +1,247 @@
mod r#box;
mod button;
mod image;
mod label;
mod progress;
mod slider;
use self::image::ImageWidget;
use self::label::LabelWidget;
use self::r#box::BoxWidget;
use self::slider::SliderWidget;
use crate::config::CommonConfig;
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
};
use crate::popup::WidgetGeometry;
use crate::script::Script;
use crate::send_async;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct CustomModule {
/// Widgets to add to the bar container
bar: Vec<WidgetConfig>,
/// Widgets to add to the popup container
popup: Option<Vec<WidgetConfig>>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct WidgetConfig {
#[serde(flatten)]
widget: Widget,
#[serde(flatten)]
common: CommonConfig,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Widget {
Box(BoxWidget),
Label(LabelWidget),
Button(ButtonWidget),
Image(ImageWidget),
Slider(SliderWidget),
Progress(ProgressWidget),
}
#[derive(Clone, Copy)]
struct CustomWidgetContext<'a> {
tx: &'a Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
}
trait CustomWidget {
type Widget;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget;
}
/// Creates a new widget of type `ty`,
/// setting its name and class based on
/// the values available on `self`.
#[macro_export]
macro_rules! build {
($self:ident, $ty:ty) => {{
let mut builder = <$ty>::builder();
if let Some(name) = &$self.name {
builder = builder.name(name);
}
let widget = builder.build();
if let Some(class) = &$self.class {
widget.style_context().add_class(class);
}
widget
}};
}
/// Sets the widget length,
/// using either a width or height request
/// based on the bar's orientation.
pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orientation) {
match bar_orientation {
Orientation::Horizontal => widget.set_width_request(length),
Orientation::Vertical => widget.set_height_request(length),
_ => {}
};
}
/// Attempts to parse an `Orientation` from `String`.
/// Will accept `horizontal`, `vertical`, `h` or `v`.
/// Ignores case.
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
match orientation.to_lowercase().as_str() {
"horizontal" | "h" => Ok(Orientation::Horizontal),
"vertical" | "v" => Ok(Orientation::Vertical),
_ => Err(Report::msg("Invalid orientation string in config")),
}
}
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, context: CustomWidgetContext, common: CommonConfig) {
macro_rules! create {
($widget:expr) => {
wrap_widget(
&$widget.into_widget(context),
common,
context.bar_orientation,
)
};
}
let event_box = match self {
Self::Box(widget) => create!(widget),
Self::Label(widget) => create!(widget),
Self::Button(widget) => create!(widget),
Self::Image(widget) => create!(widget),
Self::Slider(widget) => create!(widget),
Self::Progress(widget) => create!(widget),
};
parent.add(&event_box);
}
}
#[derive(Debug)]
pub struct ExecEvent {
cmd: String,
args: Option<Vec<String>>,
geometry: WidgetGeometry,
}
impl Module<gtk::Box> for CustomModule {
type SendMessage = ();
type ReceiveMessage = ExecEvent;
fn name() -> &'static str {
"custom"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
spawn(async move {
while let Some(event) = rx.recv().await {
if event.cmd.starts_with('!') {
let script = Script::from(&event.cmd[1..]);
debug!("executing command: '{}'", script.cmd);
let args = event.args.unwrap_or_default();
if let Err(err) = script.get_output(Some(&args)).await {
error!("{err:?}");
}
} else if event.cmd == "popup:toggle" {
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.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();
let custom_context = CustomWidgetContext {
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
};
self.bar.clone().into_iter().for_each(|widget| {
widget
.widget
.add_to(&container, custom_context, widget.common);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
popup,
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::new(Orientation::Horizontal, 0);
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
tx: &tx,
bar_orientation: info.bar_position.get_orientation(),
icon_theme: info.icon_theme,
};
for widget in popup {
widget
.widget
.add_to(&container, custom_context, widget.common);
}
}
container.show_all();
Some(container)
}
}

View File

@@ -0,0 +1,80 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use crate::dynamic_string::DynamicString;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use gtk::prelude::*;
use gtk::ProgressBar;
use serde::Deserialize;
use tokio::spawn;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
label: Option<String>,
value: Option<ScriptInput>,
#[serde(default = "default_max")]
max: f64,
length: Option<i32>,
}
const fn default_max() -> f64 {
100.0
}
impl CustomWidget for ProgressWidget {
type Widget = ProgressBar;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let progress = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
progress.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
if let Some(length) = self.length {
set_length(&progress, length, context.bar_orientation);
}
if let Some(value) = self.value {
let script = Script::from(value);
let progress = progress.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
script
.run(None, move |stream, _success| match stream {
OutputStream::Stdout(out) => match out.parse::<f64>() {
Ok(value) => send!(tx, value),
Err(err) => error!("{err:?}"),
},
OutputStream::Stderr(err) => error!("{err:?}"),
})
.await;
});
rx.attach(None, move |value| {
progress.set_fraction(value / self.max);
Continue(true)
});
}
if let Some(text) = self.label {
let progress = progress.clone();
progress.set_show_text(true);
DynamicString::new(&text, move |string| {
progress.set_text(Some(&string));
Continue(true)
});
}
progress
}
}

View File

@@ -0,0 +1,128 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use crate::modules::custom::set_length;
use crate::popup::Popup;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use gtk::prelude::*;
use gtk::Scale;
use serde::Deserialize;
use std::cell::Cell;
use std::ops::Neg;
use tokio::spawn;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
name: Option<String>,
class: Option<String>,
orientation: Option<String>,
value: Option<ScriptInput>,
on_change: Option<String>,
#[serde(default = "default_min")]
min: f64,
#[serde(default = "default_max")]
max: f64,
step: Option<f64>,
length: Option<i32>,
#[serde(default = "crate::config::default_true")]
show_label: bool,
}
const fn default_min() -> f64 {
0.0
}
const fn default_max() -> f64 {
100.0
}
impl CustomWidget for SliderWidget {
type Widget = Scale;
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let scale = build!(self, Self::Widget);
if let Some(orientation) = self.orientation {
scale.set_orientation(
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
);
}
if let Some(length) = self.length {
set_length(&scale, length, context.bar_orientation);
}
scale.set_range(self.min, self.max);
scale.set_draw_value(self.show_label);
if let Some(on_change) = self.on_change {
let min = self.min;
let max = self.max;
let step = self.step;
let tx = context.tx.clone();
// GTK will spam the same value over and over
let prev_value = Cell::new(scale.value());
scale.connect_scroll_event(move |scale, event| {
let value = scale.value();
let delta = event.delta().1.neg();
let delta = match (step, delta.is_sign_positive()) {
(Some(step), true) => step,
(Some(step), false) => -step,
(None, _) => delta,
};
scale.set_value(value + delta);
Inhibit(false)
});
scale.connect_change_value(move |scale, _, val| {
// GTK will send values outside min/max range
let val = val.clamp(min, max);
if val != prev_value.get() {
try_send!(
tx,
ExecEvent {
cmd: on_change.clone(),
args: Some(vec![val.to_string()]),
geometry: Popup::widget_geometry(scale, context.bar_orientation),
}
);
prev_value.set(val);
}
Inhibit(false)
});
}
if let Some(value) = self.value {
let script = Script::from(value);
let scale = scale.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
script
.run(None, move |stream, _success| match stream {
OutputStream::Stdout(out) => match out.parse() {
Ok(value) => send!(tx, value),
Err(err) => error!("{err:?}"),
},
OutputStream::Stderr(err) => error!("{err:?}"),
})
.await;
});
rx.attach(None, move |value| {
scale.set_value(value);
Continue(true)
});
}
scale
}
}

View File

@@ -1,8 +1,9 @@
use crate::clients::wayland::{self, ToplevelChange}; use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, read_lock, send_async}; use crate::{send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
@@ -10,7 +11,7 @@ use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error; use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule { pub struct FocusedModule {
@@ -49,40 +50,38 @@ impl Module<gtk::Box> for FocusedModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: Receiver<Self::ReceiveMessage>, _rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let focused = await_sync(async {
let wl = wayland::get_client().await;
let toplevels = read_lock!(wl.toplevels);
toplevels
.iter()
.find(|(_, (top, _))| top.active)
.map(|(_, (top, _))| top.clone())
});
if let Some(top) = focused {
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
}
spawn(async move { spawn(async move {
let mut wlrx = { let (mut wlrx, handles) = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_toplevels() wl.subscribe_toplevels()
}; };
while let Ok(event) = wlrx.recv().await { let focused = handles.values().find_map(|handle| {
let update = match event.change { handle
ToplevelChange::Focus(focus) => focus, .info()
ToplevelChange::Title(_) => event.toplevel.active, .and_then(|info| if info.focused { Some(info) } else { None })
_ => false, });
if let Some(focused) = focused {
try_send!(
tx,
ModuleUpdateEvent::Update((focused.title.clone(), focused.app_id))
);
}; };
if update { while let Ok(event) = wlrx.recv().await {
if let ToplevelEvent::Update(handle) = event {
let info = handle.info().unwrap_or_default();
if info.focused {
debug!("Changing focus");
send_async!( send_async!(
tx, tx,
ModuleUpdateEvent::Update((event.toplevel.title, event.toplevel.app_id)) ModuleUpdateEvent::Update((info.title.clone(), info.app_id.clone()))
); );
} }
} }
}
}); });
Ok(()) Ok(())
@@ -97,8 +96,11 @@ impl Module<gtk::Box> for FocusedModule {
let container = gtk::Box::new(info.bar_position.get_orientation(), 5); let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::builder().name("icon").build(); let icon = gtk::Image::new();
let label = Label::builder().name("label").build(); add_class(&icon, "icon");
let label = Label::new(None);
add_class(&label, "label");
if let Some(truncate) = self.truncate { if let Some(truncate) = self.truncate {
truncate.truncate_label(&label); truncate.truncate_label(&label);

62
src/modules/label.rs Normal file
View File

@@ -0,0 +1,62 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::try_send;
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use tokio::sync::mpsc;
#[derive(Debug, Deserialize, Clone)]
pub struct LabelModule {
label: String,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
impl Module<Label> for LabelModule {
type SendMessage = String;
type ReceiveMessage = ();
fn name() -> &'static str {
"label"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
DynamicString::new(&self.label, move |string| {
try_send!(tx, ModuleUpdateEvent::Update(string));
Continue(true)
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleWidget<Label>> {
let label = Label::new(None);
{
let label = label.clone();
context.widget_rx.attach(None, move |string| {
label.set_label(&string);
Continue(true)
});
}
Ok(ModuleWidget {
widget: label,
popup: None,
})
}
}

View File

@@ -1,10 +1,11 @@
use super::open_state::OpenState; use super::open_state::OpenState;
use crate::clients::wayland::ToplevelInfo; use crate::clients::wayland::ToplevelHandle;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent; use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup; use crate::popup::Popup;
use crate::{read_lock, try_send}; use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation}; use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
@@ -12,6 +13,7 @@ use std::rc::Rc;
use std::sync::RwLock; use std::sync::RwLock;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::error; use tracing::error;
use wayland_client::protocol::wl_seat::WlSeat;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Item { pub struct Item {
@@ -34,25 +36,31 @@ impl Item {
} }
/// Merges the provided node into this launcher item /// Merges the provided node into this launcher item
pub fn merge_toplevel(&mut self, node: ToplevelInfo) -> Window { pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> {
let id = node.id; let info = handle
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let id = info.id;
if self.windows.is_empty() { if self.windows.is_empty() {
self.name = node.title.clone(); self.name = info.title;
} }
let window: Window = node.into(); let window = Window::try_from(handle)?;
self.windows.insert(id, window.clone()); self.windows.insert(id, window.clone());
self.recalculate_open_state(); self.recalculate_open_state();
window Ok(window)
} }
pub fn unmerge_toplevel(&mut self, node: &ToplevelInfo) { pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) {
self.windows.remove(&node.id); if let Some(info) = handle.info() {
self.windows.remove(&info.id);
self.recalculate_open_state(); self.recalculate_open_state();
} }
}
pub fn set_window_name(&mut self, window_id: usize, name: String) { pub fn set_window_name(&mut self, window_id: usize, name: String) {
if let Some(window) = self.windows.get_mut(&window_id) { if let Some(window) = self.windows.get_mut(&window_id) {
@@ -87,22 +95,29 @@ impl Item {
} }
} }
impl From<ToplevelInfo> for Item { impl TryFrom<ToplevelHandle> for Item {
fn from(toplevel: ToplevelInfo) -> Self { type Error = Report;
let open_state = OpenState::from_toplevel(&toplevel);
let name = toplevel.title.clone(); fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> {
let app_id = toplevel.app_id.clone(); let info = handle
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let name = info.title.clone();
let app_id = info.app_id.clone();
let open_state = OpenState::from(&info);
let mut windows = IndexMap::new(); let mut windows = IndexMap::new();
windows.insert(toplevel.id, toplevel.into()); let window = Window::try_from(handle)?;
windows.insert(info.id, window);
Self { Ok(Self {
app_id, app_id,
favorite: false, favorite: false,
open_state, open_state,
windows, windows,
name, name,
} })
} }
} }
@@ -111,18 +126,31 @@ pub struct Window {
pub id: usize, pub id: usize,
pub name: String, pub name: String,
pub open_state: OpenState, pub open_state: OpenState,
handle: ToplevelHandle,
} }
impl From<ToplevelInfo> for Window { impl TryFrom<ToplevelHandle> for Window {
fn from(node: ToplevelInfo) -> Self { type Error = Report;
let open_state = OpenState::from_toplevel(&node);
Self { fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> {
id: node.id, let info = handle
name: node.title, .info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let open_state = OpenState::from(&info);
Ok(Self {
id: info.id,
name: info.title,
open_state, open_state,
handle,
})
} }
} }
impl Window {
pub fn focus(&self, seat: &WlSeat) {
self.handle.focus(seat);
}
} }
pub struct MenuState { pub struct MenuState {
@@ -136,27 +164,34 @@ pub struct ItemButton {
pub menu_state: Rc<RwLock<MenuState>>, pub menu_state: Rc<RwLock<MenuState>>,
} }
#[derive(Clone, Copy)]
pub struct AppearanceOptions {
pub show_names: bool,
pub show_icons: bool,
pub icon_size: i32,
}
impl ItemButton { impl ItemButton {
pub fn new( pub fn new(
item: &Item, item: &Item,
show_names: bool, appearance: AppearanceOptions,
show_icons: bool,
orientation: Orientation,
icon_theme: &IconTheme, icon_theme: &IconTheme,
orientation: Orientation,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>, tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>, controller_tx: &Sender<ItemEvent>,
) -> Self { ) -> Self {
let mut button = Button::builder(); let mut button = Button::builder();
if show_names { if appearance.show_names {
button = button.label(&item.name); button = button.label(&item.name);
} }
let button = button.build(); let button = button.build();
if show_icons { if appearance.show_icons {
let gtk_image = gtk::Image::new(); let gtk_image = gtk::Image::new();
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32); let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
match image { match image {
Ok(image) => { Ok(image) => {
button.set_image(Some(&gtk_image)); button.set_image(Some(&gtk_image));
@@ -217,7 +252,7 @@ impl ItemButton {
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::OpenPopup(Popup::button_pos(button, orientation,)) ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
); );
} else { } else {
try_send!(tx, ModuleUpdateEvent::ClosePopup); try_send!(tx, ModuleUpdateEvent::ClosePopup);
@@ -232,7 +267,7 @@ impl ItemButton {
Self { Self {
button, button,
persistent: item.favorite, persistent: item.favorite,
show_names, show_names: appearance.show_names,
menu_state, menu_state,
} }
} }

View File

@@ -3,11 +3,12 @@ mod open_state;
use self::item::{Item, ItemButton, Window}; use self::item::{Item, ItemButton, Window};
use self::open_state::OpenState; use self::open_state::OpenState;
use crate::clients::wayland::{self, ToplevelChange}; use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file; use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, read_lock, try_send, write_lock}; use crate::{lock, send_async, try_send, write_lock};
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
@@ -17,7 +18,6 @@ use serde::Deserialize;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -33,10 +33,17 @@ pub struct LauncherModule {
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
show_icons: bool, show_icons: bool,
#[serde(default = "default_icon_size")]
icon_size: i32,
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
const fn default_icon_size() -> i32 {
32
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum LauncherUpdate { pub enum LauncherUpdate {
/// Adds item /// Adds item
@@ -103,70 +110,64 @@ impl Module<gtk::Box> for LauncherModule {
let items = Arc::new(Mutex::new(items)); let items = Arc::new(Mutex::new(items));
{
let items = Arc::clone(&items);
let tx = tx.clone();
spawn(async move {
let wl = wayland::get_client().await;
let open_windows = read_lock!(wl.toplevels);
let open_windows = open_windows.clone();
for (_, (window, _)) in open_windows {
let mut items = lock!(items);
let item = items.get_mut(&window.app_id);
match item {
Some(item) => {
item.merge_toplevel(window);
}
None => {
items.insert(window.app_id.clone(), window.into());
}
}
}
let items = lock!(items);
let items = items.iter();
for (_, item) in items {
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
item.clone(),
)))?;
}
Ok::<(), Report>(())
});
}
let items2 = Arc::clone(&items); let items2 = Arc::clone(&items);
let tx2 = tx.clone();
spawn(async move { spawn(async move {
let items = items2; let items = items2;
let tx = tx2;
let mut wlrx = { let (mut wlrx, handles) = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_toplevels() wl.subscribe_toplevels()
}; };
for handle in handles.values() {
let Some(info) = handle.info() else { continue };
let mut items = lock!(items);
let item = items.get_mut(&info.app_id);
match item {
Some(item) => {
item.merge_toplevel(handle.clone())?;
}
None => {
items.insert(info.app_id.clone(), Item::try_from(handle.clone())?);
}
}
}
{
let items = lock!(items);
let items = items.iter();
for (_, item) in items {
try_send!(
tx,
ModuleUpdateEvent::Update(LauncherUpdate::AddItem(item.clone()))
);
}
}
let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update)); let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
while let Ok(event) = wlrx.recv().await { while let Ok(event) = wlrx.recv().await {
trace!("event: {:?}", event); trace!("event: {:?}", event);
let window = event.toplevel; match event {
let app_id = window.app_id.clone(); ToplevelEvent::New(handle) => {
let Some(info) = handle.info() else { continue };
match event.change {
ToplevelChange::New => {
let new_item = { let new_item = {
let mut items = lock!(items); let mut items = lock!(items);
let item = items.get_mut(&app_id); let item = items.get_mut(&info.app_id);
match item { match item {
None => { None => {
let item: Item = window.into(); let item: Item = handle.try_into()?;
items.insert(app_id.clone(), item.clone()); items.insert(info.app_id.clone(), item.clone());
ItemOrWindow::Item(item) ItemOrWindow::Item(item)
} }
Some(item) => { Some(item) => {
let window = item.merge_toplevel(window); let window = item.merge_toplevel(handle)?;
ItemOrWindow::Window(window) ItemOrWindow::Window(window)
} }
} }
@@ -177,20 +178,40 @@ impl Module<gtk::Box> for LauncherModule {
send_update(LauncherUpdate::AddItem(item)).await send_update(LauncherUpdate::AddItem(item)).await
} }
ItemOrWindow::Window(window) => { ItemOrWindow::Window(window) => {
send_update(LauncherUpdate::AddWindow(app_id, window)).await send_update(LauncherUpdate::AddWindow(info.app_id.clone(), window))
.await
} }
}?; }?;
} }
ToplevelChange::Close => { ToplevelEvent::Update(handle) => {
let Some(info) = handle.info() else { continue };
if let Some(item) = lock!(items).get_mut(&info.app_id) {
item.set_window_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone());
}
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused))
.await?;
send_update(LauncherUpdate::Title(
info.app_id.clone(),
info.id,
info.title.clone(),
))
.await?;
}
ToplevelEvent::Remove(handle) => {
let Some(info) = handle.info() else { continue };
let remove_item = { let remove_item = {
let mut items = lock!(items); let mut items = lock!(items);
let item = items.get_mut(&app_id); let item = items.get_mut(&info.app_id);
match item { match item {
Some(item) => { Some(item) => {
item.unmerge_toplevel(&window); item.unmerge_toplevel(&handle);
if item.windows.is_empty() { if item.windows.is_empty() {
items.remove(&app_id); items.remove(&info.app_id);
Some(ItemOrWindowId::Item) Some(ItemOrWindowId::Item)
} else { } else {
Some(ItemOrWindowId::Window) Some(ItemOrWindowId::Window)
@@ -202,56 +223,28 @@ impl Module<gtk::Box> for LauncherModule {
match remove_item { match remove_item {
Some(ItemOrWindowId::Item) => { Some(ItemOrWindowId::Item) => {
send_update(LauncherUpdate::RemoveItem(app_id)).await?; send_update(LauncherUpdate::RemoveItem(info.app_id.clone()))
.await?;
} }
Some(ItemOrWindowId::Window) => { Some(ItemOrWindowId::Window) => {
send_update(LauncherUpdate::RemoveWindow(app_id, window.id)) send_update(LauncherUpdate::RemoveWindow(
info.app_id.clone(),
info.id,
))
.await?; .await?;
} }
None => {} None => {}
}; };
} }
ToplevelChange::Focus(focused) => {
let mut update_title = false;
if focused {
if let Some(item) = lock!(items).get_mut(&app_id) {
item.set_window_focused(window.id, true);
// might be switching focus between windows of same app
if item.windows.len() > 1 {
item.set_window_name(window.id, window.title.clone());
update_title = true;
}
}
}
send_update(LauncherUpdate::Focus(app_id.clone(), focused)).await?;
if update_title {
send_update(LauncherUpdate::Title(app_id, window.id, window.title))
.await?;
}
}
ToplevelChange::Title(title) => {
if let Some(item) = lock!(items).get_mut(&app_id) {
item.set_window_name(window.id, title.clone());
}
send_update(LauncherUpdate::Title(app_id, window.id, title)).await?;
}
ToplevelChange::Fullscreen(_) => {}
} }
} }
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<LauncherUpdate>>>(()) Ok::<(), Report>(())
}); });
// listen to ui events // listen to ui events
spawn(async move { spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
trace!("{:?}", event);
if let ItemEvent::OpenItem(app_id) = event { if let ItemEvent::OpenItem(app_id) = event {
find_desktop_file(&app_id).map_or_else( find_desktop_file(&app_id).map_or_else(
|| error!("Could not find desktop file for {}", app_id), || error!("Could not find desktop file for {}", app_id),
@@ -275,6 +268,8 @@ impl Module<gtk::Box> for LauncherModule {
}, },
); );
} else { } else {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
let items = lock!(items); let items = lock!(items);
@@ -287,11 +282,12 @@ impl Module<gtk::Box> for LauncherModule {
}; };
if let Some(id) = id { if let Some(id) = id {
let toplevels = read_lock!(wl.toplevels); if let Some(window) =
let seat = wl.seats.first().expect("Failed to get Wayland seat"); items.iter().find_map(|(_, item)| item.windows.get(&id))
if let Some((_top, handle)) = toplevels.get(&id) { {
handle.activate(seat); let seat = wl.get_seats().pop().expect("Failed to get Wayland seat");
}; window.focus(&seat);
}
} }
// roundtrip to immediately send activate event // roundtrip to immediately send activate event
@@ -318,8 +314,13 @@ impl Module<gtk::Box> for LauncherModule {
let controller_tx = context.controller_tx.clone(); let controller_tx = context.controller_tx.clone();
let appearance_options = AppearanceOptions {
show_names: self.show_names,
show_icons: self.show_icons,
icon_size: self.icon_size,
};
let show_names = self.show_names; let show_names = self.show_names;
let show_icons = self.show_icons;
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
let mut buttons = IndexMap::<String, ItemButton>::new(); let mut buttons = IndexMap::<String, ItemButton>::new();
@@ -334,10 +335,9 @@ impl Module<gtk::Box> for LauncherModule {
} else { } else {
let button = ItemButton::new( let button = ItemButton::new(
&item, &item,
show_names, appearance_options,
show_icons,
orientation,
&icon_theme, &icon_theme,
orientation,
&context.tx, &context.tx,
&controller_tx, &controller_tx,
); );
@@ -413,10 +413,7 @@ impl Module<gtk::Box> for LauncherModule {
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250; const MAX_WIDTH: i32 = 250;
let container = gtk::Box::builder() let container = gtk::Box::new(Orientation::Vertical, 0);
.orientation(Orientation::Vertical)
.name("popup-launcher")
.build();
// we need some content to force the container to have a size // we need some content to force the container to have a size
let placeholder = Button::with_label("PLACEHOLDER"); let placeholder = Button::with_label("PLACEHOLDER");
@@ -444,12 +441,8 @@ impl Module<gtk::Box> for LauncherModule {
{ {
let tx = controller_tx.clone(); let tx = controller_tx.clone();
button.connect_clicked(move |button| { button.connect_clicked(move |_| {
try_send!(tx, ItemEvent::FocusWindow(win.id)); try_send!(tx, ItemEvent::FocusWindow(win.id));
if let Some(win) = button.window() {
win.hide();
}
}); });
} }
@@ -536,6 +529,9 @@ impl Module<gtk::Box> for LauncherModule {
/// This is a hacky number derived from /// This is a hacky number derived from
/// "what fits inside the 250px popup" /// "what fits inside the 250px popup"
/// and probably won't hold up with wide fonts. /// and probably won't hold up with wide fonts.
///
/// TODO: Migrate this to truncate system
///
fn clamp(str: &str) -> String { fn clamp(str: &str) -> String {
const MAX_CHARS: usize = 24; const MAX_CHARS: usize = 24;

View File

@@ -7,14 +7,15 @@ pub enum OpenState {
Open { focused: bool }, Open { focused: bool },
} }
impl OpenState { impl From<&ToplevelInfo> for OpenState {
/// Creates from `SwayNode` fn from(info: &ToplevelInfo) -> Self {
pub const fn from_toplevel(toplevel: &ToplevelInfo) -> Self {
Self::Open { Self::Open {
focused: toplevel.active, focused: info.focused,
}
} }
} }
impl OpenState {
/// Creates open with focused /// Creates open with focused
pub const fn focused(focused: bool) -> Self { pub const fn focused(focused: bool) -> Self {
Self::Open { focused } Self::Open { focused }

View File

@@ -10,6 +10,7 @@ pub mod clipboard;
pub mod clock; pub mod clock;
pub mod custom; pub mod custom;
pub mod focused; pub mod focused;
pub mod label;
pub mod launcher; pub mod launcher;
#[cfg(feature = "music")] #[cfg(feature = "music")]
pub mod music; pub mod music;
@@ -18,16 +19,23 @@ pub mod script;
pub mod sysinfo; pub mod sysinfo;
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub mod tray; pub mod tray;
#[cfg(feature = "upower")]
pub mod upower;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub mod workspaces; pub mod workspaces;
use crate::config::BarPosition; use crate::bridge_channel::BridgeChannel;
use crate::popup::ButtonGeometry; use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::popup::{Popup, WidgetGeometry};
use crate::{read_lock, send, write_lock};
use color_eyre::Result; use color_eyre::Result;
use glib::IsA; use glib::IsA;
use gtk::gdk::Monitor; use gtk::gdk::{EventMask, Monitor};
use gtk::{Application, IconTheme, Widget}; use gtk::prelude::*;
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::debug;
#[derive(Clone)] #[derive(Clone)]
pub enum ModuleLocation { pub enum ModuleLocation {
@@ -49,10 +57,10 @@ pub enum ModuleUpdateEvent<T> {
/// Sends an update to the module UI /// Sends an update to the module UI
Update(T), Update(T),
/// Toggles the open state of the popup. /// Toggles the open state of the popup.
TogglePopup(ButtonGeometry), TogglePopup(WidgetGeometry),
/// Force sets the popup open. /// Force sets the popup open.
/// Takes the button X position and width. /// Takes the button X position and width.
OpenPopup(ButtonGeometry), OpenPopup(WidgetGeometry),
/// Force sets the popup closed. /// Force sets the popup closed.
ClosePopup, ClosePopup,
} }
@@ -104,3 +112,184 @@ where
None None
} }
} }
/// Creates a module and sets it up.
/// This setup includes widget/popup content and event channels.
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleWidget<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.style_context().add_class(name);
let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.style_context()
.add_class(&format!("popup-{name}"));
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)
}
/// Registers the popup content with the popup.
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
write_lock!(popup).register_content(id, popup_content);
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
/// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>(
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
w_tx: glib::Sender<TSend>,
p_tx: glib::Sender<TSend>,
popup: Arc<RwLock<Popup>>,
name: &'static str,
id: usize,
has_popup: bool,
) where
TSend: Clone + Send + 'static,
{
// some rare cases can cause the popup to incorrectly calculate its size on first open.
// we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false;
channel.recv(move |ev| {
match ev {
ModuleUpdateEvent::Update(update) => {
if has_popup {
send!(p_tx, update.clone());
}
send!(w_tx, update);
}
ModuleUpdateEvent::TogglePopup(geometry) => {
debug!("Toggling popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
if popup.is_visible() {
popup.hide();
} else {
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
popup.hide();
popup.show_content(id);
popup.show(geometry);
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
has_popup_opened = true;
}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
popup.hide();
}
}
Continue(true)
});
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleWidget<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup {
popup.set_widget_name(&format!("popup-{name}"));
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
widget_parts.widget.style_context().add_class(part);
}
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup.style_context().add_class(&format!("popup-{part}"));
}
}
}
}
/// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned.
pub fn wrap_widget<W: IsA<Widget>>(
widget: &W,
common: CommonConfig,
orientation: Orientation,
) -> EventBox {
let transition_type = common
.transition_type
.as_ref()
.unwrap_or(&TransitionType::SlideStart)
.to_revealer_transition_type(orientation);
let revealer = Revealer::builder()
.transition_type(transition_type)
.transition_duration(common.transition_duration.unwrap_or(250))
.build();
revealer.add(widget);
revealer.set_reveal_child(true);
let container = EventBox::new();
container.add_events(EventMask::SCROLL_MASK);
container.add(&revealer);
common.install_events(&container, &revealer);
container
}

View File

@@ -88,6 +88,15 @@ pub struct MusicModule {
#[serde(default = "default_music_dir")] #[serde(default = "default_music_dir")]
pub(crate) music_dir: PathBuf, pub(crate) music_dir: PathBuf,
#[serde(default = "crate::config::default_true")]
pub(crate) show_status_icon: bool,
#[serde(default = "default_icon_size")]
pub(crate) icon_size: i32,
#[serde(default = "default_cover_image_size")]
pub(crate) cover_image_size: i32,
// -- Common -- // -- Common --
pub(crate) truncate: Option<TruncateMode>, pub(crate) truncate: Option<TruncateMode>,
@@ -138,3 +147,11 @@ fn default_icon_artist() -> String {
fn default_music_dir() -> PathBuf { fn default_music_dir() -> PathBuf {
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default()) audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
} }
const fn default_icon_size() -> i32 {
24
}
const fn default_cover_image_size() -> i32 {
128
}

View File

@@ -1,6 +1,7 @@
mod config; mod config;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track}; use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::gtk_helpers::add_class;
use crate::image::{new_icon_button, new_icon_label, ImageProvider}; use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup; use crate::popup::Popup;
@@ -135,7 +136,7 @@ impl Module<Button> for MusicModule {
PlayerCommand::Play => client.play(), PlayerCommand::Play => client.play(),
PlayerCommand::Pause => client.pause(), PlayerCommand::Pause => client.pause(),
PlayerCommand::Next => client.next(), PlayerCommand::Next => client.next(),
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), // .unwrap_or_else(|_| error!("Failed to update player volume")), PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
}; };
if let Err(err) = res { if let Err(err) = res {
@@ -155,10 +156,12 @@ impl Module<Button> for MusicModule {
) -> Result<ModuleWidget<Button>> { ) -> Result<ModuleWidget<Button>> {
let button = Button::new(); let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5); let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&button_contents, "contents");
button.add(&button_contents); button.add(&button_contents);
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, 24); let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, 24); let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
let label = Label::new(None); let label = Label::new(None);
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
@@ -179,7 +182,7 @@ impl Module<Button> for MusicModule {
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation,)) ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
); );
}); });
} }
@@ -192,21 +195,27 @@ impl Module<Button> for MusicModule {
if let Some(event) = event.take() { if let Some(event) = event.take() {
label.set_label(&event.display_string); label.set_label(&event.display_string);
button.show();
match event.status.state { match event.status.state {
PlayerState::Playing => { PlayerState::Playing if self.show_status_icon => {
icon_play.show(); icon_play.show();
icon_pause.hide(); icon_pause.hide();
} }
PlayerState::Paused => { PlayerState::Paused if self.show_status_icon => {
icon_pause.show(); icon_pause.show();
icon_play.hide(); icon_play.hide();
} }
PlayerState::Stopped => { PlayerState::Stopped => {
button.hide(); button.hide();
} }
_ => {}
} }
button.show(); if !self.show_status_icon {
icon_pause.hide();
icon_play.hide();
}
} else { } else {
button.hide(); button.hide();
try_send!(tx, ModuleUpdateEvent::ClosePopup); try_send!(tx, ModuleUpdateEvent::ClosePopup);
@@ -232,17 +241,13 @@ impl Module<Button> for MusicModule {
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::builder() let container = gtk::Box::new(Orientation::Horizontal, 10);
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-music")
.build();
let album_image = gtk::Image::builder() let album_image = gtk::Image::builder()
.width_request(128) .width_request(128)
.height_request(128) .height_request(128)
.name("album-art")
.build(); .build();
add_class(&album_image, "album-art");
let icons = self.icons; let icons = self.icons;
@@ -251,27 +256,28 @@ impl Module<Button> for MusicModule {
let album_label = IconLabel::new(&icons.album, None, icon_theme); let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme); let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
title_label.container.set_widget_name("title"); add_class(&title_label.container, "title");
album_label.container.set_widget_name("album"); add_class(&album_label.container, "album");
artist_label.container.set_widget_name("artist"); add_class(&artist_label.container, "artist");
info_box.add(&title_label.container); info_box.add(&title_label.container);
info_box.add(&album_label.container); info_box.add(&album_label.container);
info_box.add(&artist_label.container); info_box.add(&artist_label.container);
let controls_box = gtk::Box::builder().name("controls").build(); let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
add_class(&controls_box, "controls");
let btn_prev = new_icon_button(&icons.prev, icon_theme, 24); let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
btn_prev.set_widget_name("btn-prev"); add_class(&btn_prev, "btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, 24); let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
btn_play.set_widget_name("btn-play"); add_class(&btn_play, "btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, 24); let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
btn_pause.set_widget_name("btn-pause"); add_class(&btn_pause, "btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, 24); let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
btn_next.set_widget_name("btn-next"); add_class(&btn_next, "btn-next");
controls_box.add(&btn_prev); controls_box.add(&btn_prev);
controls_box.add(&btn_play); controls_box.add(&btn_play);
@@ -280,18 +286,15 @@ impl Module<Button> for MusicModule {
info_box.add(&controls_box); info_box.add(&controls_box);
let volume_box = gtk::Box::builder() let volume_box = gtk::Box::new(Orientation::Vertical, 5);
.orientation(Orientation::Vertical) add_class(&volume_box, "volume");
.spacing(5)
.name("volume")
.build();
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0); let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true); volume_slider.set_inverted(true);
volume_slider.set_widget_name("slider"); add_class(&volume_slider, "slider");
let volume_icon = new_icon_label(&icons.volume, icon_theme, 24); let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
volume_icon.style_context().add_class("icon"); add_class(&volume_icon, "icon");
volume_box.pack_start(&volume_slider, true, true, 0); volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0); volume_box.pack_end(&volume_icon, false, false, 0);
@@ -330,6 +333,7 @@ impl Module<Button> for MusicModule {
{ {
let icon_theme = icon_theme.clone(); let icon_theme = icon_theme.clone();
let image_size = self.cover_image_size;
let mut prev_cover = None; let mut prev_cover = None;
rx.attach(None, move |update| { rx.attach(None, move |update| {
@@ -338,9 +342,9 @@ impl Module<Button> for MusicModule {
let new_cover = update.song.cover_path; let new_cover = update.song.cover_path;
if prev_cover != new_cover { if prev_cover != new_cover {
prev_cover = new_cover.clone(); prev_cover = new_cover.clone();
let res = match new_cover let res = match new_cover.map(|cover_path| {
.map(|cover_path| ImageProvider::parse(&cover_path, &icon_theme, 128)) ImageProvider::parse(&cover_path, &icon_theme, image_size)
{ }) {
Some(Ok(image)) => image.load_into_image(album_image.clone()), Some(Ok(image)) => image.load_into_image(album_image.clone()),
Some(Err(err)) => { Some(Err(err)) => {
album_image.set_from_pixbuf(None); album_image.set_from_pixbuf(None);
@@ -451,11 +455,11 @@ impl IconLabel {
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self { fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5); let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = new_icon_label(icon_input, icon_theme, 32); let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label); let label = Label::new(label);
icon.style_context().add_class("icon"); add_class(&icon, "icon");
label.style_context().add_class("label"); add_class(&label, "label");
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);

View File

@@ -62,7 +62,7 @@ impl Module<Label> for ScriptModule {
let script: Script = self.into(); let script: Script = self.into();
spawn(async move { spawn(async move {
script.run(move |(out, _)| match out { script.run(None, move |out, _| match out {
OutputStream::Stdout(stdout) => { OutputStream::Stdout(stdout) => {
try_send!(tx, ModuleUpdateEvent::Update(stdout)); try_send!(tx, ModuleUpdateEvent::Update(stdout));
}, },

View File

@@ -1,4 +1,5 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::send_async; use crate::send_async;
use color_eyre::Result; use color_eyre::Result;
@@ -193,12 +194,11 @@ impl Module<gtk::Box> for SysInfoModule {
let mut labels = Vec::new(); let mut labels = Vec::new();
for format in &self.format { for format in &self.format {
let label = Label::builder() let label = Label::builder().label(format).use_markup(true).build();
.label(format)
.use_markup(true) add_class(&label, "item");
.name("item")
.build();
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
container.add(&label); container.add(&label);
labels.push(label); labels.push(label);
} }

View File

@@ -3,8 +3,12 @@ use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, try_send}; use crate::{await_sync, try_send};
use color_eyre::Result; use color_eyre::Result;
use gtk::gdk_pixbuf::{Colorspace, InterpType};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem}; use gtk::{
gdk_pixbuf, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem,
SeparatorMenuItem,
};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType}; use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
@@ -20,18 +24,54 @@ pub struct TrayModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
/// Gets a GTK `Image` component /// Attempts to get a GTK `Image` component
/// for the status notifier item's icon. /// for the status notifier item's icon.
fn get_icon(item: &StatusNotifierItem) -> Option<Image> { fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> {
item.icon_theme_path.as_ref().and_then(|path| { let theme = item
.icon_theme_path
.as_ref()
.map(|path| {
let theme = IconTheme::new(); let theme = IconTheme::new();
theme.append_search_path(path); theme.append_search_path(path);
theme
})
.unwrap_or_default();
item.icon_name.as_ref().and_then(|icon_name| { item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
}) })
}) }
/// Attempts to get an image from the item pixmap.
///
/// The pixmap is supplied in ARGB32 format,
/// which has 8 bits per sample and a bit stride of `4*width`.
fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image> {
const BITS_PER_SAMPLE: i32 = 8; //
let pixmap = item
.icon_pixmap
.as_ref()
.and_then(|pixmap| pixmap.first())?;
let bytes = glib::Bytes::from(&pixmap.pixels);
let row_stride = pixmap.width * 4; //
let pixbuf = gdk_pixbuf::Pixbuf::from_bytes(
&bytes,
Colorspace::Rgb,
true,
BITS_PER_SAMPLE,
pixmap.width,
pixmap.height,
row_stride,
);
let pixbuf = pixbuf
.scale_simple(16, 16, InterpType::Bilinear)
.unwrap_or(pixbuf);
Some(Image::from_pixbuf(Some(&pixbuf)))
} }
/// Recursively gets GTK `MenuItem` components /// Recursively gets GTK `MenuItem` components
@@ -147,13 +187,25 @@ impl Module<MenuBar> for TrayModule {
address, address,
menu, menu,
} => { } => {
let addr = &address;
let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| { let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| {
let menu_item = MenuItem::new(); let menu_item = MenuItem::new();
menu_item.style_context().add_class("item"); menu_item.style_context().add_class("item");
if let Some(image) = get_icon(&item) {
get_image_from_icon_name(&item)
.or_else(|| get_image_from_pixmap(&item))
.map_or_else(
|| {
let label =
Label::new(Some(item.title.as_ref().unwrap_or(addr)));
menu_item.add(&label);
},
|image| {
image.set_widget_name(address.as_str()); image.set_widget_name(address.as_str());
menu_item.add(&image); menu_item.add(&image);
} },
);
container.add(&menu_item); container.add(&menu_item);
menu_item.show_all(); menu_item.show_all();
menu_item menu_item

281
src/modules/upower.rs Normal file
View File

@@ -0,0 +1,281 @@
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{await_sync, error, send_async, try_send};
use color_eyre::Result;
use futures_lite::stream::StreamExt;
use gtk::{prelude::*, Button};
use gtk::{Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use upower_dbus::BatteryState;
use zbus;
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
const MINUTE: i64 = 60;
#[derive(Debug, Deserialize, Clone)]
pub struct UpowerModule {
#[serde(default = "default_format")]
format: String,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_format() -> String {
String::from("{percentage}%")
}
#[derive(Clone, Debug)]
pub struct UpowerProperties {
percentage: f64,
icon_name: String,
state: u32,
time_to_full: i64,
time_to_empty: i64,
}
impl Module<gtk::Box> for UpowerModule {
type SendMessage = UpowerProperties;
type ReceiveMessage = ();
fn name() -> &'static str {
"upower"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
spawn(async move {
// await_sync due to strange "higher-ranked lifetime error"
let display_proxy = await_sync(async move { get_display_proxy().await });
let mut prop_changed_stream = display_proxy.receive_properties_changed().await?;
let device_interface_name =
zbus::names::InterfaceName::from_static_str("org.freedesktop.UPower.Device")
.expect("failed to create zbus InterfaceName");
let properties = display_proxy.get_all(device_interface_name.clone()).await?;
let percentage = *properties["Percentage"]
.downcast_ref::<f64>()
.expect("expected percentage: f64 in HashMap of all properties");
let icon_name = properties["IconName"]
.downcast_ref::<str>()
.expect("expected IconName: str in HashMap of all properties")
.to_string();
let state = *properties["State"]
.downcast_ref::<u32>()
.expect("expected State: u32 in HashMap of all properties");
let time_to_full = *properties["TimeToFull"]
.downcast_ref::<i64>()
.expect("expected TimeToFull: i64 in HashMap of all properties");
let time_to_empty = *properties["TimeToEmpty"]
.downcast_ref::<i64>()
.expect("expected TimeToEmpty: i64 in HashMap of all properties");
let mut properties = UpowerProperties {
percentage,
icon_name: icon_name.clone(),
state,
time_to_full,
time_to_empty,
};
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
while let Some(signal) = prop_changed_stream.next().await {
let args = signal.args().expect("Invalid signal arguments");
if args.interface_name != device_interface_name {
continue;
}
for (name, changed_value) in args.changed_properties {
match name {
"Percentage" => {
properties.percentage = changed_value
.downcast::<f64>()
.expect("expected Percentage to be f64");
}
"IconName" => {
properties.icon_name = changed_value
.downcast_ref::<str>()
.expect("expected IconName to be str")
.to_string();
}
"State" => {
properties.state = changed_value
.downcast::<u32>()
.expect("expected State to be u32");
}
"TimeToFull" => {
properties.time_to_full = changed_value
.downcast::<i64>()
.expect("expected TimeToFull to be i64");
}
"TimeToEmpty" => {
properties.time_to_empty = changed_value
.downcast::<i64>()
.expect("expected TimeToEmpty to be i64");
}
_ => {}
}
}
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
}
Result::<()>::Ok(())
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new();
add_class(&icon, "icon");
let label = Label::builder()
.label(&self.format)
.use_markup(true)
.build();
add_class(&label, "label");
let container = gtk::Box::new(Orientation::Horizontal, 0);
add_class(&container, "upower");
let button = Button::new();
add_class(&button, "button");
button.add(&label);
container.add(&button);
container.add(&icon);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
);
});
label.set_angle(info.bar_position.get_angle());
let format = self.format.clone();
context
.widget_rx
.attach(None, move |properties: UpowerProperties| {
let format = format.replace("{percentage}", &properties.percentage.to_string());
let icon_name = String::from("icon:") + &properties.icon_name;
if let Err(err) = ImageProvider::parse(&icon_name, &icon_theme, 32)
.and_then(|provider| provider.load_into_image(icon.clone()))
{
error!("{err:?}");
}
label.set_markup(format.as_ref());
Continue(true)
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
popup,
})
}
fn into_popup(
self,
_tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.build();
let label = Label::new(None);
add_class(&label, "upower-details");
rx.attach(None, move |properties| {
let mut format = String::new();
let state = u32_to_battery_state(properties.state);
match state {
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
let ttf = properties.time_to_full;
if ttf > 0 {
format = format!("Full in {}", seconds_to_string(ttf));
}
}
Ok(BatteryState::Discharging | BatteryState::PendingDischarge) => {
let tte = properties.time_to_empty;
if tte > 0 {
format = format!("Empty in {}", seconds_to_string(tte));
}
}
Err(state) => error!("Invalid battery state: {state}"),
_ => {}
}
label.set_markup(&format);
Continue(true)
});
container.show_all();
Some(container)
}
}
fn seconds_to_string(seconds: i64) -> String {
let mut time_string = String::new();
let days = seconds / (DAY);
if days > 0 {
time_string += &format!("{days}d");
}
let hours = (seconds % DAY) / HOUR;
if hours > 0 {
time_string += &format!(" {hours}h");
}
let minutes = (seconds % HOUR) / MINUTE;
if minutes > 0 {
time_string += &format!(" {minutes}m");
}
time_string.trim_start().to_string()
}
const fn u32_to_battery_state(number: u32) -> Result<BatteryState, u32> {
if number == (BatteryState::Unknown as u32) {
Ok(BatteryState::Unknown)
} else if number == (BatteryState::Charging as u32) {
Ok(BatteryState::Charging)
} else if number == (BatteryState::Discharging as u32) {
Ok(BatteryState::Discharging)
} else if number == (BatteryState::Empty as u32) {
Ok(BatteryState::Empty)
} else if number == (BatteryState::FullyCharged as u32) {
Ok(BatteryState::FullyCharged)
} else if number == (BatteryState::PendingCharge as u32) {
Ok(BatteryState::PendingCharge)
} else if number == (BatteryState::PendingDischarge as u32) {
Ok(BatteryState::PendingDischarge)
} else {
Err(number)
}
}

View File

@@ -41,21 +41,29 @@ pub struct WorkspacesModule {
#[serde(default)] #[serde(default)]
sort: SortOrder, sort: SortOrder,
#[serde(default = "default_icon_size")]
icon_size: i32,
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
const fn default_icon_size() -> i32 {
32
}
/// Creates a button from a workspace /// Creates a button from a workspace
fn create_button( fn create_button(
name: &str, name: &str,
focused: bool, focused: bool,
name_map: &HashMap<String, String>, name_map: &HashMap<String, String>,
icon_theme: &IconTheme, icon_theme: &IconTheme,
icon_size: i32,
tx: &Sender<String>, tx: &Sender<String>,
) -> Button { ) -> Button {
let label = name_map.get(name).map_or(name, String::as_str); let label = name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, icon_theme, 32); let button = new_icon_button(label, icon_theme, icon_size);
button.set_widget_name(name); button.set_widget_name(name);
let style_context = button.style_context(); let style_context = button.style_context();
@@ -157,6 +165,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let container = container.clone(); let container = container.clone();
let output_name = info.output_name.to_string(); let output_name = info.output_name.to_string();
let icon_theme = info.icon_theme.clone(); let icon_theme = info.icon_theme.clone();
let icon_size = self.icon_size;
// keep track of whether init event has fired previously // keep track of whether init event has fired previously
// since it fires for every workspace subscriber // since it fires for every workspace subscriber
@@ -174,6 +183,7 @@ impl Module<gtk::Box> for WorkspacesModule {
workspace.focused, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size,
&context.controller_tx, &context.controller_tx,
); );
container.add(&item); container.add(&item);
@@ -209,6 +219,7 @@ impl Module<gtk::Box> for WorkspacesModule {
workspace.focused, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size,
&context.controller_tx, &context.controller_tx,
); );
@@ -233,6 +244,7 @@ impl Module<gtk::Box> for WorkspacesModule {
workspace.focused, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size,
&context.controller_tx, &context.controller_tx,
); );

View File

@@ -4,7 +4,7 @@ use crate::config::BarPosition;
use crate::modules::ModuleInfo; use crate::modules::ModuleInfo;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ApplicationWindow, Button, Orientation}; use gtk::{ApplicationWindow, Orientation};
use tracing::debug; use tracing::debug;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -19,7 +19,7 @@ impl Popup {
/// Creates a new popup window. /// Creates a new popup window.
/// This includes setting up gtk-layer-shell /// This includes setting up gtk-layer-shell
/// and an empty `gtk::Box` container. /// and an empty `gtk::Box` container.
pub fn new(module_info: &ModuleInfo) -> Self { pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
let pos = module_info.bar_position; let pos = module_info.bar_position;
let orientation = pos.get_orientation(); let orientation = pos.get_orientation();
@@ -34,22 +34,22 @@ impl Popup {
gtk_layer_shell::set_margin( gtk_layer_shell::set_margin(
&win, &win,
gtk_layer_shell::Edge::Top, gtk_layer_shell::Edge::Top,
if pos == BarPosition::Top { 5 } else { 0 }, if pos == BarPosition::Top { gap } else { 0 },
); );
gtk_layer_shell::set_margin( gtk_layer_shell::set_margin(
&win, &win,
gtk_layer_shell::Edge::Bottom, gtk_layer_shell::Edge::Bottom,
if pos == BarPosition::Bottom { 5 } else { 0 }, if pos == BarPosition::Bottom { gap } else { 0 },
); );
gtk_layer_shell::set_margin( gtk_layer_shell::set_margin(
&win, &win,
gtk_layer_shell::Edge::Left, gtk_layer_shell::Edge::Left,
if pos == BarPosition::Left { 5 } else { 0 }, if pos == BarPosition::Left { gap } else { 0 },
); );
gtk_layer_shell::set_margin( gtk_layer_shell::set_margin(
&win, &win,
gtk_layer_shell::Edge::Right, gtk_layer_shell::Edge::Right,
if pos == BarPosition::Right { 5 } else { 0 }, if pos == BarPosition::Right { gap } else { 0 },
); );
gtk_layer_shell::set_anchor( gtk_layer_shell::set_anchor(
@@ -133,7 +133,7 @@ impl Popup {
} }
/// Shows the popup /// Shows the popup
pub fn show(&self, geometry: ButtonGeometry) { pub fn show(&self, geometry: WidgetGeometry) {
self.window.show(); self.window.show();
self.set_pos(geometry); self.set_pos(geometry);
} }
@@ -150,7 +150,7 @@ impl Popup {
/// Sets the popup's X/Y position relative to the left or border of the screen /// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation). /// (depending on orientation).
fn set_pos(&self, geometry: ButtonGeometry) { fn set_pos(&self, geometry: WidgetGeometry) {
let orientation = self.pos.get_orientation(); let orientation = self.pos.get_orientation();
let mon_workarea = self.monitor.workarea(); let mon_workarea = self.monitor.workarea();
@@ -190,14 +190,17 @@ impl Popup {
/// Gets the absolute X position of the button /// Gets the absolute X position of the button
/// and its width / height (depending on orientation). /// and its width / height (depending on orientation).
pub fn button_pos(button: &Button, orientation: Orientation) -> ButtonGeometry { pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
let button_size = if orientation == Orientation::Horizontal { where
button.allocation().width() W: IsA<gtk::Widget>,
{
let widget_size = if orientation == Orientation::Horizontal {
widget.allocation().width()
} else { } else {
button.allocation().height() widget.allocation().height()
}; };
let top_level = button.toplevel().expect("Failed to get top-level widget"); let top_level = widget.toplevel().expect("Failed to get top-level widget");
let bar_size = if orientation == Orientation::Horizontal { let bar_size = if orientation == Orientation::Horizontal {
top_level.allocation().width() top_level.allocation().width()
@@ -205,26 +208,26 @@ impl Popup {
top_level.allocation().height() top_level.allocation().height()
}; };
let (button_x, button_y) = button let (widget_x, widget_y) = widget
.translate_coordinates(&top_level, 0, 0) .translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0)); .unwrap_or((0, 0));
let button_pos = if orientation == Orientation::Horizontal { let widget_pos = if orientation == Orientation::Horizontal {
button_x widget_x
} else { } else {
button_y widget_y
}; };
ButtonGeometry { WidgetGeometry {
position: button_pos, position: widget_pos,
size: button_size, size: widget_size,
bar_size, bar_size,
} }
} }
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct ButtonGeometry { pub struct WidgetGeometry {
position: i32, position: i32,
size: i32, size: i32,
bar_size: i32, bar_size: i32,

View File

@@ -2,6 +2,7 @@ use crate::send_async;
use color_eyre::eyre::WrapErr; use color_eyre::eyre::WrapErr;
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use serde::Deserialize; use serde::Deserialize;
use std::cmp::min;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::process::Stdio; use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::{AsyncBufReadExt, BufReader};
@@ -9,7 +10,7 @@ use tokio::process::Command;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::time::sleep; use tokio::time::sleep;
use tokio::{select, spawn}; use tokio::{select, spawn};
use tracing::{error, warn}; use tracing::{debug, error, trace, warn};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(untagged)] #[serde(untagged)]
@@ -110,7 +111,13 @@ enum ScriptInputToken {
Mode(ScriptMode), Mode(ScriptMode),
Interval(u64), Interval(u64),
Cmd(String), Cmd(String),
Colon, }
#[derive(Debug, Copy, Clone)]
enum CurrentToken {
Mode,
Interval,
Cmd,
} }
impl From<&str> for Script { impl From<&str> for Script {
@@ -118,46 +125,53 @@ impl From<&str> for Script {
let mut script = Self::default(); let mut script = Self::default();
let mut tokens = vec![]; let mut tokens = vec![];
let mut current_state = CurrentToken::Mode;
let mut chars = str.chars().collect::<Vec<_>>(); let mut chars = str.chars().collect::<Vec<_>>();
while !chars.is_empty() { while !chars.is_empty() {
let char = chars[0]; let char = chars[0];
let (token, skip) = match char { let parse_res = match current_state {
':' => (ScriptInputToken::Colon, 1), CurrentToken::Mode => {
// interval current_state = CurrentToken::Interval;
'0'..='9' => {
if matches!(char, 'p' | 'w') {
let mode_str = chars.iter().take_while(|&c| c != &':').collect::<String>();
let len = mode_str.len();
let token = ScriptMode::try_parse(&mode_str).ok();
token.map(|token| (ScriptInputToken::Mode(token), len))
} else {
None
}
}
CurrentToken::Interval => {
current_state = CurrentToken::Cmd;
if char.is_ascii_digit() {
let interval_str = chars let interval_str = chars
.iter() .iter()
.take_while(|c| c.is_ascii_digit()) .take_while(|c| c.is_ascii_digit())
.collect::<String>(); .collect::<String>();
let len = interval_str.len();
let interval = interval_str.parse::<u64>().unwrap_or_else(|_| { let token = interval_str.parse::<u64>().ok();
warn!("Received invalid interval in script string. Falling back to default `5000ms`."); token.map(|token| (ScriptInputToken::Interval(token), len))
5000 } else {
}); None
(ScriptInputToken::Interval(interval), interval_str.len())
} }
// watching or polling
'w' | 'p' => {
let mode_str = chars.iter().take_while(|&c| c != &':').collect::<String>();
let len = mode_str.len();
let token = ScriptMode::try_parse(&mode_str)
.map_or(ScriptInputToken::Cmd(mode_str), |mode| {
ScriptInputToken::Mode(mode)
});
(token, len)
} }
_ => { CurrentToken::Cmd => {
let cmd_str = chars.iter().take_while(|_| true).collect::<String>(); let cmd_str = chars.iter().take_while(|_| true).collect::<String>();
let len = cmd_str.len(); let len = cmd_str.len();
(ScriptInputToken::Cmd(cmd_str), len) Some((ScriptInputToken::Cmd(cmd_str), len))
} }
}; };
if let Some((token, skip)) = parse_res {
tokens.push(token); tokens.push(token);
chars.drain(..skip); chars.drain(..min(skip + 1, chars.len())); // skip 1 extra for colon
}
} }
for token in tokens { for token in tokens {
@@ -165,7 +179,6 @@ impl From<&str> for Script {
ScriptInputToken::Mode(mode) => script.mode = mode, ScriptInputToken::Mode(mode) => script.mode = mode,
ScriptInputToken::Interval(interval) => script.interval = interval, ScriptInputToken::Interval(interval) => script.interval = interval,
ScriptInputToken::Cmd(cmd) => script.cmd = cmd, ScriptInputToken::Cmd(cmd) => script.cmd = cmd,
ScriptInputToken::Colon => {}
} }
} }
@@ -180,20 +193,22 @@ impl Script {
script script
} }
pub async fn run<F>(&self, callback: F) /// Runs the script, passing `args` if provided.
/// Runs `f`, passing the output stream and whether the command returned 0.
pub async fn run<F>(&self, args: Option<&[String]>, callback: F)
where where
F: Fn((OutputStream, bool)), F: Fn(OutputStream, bool),
{ {
loop { loop {
match self.mode { match self.mode {
ScriptMode::Poll => match self.get_output().await { ScriptMode::Poll => match self.get_output(args).await {
Ok(output) => callback(output), Ok(output) => callback(output.0, output.1),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}, },
ScriptMode::Watch => match self.spawn().await { ScriptMode::Watch => match self.spawn().await {
Ok(mut rx) => { Ok(mut rx) => {
while let Some(msg) = rx.recv().await { while let Some(msg) = rx.recv().await {
callback((msg, true)); callback(msg, true);
} }
} }
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
@@ -210,28 +225,45 @@ impl Script {
/// the `stdout` is returned. /// the `stdout` is returned.
/// Otherwise, an `Err` variant /// Otherwise, an `Err` variant
/// containing the `stderr` is returned. /// containing the `stderr` is returned.
pub async fn get_output(&self) -> Result<(OutputStream, bool)> { pub async fn get_output(&self, args: Option<&[String]>) -> Result<(OutputStream, bool)> {
let mut args_list = vec!["-c", &self.cmd];
if let Some(args) = args {
args_list.extend(args.iter().map(String::as_str));
}
debug!("Running sh with args: {args_list:?}");
let output = Command::new("sh") let output = Command::new("sh")
.args(["-c", &self.cmd]) .args(&args_list)
.output() .output()
.await .await
.wrap_err("Failed to get script output")?; .wrap_err("Failed to get script output")?;
trace!("Script output with args: {output:?}");
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8(output.stdout) let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string()) .map(|output| output.trim().to_string())
.wrap_err("Script stdout not valid UTF-8")?; .wrap_err("Script stdout not valid UTF-8")?;
debug!("sending stdout: '{stdout}'");
Ok((OutputStream::Stdout(stdout), true)) Ok((OutputStream::Stdout(stdout), true))
} else { } else {
let stderr = String::from_utf8(output.stderr) let stderr = String::from_utf8(output.stderr)
.map(|output| output.trim().to_string()) .map(|output| output.trim().to_string())
.wrap_err("Script stderr not valid UTF-8")?; .wrap_err("Script stderr not valid UTF-8")?;
debug!("sending stderr: '{stderr}'");
Ok((OutputStream::Stderr(stderr), false)) Ok((OutputStream::Stderr(stderr), false))
} }
} }
/// Spawns a long-running process.
/// Returns a `mpsc::Receiver` that sends a message
/// every time a new line is written to `stdout` or `stderr`.
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> { pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
let mut handle = Command::new("sh") let mut handle = Command::new("sh")
.args(["-c", &self.cmd]) .args(["-c", &self.cmd])
@@ -240,6 +272,9 @@ impl Script {
.stdin(Stdio::null()) .stdin(Stdio::null())
.spawn()?; .spawn()?;
debug!("Spawned a long-running process for '{}'", self.cmd);
trace!("Handle: {:?}", handle);
let mut stdout_lines = BufReader::new( let mut stdout_lines = BufReader::new(
handle handle
.stdout .stdout
@@ -263,9 +298,11 @@ impl Script {
select! { select! {
_ = handle.wait() => break, _ = handle.wait() => break,
Ok(Some(line)) = stdout_lines.next_line() => { Ok(Some(line)) = stdout_lines.next_line() => {
debug!("sending stdout line: '{line}'");
send_async!(tx, OutputStream::Stdout(line)); send_async!(tx, OutputStream::Stdout(line));
} }
Ok(Some(line)) = stderr_lines.next_line() => { Ok(Some(line)) = stderr_lines.next_line() => {
debug!("sending stderr line: '{line}'");
send_async!(tx, OutputStream::Stderr(line)); send_async!(tx, OutputStream::Stderr(line));
} }
} }
@@ -274,6 +311,27 @@ impl Script {
Ok(rx) Ok(rx)
} }
/// Executes the script in oneshot mode,
/// meaning it is not awaited and output cannot be captured.
///
/// If the script errors, this is logged.
///
/// This has some overhead,
/// as the script has to be cloned to the thread.
///
pub fn run_as_oneshot(&self, args: Option<&[String]>) {
let script = self.clone();
let args = args.map(<[String]>::to_vec);
spawn(async move {
match script.get_output(args.as_deref()).await {
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
Err(err) => error!("{err:?}"),
_ => {}
}
});
}
} }
#[cfg(test)] #[cfg(test)]