Compare commits
61 Commits
feat/volum
...
refactor/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c73585324c | ||
|
|
0e3102de8c | ||
|
|
ad3c171eca | ||
|
|
e5bc44168f | ||
|
|
cc62927f15 | ||
|
|
76e2b7ba3e | ||
|
|
033d0f7e6e | ||
|
|
dc16b1e15a | ||
|
|
03cd263095 | ||
|
|
db0868a3fc | ||
|
|
0382b50cf4 | ||
|
|
338f5a0e1b | ||
|
|
20949a7744 | ||
|
|
2da28b9bf5 | ||
|
|
618e97f1e8 | ||
|
|
dd7c9f30db | ||
|
|
1fa0c0e977 | ||
|
|
74d18aedfb | ||
|
|
2c88c99cb6 | ||
|
|
236bb09170 | ||
|
|
83f44fd92f | ||
|
|
1855416db4 | ||
|
|
e63509a3a7 | ||
|
|
4a09b70854 | ||
|
|
9d09855fce | ||
|
|
e9d0273176 | ||
|
|
7926bb07eb | ||
|
|
6fd69d657c | ||
|
|
27d11de661 | ||
|
|
07df51c249 | ||
|
|
b038e7671a | ||
|
|
e5ab9f33b5 | ||
|
|
68bc8230dd | ||
|
|
246313136f | ||
|
|
15a9d8d42c | ||
|
|
a87d8d5c30 | ||
|
|
8e99fd4d0f | ||
|
|
1e1d65ae49 | ||
|
|
2815cef440 | ||
|
|
138b5b3903 | ||
|
|
7355db74ec | ||
|
|
c214f65ecb | ||
|
|
3d308ab572 | ||
|
|
b770ae716c | ||
|
|
3613aef5c5 | ||
|
|
a9d1233909 | ||
|
|
72b14b6c4e | ||
|
|
910945306c | ||
|
|
dfe1964abf | ||
|
|
e928b30f99 | ||
|
|
2ab06f044e | ||
|
|
4b4f1ffc21 | ||
|
|
0691db3b87 | ||
|
|
cac064f479 | ||
|
|
6c622864b3 | ||
|
|
55c06c4766 | ||
|
|
1b0287becc | ||
|
|
7bf44ca75d | ||
|
|
fb04ceab7d | ||
|
|
102d2478a9 | ||
|
|
80a414ab67 |
38
CHANGELOG.md
38
CHANGELOG.md
@@ -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
|
||||||
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ironbar"
|
name = "ironbar"
|
||||||
version = "0.10.0"
|
version = "0.11.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,11 +14,11 @@ 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+json" = ["universal-config/json"]
|
"config+json" = ["universal-config/json"]
|
||||||
@@ -39,10 +39,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"]
|
||||||
@@ -50,9 +46,9 @@ workspaces = ["futures-util"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# core
|
# core
|
||||||
gtk = "0.17.0"
|
gtk = { package = "gtk4", version = "0.6.6" }
|
||||||
gtk-layer-shell = "0.6.0"
|
gtk-layer-shell = { package = "gtk4-layer-shell", version = "0.0.3" }
|
||||||
glib = "0.17.5"
|
glib = "0.17.9"
|
||||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
@@ -62,13 +58,13 @@ 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.29.5"
|
||||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||||
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
|
||||||
universal-config = { version = "0.2.1", default_features = false }
|
universal-config = { version = "0.3.0", default_features = false }
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
@@ -88,18 +84,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
|
||||||
|
|||||||
@@ -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,30 @@ 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. |
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
|
||||||
|
|
||||||
|
| 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}}`. |
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
- [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)
|
||||||
|
|||||||
@@ -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. |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
There are many widget types, each with their own config options.
|
||||||
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------|
|
You can think of these like HTML elements and their attributes.
|
||||||
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. |
|
|
||||||
| `name` | `string` | `null` | Widget name. |
|
|
||||||
| `class` | `string` | `null` | Widget class name. |
|
|
||||||
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
|
|
||||||
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
|
|
||||||
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
|
|
||||||
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
|
|
||||||
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
|
|
||||||
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
|
|
||||||
|
|
||||||
### Labels
|
Every widget has the following options available; `type` is mandatory.
|
||||||
|
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
||||||
|
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
||||||
|
| `name` | `string` | `null` | Widget name. |
|
||||||
|
| `class` | `string` | `null` | Widget class name. |
|
||||||
|
|
||||||
|
#### Box
|
||||||
|
|
||||||
|
A container to place nested widgets inside.
|
||||||
|
|
||||||
|
> Type: `box`
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------|
|
||||||
|
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. |
|
||||||
|
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||||
|
|
||||||
|
#### Label
|
||||||
|
|
||||||
|
A text label. Pango markup 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,27 +356,32 @@ end:
|
|||||||
|
|
||||||
```corn
|
```corn
|
||||||
let {
|
let {
|
||||||
|
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||||
|
|
||||||
|
$popup = {
|
||||||
|
type = "box"
|
||||||
|
orientation = "vertical"
|
||||||
|
widgets = [
|
||||||
|
{ type = "label" name = "header" label = "Power menu" }
|
||||||
|
{
|
||||||
|
type = "box"
|
||||||
|
widgets = [
|
||||||
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||||
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
$power_menu = {
|
$power_menu = {
|
||||||
type = "custom"
|
type = "custom"
|
||||||
class = "power-menu"
|
class = "power-menu"
|
||||||
|
|
||||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
bar = [ $button ]
|
||||||
|
popup = [ $popup ]
|
||||||
|
|
||||||
popup = [ {
|
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
type = "box"
|
|
||||||
orientation = "vertical"
|
|
||||||
widgets = [
|
|
||||||
{ type = "label" name = "header" label = "Power menu" }
|
|
||||||
{
|
|
||||||
type = "box"
|
|
||||||
widgets = [
|
|
||||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
|
||||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
|
||||||
]
|
|
||||||
} ]
|
|
||||||
}
|
}
|
||||||
} in {
|
} in {
|
||||||
end = [ $power_menu ]
|
end = [ $power_menu ]
|
||||||
@@ -269,7 +392,9 @@ 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 the `name` and `class` attributes, then target them using `#name` and `.class`.
|
||||||
|
|
||||||
|
The following top-level selector is always available:
|
||||||
|
|
||||||
| Selector | Description |
|
| Selector | Description |
|
||||||
|-----------|-------------------------|
|
|-----------|-------------------------|
|
||||||
|
|||||||
70
docs/modules/Label.md
Normal file
70
docs/modules/Label.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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 |
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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. |
|
||||||
|
|
||||||
|
|||||||
80
docs/modules/Upower.md
Normal file
80
docs/modules/Upower.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
Displays system power information such as the battery percentage, and estimated time to empty.
|
||||||
|
|
||||||
|
`TODO: ADD SCREENSHOT`
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
> Type: `upower`
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|----------|----------|-----------------|---------------------------------------------------|
|
||||||
|
| `format` | `string` | `{percentage}%` | Format string to use for the widget button label. |
|
||||||
|
|
||||||
|
<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` | Clock popup box. |
|
||||||
|
| `#popup-upower #upower-details` | Label inside the popup. |
|
||||||
@@ -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. |
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,3 +116,7 @@ favorites = [
|
|||||||
'Steam',
|
'Steam',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[start]]
|
||||||
|
label = 'random num: {{500:echo $RANDOM}}'
|
||||||
|
type = 'label'
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@@ -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;
|
||||||
|
|||||||
@@ -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
5
scripts/generate-examples.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
corn examples/config.corn -t json > examples/config.json
|
||||||
|
corn examples/config.corn -t toml > examples/config.toml
|
||||||
|
corn examples/config.corn -t yaml > examples/config.yaml
|
||||||
264
src/bar.rs
264
src/bar.rs
@@ -1,18 +1,14 @@
|
|||||||
use crate::bridge_channel::BridgeChannel;
|
use crate::config::{BarPosition, MarginConfig, ModuleConfig};
|
||||||
use crate::config::{BarPosition, CommonConfig, MarginConfig, ModuleConfig};
|
use crate::modules::{create_module, wrap_widget, ModuleInfo, ModuleLocation};
|
||||||
use crate::dynamic_string::DynamicString;
|
|
||||||
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 glib::signal::Inhibit;
|
||||||
use tokio::sync::mpsc;
|
use tracing::{debug, info};
|
||||||
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.
|
||||||
@@ -53,16 +49,16 @@ pub fn create_bar(
|
|||||||
let center = create_container("center", orientation);
|
let center = create_container("center", orientation);
|
||||||
let end = create_container("end", orientation);
|
let end = create_container("end", orientation);
|
||||||
|
|
||||||
content.add(&start);
|
content.append(&start);
|
||||||
content.set_center_widget(Some(¢er));
|
content.set_center_widget(Some(¢er));
|
||||||
content.pack_end(&end, false, false, 0);
|
content.pack_end(&end, false, false, 0);
|
||||||
|
|
||||||
load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||||
win.add(&content);
|
win.append(&content);
|
||||||
|
|
||||||
win.connect_destroy_event(|_, _| {
|
win.connect_destroy_event(|_, _| {
|
||||||
info!("Shutting down");
|
info!("Shutting down");
|
||||||
gtk::main_quit();
|
// gtk::main_quit();
|
||||||
Inhibit(false)
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,17 +164,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 +182,23 @@ 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: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
|
fn add_modules(
|
||||||
let popup = Popup::new(info);
|
content: >k::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 = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
|
||||||
|
let container = wrap_widget(&widget, common, orientation);
|
||||||
let container = wrap_widget(&widget);
|
content.append(&container);
|
||||||
content.add(&container);
|
|
||||||
setup_module_common_options(container, common);
|
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +210,7 @@ fn add_modules(content: >k::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 +219,8 @@ fn add_modules(content: >k::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 +228,3 @@ fn add_modules(content: >k::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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ 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 mut cache = lock!(cache);
|
||||||
let removed_id = cache
|
let removed_id = lock!(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));
|
||||||
@@ -131,8 +131,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();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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
40
src/clients/upower.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use async_once::AsyncOnce;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use upower_dbus::UPowerProxy;
|
||||||
|
use zbus::fdo::PropertiesProxy;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref DISPLAY_PROXY: AsyncOnce<Arc<PropertiesProxy<'static>>> = AsyncOnce::new(async {
|
||||||
|
let dbus = zbus::Connection::system()
|
||||||
|
.await
|
||||||
|
.expect("failed to create connection to system bus");
|
||||||
|
|
||||||
|
let device_proxy = UPowerProxy::new(&dbus)
|
||||||
|
.await
|
||||||
|
.expect("failed to create upower proxy");
|
||||||
|
|
||||||
|
let display_device = device_proxy
|
||||||
|
.get_display_device()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
|
||||||
|
|
||||||
|
let path = display_device.path().to_owned();
|
||||||
|
|
||||||
|
let proxy = PropertiesProxy::builder(&dbus)
|
||||||
|
.destination("org.freedesktop.UPower")
|
||||||
|
.expect("failed to set proxy destination address")
|
||||||
|
.path(path)
|
||||||
|
.expect("failed to set proxy path")
|
||||||
|
.cache_properties(zbus::CacheProperties::No)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.expect("failed to build proxy");
|
||||||
|
|
||||||
|
Arc::new(proxy)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_display_proxy() -> &'static PropertiesProxy<'static> {
|
||||||
|
DISPLAY_PROXY.get().await
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#[cfg(feature = "volume+pulse")]
|
|
||||||
pub mod pulse_bak;
|
|
||||||
// #[cfg(feature = "volume+pulse")]
|
|
||||||
// pub mod pulse;
|
|
||||||
|
|
||||||
trait VolumeClient {
|
|
||||||
// TODO: Write
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
// }
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
|
||||||
162
src/config/common.rs
Normal file
162
src/config/common.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
use glib::signal::Inhibit;
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::script::{Script, ScriptInput};
|
||||||
|
use crate::send;
|
||||||
|
use gtk::gdk::ScrollDirection;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{GestureClick, Orientation, Revealer, RevealerTransitionType, Widget};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
/// Common configuration options
|
||||||
|
/// which can be set on every module.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct CommonConfig {
|
||||||
|
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 fn to_revealer_transition_type(&self, orientation: Orientation) -> RevealerTransitionType {
|
||||||
|
match (self, orientation) {
|
||||||
|
(TransitionType::SlideStart, Orientation::Horizontal) => {
|
||||||
|
RevealerTransitionType::SlideLeft
|
||||||
|
}
|
||||||
|
(TransitionType::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
|
||||||
|
(TransitionType::SlideEnd, Orientation::Horizontal) => {
|
||||||
|
RevealerTransitionType::SlideRight
|
||||||
|
}
|
||||||
|
(TransitionType::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
|
||||||
|
(TransitionType::Crossfade, _) => RevealerTransitionType::Crossfade,
|
||||||
|
_ => RevealerTransitionType::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommonConfig {
|
||||||
|
/// Configures the module's container according to the common config options.
|
||||||
|
pub fn install<W: IsA<Widget>>(mut self, widget: &W, revealer: &Revealer) {
|
||||||
|
self.install_show_if(widget, 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);
|
||||||
|
|
||||||
|
let gesture = GestureClick::new();
|
||||||
|
|
||||||
|
gesture.connect_pressed(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||||
|
let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
|
||||||
|
|
||||||
|
widget.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| {
|
||||||
|
widget.$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 = widget.clone();
|
||||||
|
DynamicString::new(&tooltip, move |string| {
|
||||||
|
container.set_tooltip_text(Some(&string));
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_show_if<W: IsA<Widget>>(&mut self, widget: &W, revealer: &Revealer) {
|
||||||
|
self.show_if.take().map_or_else(
|
||||||
|
|| {
|
||||||
|
widget.set_visible(true)
|
||||||
|
},
|
||||||
|
|show_if| {
|
||||||
|
let script = Script::new_polling(show_if);
|
||||||
|
let widget = widget.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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +51,7 @@ 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);
|
||||||
|
|
||||||
@@ -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)]
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
|
|||||||
let image = Image::new();
|
let image = Image::new();
|
||||||
image.set_widget_name("image");
|
image.set_widget_name("image");
|
||||||
|
|
||||||
container.add(&image);
|
container.append(&image);
|
||||||
|
|
||||||
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
|
||||||
.and_then(|provider| provider.load_into_image(image))
|
.and_then(|provider| provider.load_into_image(image))
|
||||||
@@ -49,7 +49,7 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
|
|||||||
let label = Label::new(Some(input));
|
let label = Label::new(Some(input));
|
||||||
label.set_widget_name("label");
|
label.set_widget_name("label");
|
||||||
|
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
container
|
container
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ impl<'a> ImageProvider<'a> {
|
|||||||
|
|
||||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
fn load_into_image_sync(&self, image: >k::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,11 +158,12 @@ 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 =
|
||||||
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
|
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||||
None => Ok(None),
|
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
|
||||||
}?;
|
None => Ok(None),
|
||||||
|
}?;
|
||||||
|
|
||||||
pixbuf.map_or_else(
|
pixbuf.map_or_else(
|
||||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||||
@@ -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?;
|
||||||
Ok(glib::Bytes::from_owned(bytes))
|
|
||||||
|
let status = res.status();
|
||||||
|
if status.is_success() {
|
||||||
|
let bytes = res.bytes().await?;
|
||||||
|
Ok(glib::Bytes::from_owned(bytes))
|
||||||
|
} else {
|
||||||
|
Err(Report::msg(format!(
|
||||||
|
"Received non-success HTTP code ({status})"
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -1,3 +1,5 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
mod bar;
|
mod bar;
|
||||||
mod bridge_channel;
|
mod bridge_channel;
|
||||||
mod clients;
|
mod clients;
|
||||||
@@ -19,7 +21,7 @@ use crate::style::load_css;
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
use gtk::gdk::Display;
|
use gtk::gdk::{Display, Monitor};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Application;
|
use gtk::Application;
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -44,8 +46,6 @@ async fn main() -> Result<()> {
|
|||||||
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();
|
||||||
@@ -60,10 +60,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,
|
||||||
@@ -120,7 +120,10 @@ fn create_bars(
|
|||||||
debug!("Received {} outputs from Wayland", outputs.len());
|
debug!("Received {} outputs from Wayland", outputs.len());
|
||||||
debug!("Outputs: {:?}", outputs);
|
debug!("Outputs: {:?}", outputs);
|
||||||
|
|
||||||
let num_monitors = display.n_monitors();
|
for monitor in display.monitors().iter::<Monitor>() {
|
||||||
|
let monitor = monitor.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
for i in 0..num_monitors {
|
for i in 0..num_monitors {
|
||||||
let monitor = display
|
let monitor = display
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ use crate::try_send;
|
|||||||
use gtk::gdk_pixbuf::Pixbuf;
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
|
use gtk::{Button, Image, Label, Orientation, RadioButton, Widget};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
@@ -21,6 +22,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 +39,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
|
||||||
}
|
}
|
||||||
@@ -120,11 +128,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));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,10 +162,10 @@ impl Module<Button> for ClipboardModule {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
||||||
container.add(&entries);
|
container.append(&entries);
|
||||||
|
|
||||||
let hidden_option = RadioButton::new();
|
let hidden_option = RadioButton::new();
|
||||||
entries.add(&hidden_option);
|
entries.append(&hidden_option);
|
||||||
|
|
||||||
let mut items = HashMap::new();
|
let mut items = HashMap::new();
|
||||||
|
|
||||||
@@ -176,7 +184,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
let button = RadioButton::from_widget(&hidden_option);
|
let button = RadioButton::from_widget(&hidden_option);
|
||||||
|
|
||||||
let label = Label::new(Some(value));
|
let label = Label::new(Some(value));
|
||||||
button.add(&label);
|
button.append(&label);
|
||||||
|
|
||||||
if let Some(truncate) = self.truncate {
|
if let Some(truncate) = self.truncate {
|
||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
@@ -211,7 +219,7 @@ impl Module<Button> for ClipboardModule {
|
|||||||
button.set_active(true); // if just added, should be on clipboard
|
button.set_active(true); // if just added, should be on clipboard
|
||||||
|
|
||||||
let button_wrapper = EventBox::new();
|
let button_wrapper = EventBox::new();
|
||||||
button_wrapper.add(&button);
|
button_wrapper.append(&button);
|
||||||
|
|
||||||
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
||||||
button_wrapper.set_above_child(true);
|
button_wrapper.set_above_child(true);
|
||||||
@@ -254,12 +262,11 @@ impl Module<Button> for ClipboardModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
row.add(&button_wrapper);
|
row.append(&button_wrapper);
|
||||||
row.pack_end(&remove_button, false, false, 0);
|
row.pack_end(&remove_button, false, false, 0);
|
||||||
|
|
||||||
entries.add(&row);
|
entries.append(&row);
|
||||||
entries.reorder_child(&row, 0);
|
entries.reorder_child(&row, 0);
|
||||||
row.show_all();
|
|
||||||
|
|
||||||
items.insert(id, (row, button));
|
items.insert(id, (row, button));
|
||||||
}
|
}
|
||||||
@@ -293,7 +300,6 @@ impl Module<Button> for ClipboardModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
hidden_option.hide();
|
hidden_option.hide();
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ impl Module<Button> for ClockModule {
|
|||||||
let button = Button::new();
|
let button = Button::new();
|
||||||
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());
|
||||||
button.add(&label);
|
button.append(&label);
|
||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
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))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,10 +107,10 @@ impl Module<Button> for ClockModule {
|
|||||||
.build();
|
.build();
|
||||||
let format = "%H:%M:%S";
|
let format = "%H:%M:%S";
|
||||||
|
|
||||||
container.add(&clock);
|
container.append(&clock);
|
||||||
|
|
||||||
let calendar = Calendar::builder().name("calendar").build();
|
let calendar = Calendar::builder().name("calendar").build();
|
||||||
container.add(&calendar);
|
container.append(&calendar);
|
||||||
|
|
||||||
{
|
{
|
||||||
rx.attach(None, move |date| {
|
rx.attach(None, move |date| {
|
||||||
@@ -120,8 +120,6 @@ impl Module<Button> for ClockModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
Some(container)
|
Some(container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
use crate::config::CommonConfig;
|
|
||||||
use crate::dynamic_string::DynamicString;
|
|
||||||
use crate::image::ImageProvider;
|
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
|
||||||
use crate::popup::{ButtonGeometry, Popup};
|
|
||||||
use crate::script::Script;
|
|
||||||
use crate::{send_async, try_send};
|
|
||||||
use color_eyre::{Report, Result};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{Button, IconTheme, Label, Orientation};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::spawn;
|
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
|
||||||
use tracing::{debug, error};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct CustomModule {
|
|
||||||
/// Container class name
|
|
||||||
class: Option<String>,
|
|
||||||
/// Widgets to add to the bar container
|
|
||||||
bar: Vec<Widget>,
|
|
||||||
/// Widgets to add to the popup container
|
|
||||||
popup: Option<Vec<Widget>>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub common: Option<CommonConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to parse an `Orientation` from `String`
|
|
||||||
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
|
||||||
match orientation.to_lowercase().as_str() {
|
|
||||||
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
|
||||||
"vertical" | "v" => Ok(Orientation::Vertical),
|
|
||||||
_ => Err(Report::msg("Invalid orientation string in config")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Widget attributes
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct Widget {
|
|
||||||
/// Type of GTK widget to add
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
widget_type: WidgetType,
|
|
||||||
widgets: Option<Vec<Widget>>,
|
|
||||||
label: Option<String>,
|
|
||||||
name: Option<String>,
|
|
||||||
class: Option<String>,
|
|
||||||
on_click: Option<String>,
|
|
||||||
orientation: Option<String>,
|
|
||||||
src: Option<String>,
|
|
||||||
size: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supported GTK widget types
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum WidgetType {
|
|
||||||
Box,
|
|
||||||
Label,
|
|
||||||
Button,
|
|
||||||
Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
/// Creates this widget and adds it to the parent container
|
|
||||||
fn add_to(
|
|
||||||
self,
|
|
||||||
parent: >k::Box,
|
|
||||||
tx: Sender<ExecEvent>,
|
|
||||||
bar_orientation: Orientation,
|
|
||||||
icon_theme: &IconTheme,
|
|
||||||
) {
|
|
||||||
match self.widget_type {
|
|
||||||
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)),
|
|
||||||
WidgetType::Label => parent.add(&self.into_label()),
|
|
||||||
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
|
||||||
WidgetType::Image => parent.add(&self.into_image(icon_theme)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a `gtk::Box` from this widget
|
|
||||||
fn into_box(
|
|
||||||
self,
|
|
||||||
tx: &Sender<ExecEvent>,
|
|
||||||
bar_orientation: Orientation,
|
|
||||||
icon_theme: &IconTheme,
|
|
||||||
) -> gtk::Box {
|
|
||||||
let mut builder = gtk::Box::builder();
|
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
|
||||||
builder = builder.name(&name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(orientation) = self.orientation {
|
|
||||||
builder = builder
|
|
||||||
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = builder.build();
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
|
||||||
container.style_context().add_class(&class);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(widgets) = self.widgets {
|
|
||||||
for widget in widgets {
|
|
||||||
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a `gtk::Label` from this widget
|
|
||||||
fn into_label(self) -> Label {
|
|
||||||
let mut builder = Label::builder().use_markup(true);
|
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
|
||||||
builder = builder.name(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = builder.build();
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
|
||||||
label.style_context().add_class(&class);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = self.label.map_or_else(String::new, |text| text);
|
|
||||||
|
|
||||||
{
|
|
||||||
let label = label.clone();
|
|
||||||
DynamicString::new(&text, move |string| {
|
|
||||||
label.set_label(&string);
|
|
||||||
Continue(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
label
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a `gtk::Button` from this widget
|
|
||||||
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
|
|
||||||
let mut builder = Button::builder();
|
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
|
||||||
builder = builder.name(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = builder.build();
|
|
||||||
|
|
||||||
if let Some(text) = self.label {
|
|
||||||
let label = Label::new(None);
|
|
||||||
label.set_use_markup(true);
|
|
||||||
label.set_markup(&text);
|
|
||||||
button.add(&label);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
|
||||||
button.style_context().add_class(&class);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(exec) = self.on_click {
|
|
||||||
button.connect_clicked(move |button| {
|
|
||||||
try_send!(
|
|
||||||
tx,
|
|
||||||
ExecEvent {
|
|
||||||
cmd: exec.clone(),
|
|
||||||
geometry: Popup::button_pos(button, bar_orientation),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_image(self, icon_theme: &IconTheme) -> gtk::Image {
|
|
||||||
let mut builder = gtk::Image::builder();
|
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
|
||||||
builder = builder.name(&name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let gtk_image = builder.build();
|
|
||||||
|
|
||||||
if let Some(src) = self.src {
|
|
||||||
let size = self.size.unwrap_or(32);
|
|
||||||
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
|
|
||||||
.and_then(|image| image.load_into_image(gtk_image.clone()))
|
|
||||||
{
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
|
||||||
gtk_image.style_context().add_class(&class);
|
|
||||||
}
|
|
||||||
|
|
||||||
gtk_image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ExecEvent {
|
|
||||||
cmd: String,
|
|
||||||
geometry: ButtonGeometry,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module<gtk::Box> for CustomModule {
|
|
||||||
type SendMessage = ();
|
|
||||||
type ReceiveMessage = ExecEvent;
|
|
||||||
|
|
||||||
fn name() -> &'static str {
|
|
||||||
"custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_controller(
|
|
||||||
&self,
|
|
||||||
_info: &ModuleInfo,
|
|
||||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
|
||||||
mut rx: Receiver<Self::ReceiveMessage>,
|
|
||||||
) -> Result<()> {
|
|
||||||
spawn(async move {
|
|
||||||
while let Some(event) = rx.recv().await {
|
|
||||||
if event.cmd.starts_with('!') {
|
|
||||||
let script = Script::from(&event.cmd[1..]);
|
|
||||||
|
|
||||||
debug!("executing command: '{}'", script.cmd);
|
|
||||||
// TODO: Migrate to use script.run
|
|
||||||
if let Err(err) = script.get_output().await {
|
|
||||||
error!("{err:?}");
|
|
||||||
}
|
|
||||||
} else if event.cmd == "popup:toggle" {
|
|
||||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
|
|
||||||
} else if event.cmd == "popup:open" {
|
|
||||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
|
||||||
} else if event.cmd == "popup:close" {
|
|
||||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
|
||||||
} else {
|
|
||||||
error!("Received invalid command: '{}'", event.cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_widget(
|
|
||||||
self,
|
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
||||||
info: &ModuleInfo,
|
|
||||||
) -> Result<ModuleWidget<gtk::Box>> {
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
|
||||||
let container = gtk::Box::builder().orientation(orientation).build();
|
|
||||||
|
|
||||||
if let Some(ref class) = self.class {
|
|
||||||
container.style_context().add_class(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.bar.clone().into_iter().for_each(|widget| {
|
|
||||||
widget.add_to(
|
|
||||||
&container,
|
|
||||||
context.controller_tx.clone(),
|
|
||||||
orientation,
|
|
||||||
info.icon_theme,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
|
||||||
|
|
||||||
Ok(ModuleWidget {
|
|
||||||
widget: container,
|
|
||||||
popup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_popup(
|
|
||||||
self,
|
|
||||||
tx: Sender<Self::ReceiveMessage>,
|
|
||||||
_rx: glib::Receiver<Self::SendMessage>,
|
|
||||||
info: &ModuleInfo,
|
|
||||||
) -> Option<gtk::Box>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let container = gtk::Box::builder().name("popup-custom").build();
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
|
||||||
container
|
|
||||||
.style_context()
|
|
||||||
.add_class(format!("popup-{class}").as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(popup) = self.popup {
|
|
||||||
for widget in popup {
|
|
||||||
widget.add_to(
|
|
||||||
&container,
|
|
||||||
tx.clone(),
|
|
||||||
Orientation::Horizontal,
|
|
||||||
info.icon_theme,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
Some(container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/modules/custom/box.rs
Normal file
36
src/modules/custom/box.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::build;
|
||||||
|
use crate::modules::custom::WidgetConfig;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::Orientation;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct BoxWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
orientation: Option<String>,
|
||||||
|
widgets: Option<Vec<WidgetConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for BoxWidget {
|
||||||
|
type Widget = gtk::Box;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let container = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
if let Some(orientation) = self.orientation {
|
||||||
|
container.set_orientation(
|
||||||
|
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(widgets) = self.widgets {
|
||||||
|
for widget in widgets {
|
||||||
|
widget.widget.add_to(&container, context, widget.common);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/modules/custom/button.rs
Normal file
52
src/modules/custom/button.rs
Normal 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.append(&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
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/modules/custom/image.rs
Normal file
47
src/modules/custom/image.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/modules/custom/label.rs
Normal file
33
src/modules/custom/label.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/modules/custom/mod.rs
Normal file
257
src/modules/custom/mod.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
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 {
|
||||||
|
/// Container class name
|
||||||
|
class: Option<String>,
|
||||||
|
/// 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: >k::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();
|
||||||
|
|
||||||
|
if let Some(ref class) = self.class {
|
||||||
|
container.style_context().add_class(class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/modules/custom/progress.rs
Normal file
80
src/modules/custom/progress.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/modules/custom/slider.rs
Normal file
129
src/modules/custom/slider.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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 glib::signal::Inhibit;
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,8 +104,8 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.add(&icon);
|
container.append(&icon);
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
|
|
||||||
{
|
{
|
||||||
let icon_theme = icon_theme.clone();
|
let icon_theme = icon_theme.clone();
|
||||||
|
|||||||
62
src/modules/label.rs
Normal file
62
src/modules/label.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use gtk::{Button, IconTheme, Orientation};
|
|||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -136,27 +137,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(>k_image));
|
button.set_image(Some(>k_image));
|
||||||
@@ -217,7 +225,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);
|
||||||
@@ -227,12 +235,10 @@ impl ItemButton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
button.show_all();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
button,
|
button,
|
||||||
persistent: item.favorite,
|
persistent: item.favorite,
|
||||||
show_names,
|
show_names: appearance.show_names,
|
||||||
menu_state,
|
menu_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use self::open_state::OpenState;
|
|||||||
use crate::clients::wayland::{self, ToplevelChange};
|
use crate::clients::wayland::{self, ToplevelChange};
|
||||||
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, read_lock, try_send, write_lock};
|
||||||
use color_eyre::{Help, Report};
|
use color_eyre::{Help, Report};
|
||||||
@@ -33,10 +34,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
|
||||||
@@ -318,8 +326,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,15 +347,14 @@ 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,
|
||||||
);
|
);
|
||||||
|
|
||||||
container.add(&button.button);
|
container.append(&button.button);
|
||||||
buttons.insert(item.app_id, button);
|
buttons.insert(item.app_id, button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,7 +433,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
// 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");
|
||||||
placeholder.set_width_request(MAX_WIDTH);
|
placeholder.set_width_request(MAX_WIDTH);
|
||||||
container.add(&placeholder);
|
container.append(&placeholder);
|
||||||
|
|
||||||
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
||||||
|
|
||||||
@@ -513,10 +525,9 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
if let Some(buttons) = buttons.get(&app_id) {
|
if let Some(buttons) = buttons.get(&app_id) {
|
||||||
for (_, button) in buttons {
|
for (_, button) in buttons {
|
||||||
button.style_context().add_class("popup-item");
|
button.style_context().add_class("popup-item");
|
||||||
container.add(button);
|
container.append(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
container.set_width_request(MAX_WIDTH);
|
container.set_width_request(MAX_WIDTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,154 @@ 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<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.
|
||||||
|
pub fn wrap_widget<W: IsA<Widget>>(
|
||||||
|
widget: &W,
|
||||||
|
common: CommonConfig,
|
||||||
|
orientation: Orientation,
|
||||||
|
) -> EventBox {
|
||||||
|
let revealer = Revealer::builder()
|
||||||
|
.transition_type(
|
||||||
|
common
|
||||||
|
.transition_type
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&TransitionType::SlideStart)
|
||||||
|
.to_revealer_transition_type(orientation),
|
||||||
|
)
|
||||||
|
.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.append(&revealer);
|
||||||
|
|
||||||
|
common.install(&container, &revealer);
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use regex::Regex;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -155,10 +156,10 @@ 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);
|
||||||
button.add(&button_contents);
|
button.append(&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());
|
||||||
@@ -167,9 +168,9 @@ impl Module<Button> for MusicModule {
|
|||||||
truncate.truncate_label(&label);
|
truncate.truncate_label(&label);
|
||||||
}
|
}
|
||||||
|
|
||||||
button_contents.add(&icon_pause);
|
button_contents.append(&icon_pause);
|
||||||
button_contents.add(&icon_play);
|
button_contents.append(&icon_play);
|
||||||
button_contents.add(&label);
|
button_contents.append(&label);
|
||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
@@ -179,7 +180,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 +193,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);
|
||||||
@@ -255,30 +262,30 @@ impl Module<Button> for MusicModule {
|
|||||||
album_label.container.set_widget_name("album");
|
album_label.container.set_widget_name("album");
|
||||||
artist_label.container.set_widget_name("artist");
|
artist_label.container.set_widget_name("artist");
|
||||||
|
|
||||||
info_box.add(&title_label.container);
|
info_box.append(&title_label.container);
|
||||||
info_box.add(&album_label.container);
|
info_box.append(&album_label.container);
|
||||||
info_box.add(&artist_label.container);
|
info_box.append(&artist_label.container);
|
||||||
|
|
||||||
let controls_box = gtk::Box::builder().name("controls").build();
|
let controls_box = gtk::Box::builder().name("controls").build();
|
||||||
|
|
||||||
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");
|
btn_prev.set_widget_name("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");
|
btn_play.set_widget_name("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");
|
btn_pause.set_widget_name("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");
|
btn_next.set_widget_name("btn-next");
|
||||||
|
|
||||||
controls_box.add(&btn_prev);
|
controls_box.append(&btn_prev);
|
||||||
controls_box.add(&btn_play);
|
controls_box.append(&btn_play);
|
||||||
controls_box.add(&btn_pause);
|
controls_box.append(&btn_pause);
|
||||||
controls_box.add(&btn_next);
|
controls_box.append(&btn_next);
|
||||||
|
|
||||||
info_box.add(&controls_box);
|
info_box.append(&controls_box);
|
||||||
|
|
||||||
let volume_box = gtk::Box::builder()
|
let volume_box = gtk::Box::builder()
|
||||||
.orientation(Orientation::Vertical)
|
.orientation(Orientation::Vertical)
|
||||||
@@ -290,15 +297,15 @@ impl Module<Button> for MusicModule {
|
|||||||
volume_slider.set_inverted(true);
|
volume_slider.set_inverted(true);
|
||||||
volume_slider.set_widget_name("slider");
|
volume_slider.set_widget_name("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");
|
volume_icon.style_context().add_class("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);
|
||||||
|
|
||||||
container.add(&album_image);
|
container.append(&album_image);
|
||||||
container.add(&info_box);
|
container.append(&info_box);
|
||||||
container.add(&volume_box);
|
container.append(&volume_box);
|
||||||
|
|
||||||
let tx_prev = tx.clone();
|
let tx_prev = tx.clone();
|
||||||
btn_prev.connect_clicked(move |_| {
|
btn_prev.connect_clicked(move |_| {
|
||||||
@@ -326,10 +333,9 @@ impl Module<Button> for MusicModule {
|
|||||||
Inhibit(false)
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
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 +344,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,14 +457,14 @@ 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");
|
icon.style_context().add_class("icon");
|
||||||
label.style_context().add_class("label");
|
label.style_context().add_class("label");
|
||||||
|
|
||||||
container.add(&icon);
|
container.append(&icon);
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
|
|
||||||
Self { label, container }
|
Self { label, container }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||||||
.name("item")
|
.name("item")
|
||||||
.build();
|
.build();
|
||||||
label.set_angle(info.bar_position.get_angle());
|
label.set_angle(info.bar_position.get_angle());
|
||||||
container.add(&label);
|
container.append(&label);
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 +24,9 @@ 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| {
|
item.icon_theme_path.as_ref().and_then(|path| {
|
||||||
let theme = IconTheme::new();
|
let theme = IconTheme::new();
|
||||||
theme.append_search_path(path);
|
theme.append_search_path(path);
|
||||||
@@ -34,6 +38,37 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
/// for the provided submenu array.
|
/// for the provided submenu array.
|
||||||
fn get_menu_items(
|
fn get_menu_items(
|
||||||
@@ -56,7 +91,7 @@ fn get_menu_items(
|
|||||||
let menu = Menu::new();
|
let menu = Menu::new();
|
||||||
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
|
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|item| menu.add(item));
|
.for_each(|item| menu.append(item));
|
||||||
|
|
||||||
builder = builder.submenu(&menu);
|
builder = builder.submenu(&menu);
|
||||||
}
|
}
|
||||||
@@ -147,14 +182,26 @@ 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) {
|
|
||||||
image.set_widget_name(address.as_str());
|
get_image_from_icon_name(&item)
|
||||||
menu_item.add(&image);
|
.or_else(|| get_image_from_pixmap(&item))
|
||||||
}
|
.map_or_else(
|
||||||
container.add(&menu_item);
|
|| {
|
||||||
|
let label =
|
||||||
|
Label::new(Some(item.title.as_ref().unwrap_or(addr)));
|
||||||
|
menu_item.add(&label);
|
||||||
|
},
|
||||||
|
|image| {
|
||||||
|
image.set_widget_name(address.as_str());
|
||||||
|
menu_item.append(&image);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
container.append(&menu_item);
|
||||||
menu_item.show_all();
|
menu_item.show_all();
|
||||||
menu_item
|
menu_item
|
||||||
});
|
});
|
||||||
@@ -169,7 +216,7 @@ impl Module<MenuBar> for TrayModule {
|
|||||||
&menu_path,
|
&menu_path,
|
||||||
)
|
)
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|item| menu.add(item));
|
.for_each(|item| menu.append(item));
|
||||||
menu_item.set_submenu(Some(&menu));
|
menu_item.set_submenu(Some(&menu));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
281
src/modules/upower.rs
Normal file
281
src/modules/upower.rs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
use crate::clients::upower::get_display_proxy;
|
||||||
|
use crate::config::CommonConfig;
|
||||||
|
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::builder().name("icon").build();
|
||||||
|
|
||||||
|
let label = Label::builder()
|
||||||
|
.label(&self.format)
|
||||||
|
.use_markup(true)
|
||||||
|
.name("label")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let container = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.name("upower")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let button = Button::builder().name("button").build();
|
||||||
|
|
||||||
|
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)
|
||||||
|
.name("popup-upower")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let label = Label::builder().name("upower-details").build();
|
||||||
|
container.add(&label);
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,9 +183,10 @@ 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.append(&item);
|
||||||
|
|
||||||
button_map.insert(workspace.name, item);
|
button_map.insert(workspace.name, item);
|
||||||
}
|
}
|
||||||
@@ -186,7 +196,6 @@ impl Module<gtk::Box> for WorkspacesModule {
|
|||||||
reorder_workspaces(&container);
|
reorder_workspaces(&container);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.show_all();
|
|
||||||
has_initialized = true;
|
has_initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,10 +218,11 @@ 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.append(&item);
|
||||||
if self.sort == SortOrder::Alphanumeric {
|
if self.sort == SortOrder::Alphanumeric {
|
||||||
reorder_workspaces(&container);
|
reorder_workspaces(&container);
|
||||||
}
|
}
|
||||||
@@ -233,10 +243,11 @@ 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.append(&item);
|
||||||
|
|
||||||
if self.sort == SortOrder::Alphanumeric {
|
if self.sort == SortOrder::Alphanumeric {
|
||||||
reorder_workspaces(&container);
|
reorder_workspaces(&container);
|
||||||
|
|||||||
48
src/popup.rs
48
src/popup.rs
@@ -1,10 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use glib::signal::Inhibit;
|
||||||
|
|
||||||
use crate::config::BarPosition;
|
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 +20,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 +35,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(
|
||||||
@@ -121,7 +122,7 @@ impl Popup {
|
|||||||
|
|
||||||
if let Some(content) = self.cache.get(&key) {
|
if let Some(content) = self.cache.get(&key) {
|
||||||
content.style_context().add_class("popup");
|
content.style_context().add_class("popup");
|
||||||
self.window.add(content);
|
self.window.append(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +134,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 +151,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 +191,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 +209,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,
|
||||||
|
|||||||
130
src/script.rs
130
src/script.rs
@@ -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' => {
|
|
||||||
let interval_str = chars
|
|
||||||
.iter()
|
|
||||||
.take_while(|c| c.is_ascii_digit())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let interval = interval_str.parse::<u64>().unwrap_or_else(|_| {
|
if matches!(char, 'p' | 'w') {
|
||||||
warn!("Received invalid interval in script string. Falling back to default `5000ms`.");
|
let mode_str = chars.iter().take_while(|&c| c != &':').collect::<String>();
|
||||||
5000
|
let len = mode_str.len();
|
||||||
});
|
|
||||||
(ScriptInputToken::Interval(interval), interval_str.len())
|
let token = ScriptMode::try_parse(&mode_str).ok();
|
||||||
|
token.map(|token| (ScriptInputToken::Mode(token), len))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// watching or polling
|
CurrentToken::Interval => {
|
||||||
'w' | 'p' => {
|
current_state = CurrentToken::Cmd;
|
||||||
let mode_str = chars.iter().take_while(|&c| c != &':').collect::<String>();
|
|
||||||
let len = mode_str.len();
|
|
||||||
|
|
||||||
let token = ScriptMode::try_parse(&mode_str)
|
if char.is_ascii_digit() {
|
||||||
.map_or(ScriptInputToken::Cmd(mode_str), |mode| {
|
let interval_str = chars
|
||||||
ScriptInputToken::Mode(mode)
|
.iter()
|
||||||
});
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.collect::<String>();
|
||||||
|
let len = interval_str.len();
|
||||||
|
|
||||||
(token, len)
|
let token = interval_str.parse::<u64>().ok();
|
||||||
|
token.map(|token| (ScriptInputToken::Interval(token), len))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
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))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tokens.push(token);
|
if let Some((token, skip)) = parse_res {
|
||||||
chars.drain(..skip);
|
tokens.push(token);
|
||||||
|
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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user