1 Commits

Author SHA1 Message Date
Abdallah Gamal
d8e9bdea83 fix: not resolving flatpak application icons 2023-06-29 19:55:24 +01:00
81 changed files with 2312 additions and 4027 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use flake

View File

@@ -73,6 +73,4 @@ jobs:
name: jakestanger name: jakestanger
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build --print-build-logs - run: nix build --print-build-logs

View File

@@ -4,58 +4,6 @@ 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.13.0] - 2023-08-16
### :sparkles: New Features
- [`c3e9654`](https://github.com/JakeStanger/ironbar/commit/c3e9654cd3dfcea4276f5b114112b7541dd847fd) - **upower**: icon size option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f5bdc5a`](https://github.com/JakeStanger/ironbar/commit/f5bdc5a0272fefca4c91336699ea63913cdf3c08) - ipc server and cli *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ded50cc`](https://github.com/JakeStanger/ironbar/commit/ded50cca6f01f08a8e44257394fdde634d421e8e) - support for 'ironvar' dynamic variables *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c6319b7`](https://github.com/JakeStanger/ironbar/commit/c6319b78fd3992ad43327e90b6326ab653238f2e) - **ipc**: support for injecting additional stylesheets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`27f920d`](https://github.com/JakeStanger/ironbar/commit/27f920d01217bedba279003291ad48c1aaa56bb0) - **launcher**: slightly improve focus logic when clicking item with multiple windows *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`bd90167`](https://github.com/JakeStanger/ironbar/commit/bd90167f4ea90cb97984b9f3b5e6f65b375d0c4a) - **clock**: format option for popup header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`12053f1`](https://github.com/JakeStanger/ironbar/commit/12053f111a6f05a59e33396b9042821b98b9bc5c) - **music**: progress/seek bar in popup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7d3bb02`](https://github.com/JakeStanger/ironbar/commit/7d3bb02b4612f278bcc8a268a48c61b239c63e82) - **ipc**: reload config command *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`b310ea7`](https://github.com/JakeStanger/ironbar/commit/b310ea76362bcdf10e187d6b00cd2b8ed2870c41) - **clock**: localization support *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`738b9e3`](https://github.com/JakeStanger/ironbar/commit/738b9e3da714c9b998030e9f60b9b6f50c62ec76) - **config**: use default fallback with config instructions *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2ccb263`](https://github.com/JakeStanger/ironbar/commit/2ccb2633c6c4d7f6eb264a6c49951853b728c9f3) - IPC for get_visible, set_visible, new bar `name` config option *(commit by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja))*
- [`b7ee794`](https://github.com/JakeStanger/ironbar/commit/b7ee794bfc86730e7921c8a930cf8d8bb44474ad) - **ipc**: commands for opening/closing popups *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`ef443e6`](https://github.com/JakeStanger/ironbar/commit/ef443e6978949479388129760dabc3f8930c0b0f) - **image resolver**: add fallback image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`9f65cf2`](https://github.com/JakeStanger/ironbar/commit/9f65cf293d9527a2c536847f0005957421a9be33) - **workspaces**: add `favorites` and `hidden` options *(commit by [@yavko](https://github.com/yavko))*
- [`19c684e`](https://github.com/JakeStanger/ironbar/commit/19c684e49facb57e3e2acf9cafecf177c2e1bfbf) - **nix**: automatic development environment with direnv *(commit by [@yavko](https://github.com/yavko))*
### :bug: Bug Fixes
- [`6db7742`](https://github.com/JakeStanger/ironbar/commit/6db7742e068dc03d67dbf35e0d9db27f900392fe) - crash on startup introduced by recent refactors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`f78c7f9`](https://github.com/JakeStanger/ironbar/commit/f78c7f9b981c98676e855ff6a63e33a51927c709) - not resolving flatpak application icons *(commit by [@body20002](https://github.com/body20002))*
- [`1759945`](https://github.com/JakeStanger/ironbar/commit/1759945912e376581e5fcd5ed2916f89e2090f2b) - **music**: correctly show/hide popup elements based on player capabilities *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`a9ac29d`](https://github.com/JakeStanger/ironbar/commit/a9ac29d8857256d13e14104db235117e3c752972) - clipboard partially behind wrong feature flag *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c711dd8`](https://github.com/JakeStanger/ironbar/commit/c711dd858554140bcfb6df515a43b40ee2baee67) - failing to resolve icons with home_manager *(commit by [@christoph00](https://github.com/christoph00))*
- [`1a272e0`](https://github.com/JakeStanger/ironbar/commit/1a272e00fbeca4b5e39b527ffed439bc51fd4080) - **label**: not using markup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4ca17d1`](https://github.com/JakeStanger/ironbar/commit/4ca17d1337acfbbc21c04058d97f689a1cce60a6) - **launcher**: incorrectly resolving some applications *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`eee2182`](https://github.com/JakeStanger/ironbar/commit/eee2182ab90fdc22cd05da9417cbee17e4c67088) - **ipc**: command/response casing *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`c582bc3`](https://github.com/JakeStanger/ironbar/commit/c582bc33905702a9ebe323e6dfa9413485f48fb7) - **cli**: `set-visible` command causing panic *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`87dd764`](https://github.com/JakeStanger/ironbar/commit/87dd7646fc9223ac7e67842934f3bd104b4eea80) - **launcher**: not clearing focused state when closing window *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`6f57ad4`](https://github.com/JakeStanger/ironbar/commit/6f57ad47ac30348c0ae2b7dba35d5ffdbf96f72d) - **launcher**: not setting focus state when opening favourite *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2367faa`](https://github.com/JakeStanger/ironbar/commit/2367faab0440327620052de845c6a0d3032f2f05) - **image**: using fallback in places it shouldn't *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7f6fef6`](https://github.com/JakeStanger/ironbar/commit/7f6fef6338d7a8c909f3224b32426dfc2aacc295) - **image**: matching desktop file names too eagerly *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`89ec06f`](https://github.com/JakeStanger/ironbar/commit/89ec06fc7b252052f96e45f5d0f6d6504878a13a) - **music**: hide album art widget when no image *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`2902331`](https://github.com/JakeStanger/ironbar/commit/2902331af00f2e52fdea06964fbd89d72fe2ebbb) - **dynamic string**: incorrectly handling strings containing multipoint utf-8 chars *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`901a86c`](https://github.com/JakeStanger/ironbar/commit/901a86caa491648268f0618e17a25b978552db0c) - **custom**: crash when clicking non-popup button *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`54f0f23`](https://github.com/JakeStanger/ironbar/commit/54f0f232f208602699e5021942c3d0f3947ca6de) - **launcher**: popup not closing when hover leaves widget *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :recycle: Refactors
- [`d121dc3`](https://github.com/JakeStanger/ironbar/commit/d121dc3d1e9468a67deb528c35fc3897c3840f77) - fix unused var warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`cc181a8`](https://github.com/JakeStanger/ironbar/commit/cc181a8b6d0ac1cccd4ed2f9f420c138ed5383d2) - fix new clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`7016f7f`](https://github.com/JakeStanger/ironbar/commit/7016f7f79e7e29a3318b535ba224aa78bd91688a) - use new smart pointer macros throughout codebase *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`06251e2`](https://github.com/JakeStanger/ironbar/commit/06251e293e8f56e1897fed80335f114fdea57183) - fix new pedantic clippy warnings *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`36f3db7`](https://github.com/JakeStanger/ironbar/commit/36f3db741178b959070ee90bcd6448e1b2a6ef26) - **image**: do not try to read desktop files where definitely not necessary *(commit by [@JakeStanger](https://github.com/JakeStanger))*
### :memo: Documentation Changes
- [`aea8de2`](https://github.com/JakeStanger/ironbar/commit/aea8de25522e5f5e7f92f46a6248eb2e79cb457e) - update CHANGELOG.md for v0.12.1 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`607c728`](https://github.com/JakeStanger/ironbar/commit/607c7285d7e01265a8c8417e2941b2099e61aa42) - update for ipc/cli, tidy a bit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4b88079`](https://github.com/JakeStanger/ironbar/commit/4b88079561e5c9fec63480afe56a1f89c76dc094) - fix header *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`4620f29`](https://github.com/JakeStanger/ironbar/commit/4620f29d381394aef8b241b03009ef8c3b8d0145) - **examples**: update stylesheet *(commit by [@JakeStanger](https://github.com/JakeStanger))*
- [`3d94987`](https://github.com/JakeStanger/ironbar/commit/3d949874de90b0e5c06cb62726629133d0ea76e3) - **ipc**: add link to luajit library *(commit by [@JakeStanger](https://github.com/JakeStanger))*
## [v0.12.1] - 2023-06-18 ## [v0.12.1] - 2023-06-18
### :boom: BREAKING CHANGES ### :boom: BREAKING CHANGES
- due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*: - due to [`e11177f`](https://github.com/JakeStanger/ironbar/commit/e11177fea3095560057278d71cebca01bed295d6) - add sensible class names for icon labels *(commit by [@JakeStanger](https://github.com/JakeStanger))*:
@@ -425,4 +373,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0 [v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0 [v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1 [v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0

1381
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.14.0-pre" version = "0.13.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar" description = "Customisable GTK Layer Shell wlroots/sway bar"
repository = "https://github.com/jakestanger/ironbar" repository = "https://github.com/jakestanger/ironbar"
categories = ["gui"]
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features] [features]
default = [ default = [
@@ -52,7 +50,7 @@ music = ["regex"]
sys_info = ["sysinfo", "regex"] sys_info = ["sysinfo", "regex"]
tray = ["system-tray"] tray = ["stray"]
upower = ["upower_dbus", "zbus", "futures-lite"] upower = ["upower_dbus", "zbus", "futures-lite"]
@@ -63,10 +61,10 @@ workspaces = ["futures-util"]
[dependencies] [dependencies]
# core # core
gtk = "0.18.1" gtk = "0.17.0"
gtk-layer-shell = "0.8.0" gtk-layer-shell = "0.6.0"
glib = "0.18.5" glib = "0.17.10"
tokio = { version = "1.35.1", features = [ tokio = { version = "1.28.2", features = [
"macros", "macros",
"rt-multi-thread", "rt-multi-thread",
"time", "time",
@@ -75,66 +73,69 @@ tokio = { version = "1.35.1", features = [
"io-util", "io-util",
"net", "net",
] } ] }
tracing = "0.1.40" tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-appender = "0.2.3" tracing-appender = "0.2.2"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2" color-eyre = "0.6.2"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
indexmap = "2.1.0" indexmap = "2.0.0"
dirs = "5.0.1" dirs = "5.0.1"
walkdir = "2.4.0" walkdir = "2.3.2"
notify = { version = "6.1.1", default-features = false } notify = { version = "6.0.1", default-features = false }
wayland-client = "0.31.1" wayland-client = "0.30.2"
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] } wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
smithay-client-toolkit = { version = "0.18.0", default-features = false, features = [ smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
"calloop", "calloop",
] } ] }
universal-config = { version = "0.4.3", default_features = false } universal-config = { version = "0.4.0", default_features = false }
ctrlc = "3.4.2" ctrlc = "3.4.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
async_once = "0.2.6" async_once = "0.2.6"
cfg-if = "1.0.0" cfg-if = "1.0.0"
# cli # cli
clap = { version = "4.4.12", optional = true, features = ["derive"] } clap = { version = "4.2.7", optional = true, features = ["derive"] }
# ipc # ipc
serde_json = { version = "1.0.109", optional = true } serde_json = { version = "1.0.96", optional = true }
# http # http
reqwest = { version = "0.11.23", optional = true } reqwest = { version = "0.11.18", optional = true }
# clipboard # clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] } nix = { version = "0.26.2", optional = true, features = ["event"] }
# clock # clock
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] } chrono = { version = "0.4.26", optional = true }
# music # music
mpd_client = { version = "1.3.0", optional = true } mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.1", optional = true } mpris = { version = "2.0.1", optional = true }
# sys_info # sys_info
sysinfo = { version = "0.29.11", optional = true } sysinfo = { version = "0.29.2", optional = true }
# tray # tray
system-tray = { version = "0.1.4", optional = true } stray = { version = "0.1.3", optional = true }
# upower # upower
upower_dbus = { version = "0.3.2", optional = true } upower_dbus = { version = "0.3.2", optional = true }
futures-lite = { version = "2.1.0", optional = true } futures-lite = { version = "1.12.0", optional = true }
zbus = { version = "3.14.1", optional = true } zbus = { version = "3.13.1", optional = true }
# workspaces # workspaces
swayipc-async = { version = "2.0.1", optional = true } swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.12", features = ["silent"], optional = true } hyprland = { version = "=0.3.1", optional = true }
futures-util = { version = "0.3.30", optional = true } futures-util = { version = "0.3.21", optional = true }
# shared # shared
regex = { version = "1.10.2", default-features = false, features = [ regex = { version = "1.8.4", default-features = false, features = [
"std", "std",
], optional = true } # music, sys_info ], optional = true } # music, sys_info
[patch.crates-io]
stray = { git = "https://github.com/jakestanger/stray", branch = "fix/connection-errors" }

View File

@@ -75,15 +75,7 @@ cargo install ironbar
yay -S ironbar-git yay -S ironbar-git
``` ```
### Nix ### Nix Flake
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
```sh
nix-shell -p ironbar
```
#### Flake
A flake is included with the repo which can be used with Home Manager. A flake is included with the repo which can be used with Home Manager.

View File

@@ -18,8 +18,6 @@ You also need rust; only the latest stable version is supported.
```shell ```shell
pacman -S gtk3 gtk-layer-shell pacman -S gtk3 gtk-layer-shell
# for http support
pacman -S openssl
``` ```
### Ubuntu/Debian ### Ubuntu/Debian
@@ -33,9 +31,7 @@ apt install libssl-dev
### Fedora ### Fedora
```shell ```shell
dnf install gtk3-devel gtk-layer-shell-devel dnf install gtk3 gtk-layer-shell
# for http support
dnf install openssl-devel
``` ```
## Features ## Features

View File

@@ -95,11 +95,7 @@ Create a map/object called `monitors` inside the top-level object.
Each of the map's keys should be an output name, Each of the map's keys should be an output name,
and each value should be an object containing the bar config. and each value should be an object containing the bar config.
You can still define a top-level "default" config to use for unspecified monitors. To find your output names, run `wayland-info | grep wl_output -A1`.
Alternatively, leave the top-level `start`, `center` and `end` keys null to hide bars on unspecified monitors.
> [!TIP]
> To find your output names, run `wayland-info | grep wl_output -A1`.
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>
@@ -272,8 +268,7 @@ Check [here](config) for an example config file for a fully configured bar in ea
The following table lists each of the top-level bar config options: The following table lists each of the top-level bar config options:
| Name | Type | Default | Description | | Name | Type | Default | Description |
|--------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------| |--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. |
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | | `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. |
@@ -284,8 +279,6 @@ The following table lists each of the top-level bar config options:
| `margin.right` | `integer` | `0` | The margin on the right of the bar | | `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | | `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. | | `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
| `start` | `Module[]` | `[]` | Array of left or top modules. | | `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. | | `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. | | `end` | `Module[]` | `[]` | Array of right or bottom modules. |

View File

@@ -31,12 +31,6 @@ Commands and responses are sent as JSON objects, denoted by their `type` key.
The message buffer is currently limited to `1024` bytes. The message buffer is currently limited to `1024` bytes.
Particularly large messages will be truncated or cause an error. Particularly large messages will be truncated or cause an error.
The full spec can be found below.
## Libraries
- [Luajit](https://github.com/A-Cloud-Ninja/ironbar-ipc-luajit) - Maintained by [@A-Cloud-Ninja](https://github.com/A-Cloud-Ninja)
## Commands ## Commands
### `ping` ### `ping`
@@ -63,20 +57,6 @@ Responds with `ok`.
} }
``` ```
### `reload`
Restarts the bars, reloading the config in the process.
The IPC server and main GTK application are untouched.
Responds with `ok`.
```json
{
"type": "reload"
}
```
### `get` ### `get`
Gets an [ironvar](ironvars) value. Gets an [ironvar](ironvars) value.
@@ -117,76 +97,6 @@ Responds with `ok` if the stylesheet exists, otherwise `error`.
} }
``` ```
### `set_visible`
Sets a bar's visibility.
Responds with `ok` if the bar exists, otherwise `error`.
```json
{
"type": "set_visible",
"bar_name": "bar-123",
"visible": true
}
```
### `get_visible`
Gets a bar's visibility.
Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, otherwise `error`.
```json
{
"type": "get_visible",
"bar_name": "bar-123"
}
```
### `toggle_popup`
Toggles the open/closed state for a module's popup.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "toggle_popup",
"bar_name": "bar-123",
"name": "clock"
}
```
### `open_popup`
Sets a module's popup open, regardless of its current state.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "open_popup",
"bar_name": "bar-123",
"name": "clock"
}
```
### `close_popup`
Sets the popup on a bar closed, regardless of which module it is open for.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "close_popup",
"bar_name": "bar-123"
}
```
## Responses ## Responses
### `ok` ### `ok`

View File

@@ -1,7 +1,7 @@
In some configuration locations, Ironbar supports dynamic values, In some configuration locations, Ironbar supports dynamic values,
meaning you can inject content into the bar from an external source. meaning you can inject content into the bar from an external source.
Currently two dynamic content sources are supported - [scripts](scripts) (via shorthand syntax) and [ironvars](ironvars). Currently two dynamic content sources are supported - scripts and ironvars.
## Dynamic String ## Dynamic String

View File

@@ -11,33 +11,24 @@ The below table describes the selectors provided by the bar itself.
Information on styling individual modules can be found on their pages in the sidebar. Information on styling individual modules can be found on their pages in the sidebar.
| Selector | Description | | Selector | Description |
|---------------------|--------------------------------------------| |----------------|--------------------------------------------|
| `.background` | Top-level window. | | `.background` | Top-level window. |
| `#bar` | Bar root box. | | `#bar` | Bar root box. |
| `#bar #start` | Bar left or top modules container box. | | `#bar #start` | Bar left or top modules container box. |
| `#bar #center` | Bar center modules container box. | | `#bar #center` | Bar center modules container box. |
| `#bar #end` | Bar right or bottom modules container box. | | `#bar #end` | Bar right or bottom modules container box. |
| `.container` | All of the above. | | `.container` | All of the above. |
| `.widget-container` | The `EventBox` wrapping any widget. |
| `.widget` | Any widget. |
| `.popup` | Any popup box. | | `.popup` | Any popup box. |
Every Ironbar widget can be selected using a `kebab-case` class name matching its name. Every widget can be selected using a `kebab-case` class name matching its name.
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively. You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
Setting the `name` option on a widget allows you to target that specific instance using `#name`. Setting the `name` option on a widget allows you to target that specific instance using `#name`.
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`). You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
You can also target all GTK widgets of a certain type directly using their name. For example, `label` will select all labels, and `button:hover` will select the hover state on *all* buttons. You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
These names are all lower case with no separator, so `MenuBar` -> `menubar`. These names are all lower case with no separator, so `MenuBar` -> `menubar`.
> [!NOTE]
> If an entry takes no effect you might have to use a more specific selector.
> For example, attempting to set text size on `.popup-clipboard .item` will likely have no effect.
> Instead, you can target the more specific `.popup-clipboard .item label`.
Running `ironbar inspect` can be used to find out how to address an element.
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours: GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
```css ```css

View File

@@ -9,12 +9,8 @@ Clicking on the widget opens a popup with the time and a calendar.
> Type: `clock` > Type: `clock`
| Name | Type | Default | Description | | Name | Type | Default | Description |
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------| |----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. | | `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
| `format_popup` | `string` | `%H:%M:%S` | Date/time format string to display in the popup header. |
| `locale` | `string` | `$LC_TIME` or `$LANG` or `'POSIX'` | Locale to use (eg `en_GB`). Defaults to the system language (reading from env var). |
> Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html>
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>

View File

@@ -21,12 +21,12 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | | `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. | | `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. | | `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
| `icons.prev` | `string` or [image](images) | `󰒮` | Icon to show on previous button. | | `icons.prev` | `string` or [image](images) | `` | Icon to show on previous button. |
| `icons.next` | `string` or [image](images) | `󰒭` | Icon to show on next button. | | `icons.next` | `string` or [image](images) | `` | Icon to show on next button. |
| `icons.volume` | `string` or [image](images) | `󰕾` | Icon to show under popup volume slider. | | `icons.volume` | `string` or [image](images) | `` | Icon to show under popup volume slider. |
| `icons.track` | `string` or [image](images) | `󰎈` | Icon to show next to track title. | | `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
| `icons.album` | `string` or [image](images) | `󰀥` | Icon to show next to album name. | | `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
| `icons.artist` | `string` or [image](images) | `󰠃` | Icon to show next to artist name. | | `icons.artist` | `string` or [image](images) | `` | Icon to show next to artist name. |
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. | | `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | | `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. | | `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
@@ -128,6 +128,8 @@ and will be replaced with values from the currently playing track:
| `{track}` | Track number | | `{track}` | Track number |
| `{disc}` | Disc number | | `{disc}` | Disc number |
| `{genre}` | Genre | | `{genre}` | Genre |
| `{duration}` | Duration in `mm:ss` |
| `{elapsed}` | Time elapsed in `mm:ss` |
## Styling ## Styling
@@ -164,10 +166,7 @@ and will be replaced with values from the currently playing track:
| `.popup-music .controls .btn-pause` | Pause button inside popup box | | `.popup-music .controls .btn-pause` | Pause button inside popup box |
| `.popup-music .controls .btn-next` | Next button inside popup box | | `.popup-music .controls .btn-next` | Next button inside popup box |
| `.popup-music .volume` | Volume container inside popup box | | `.popup-music .volume` | Volume container inside popup box |
| `.popup-music .volume .slider` | Slider inside volume container | | `.popup-music .volume .slider` | Volume slider popup box |
| `.popup-music .volume .icon` | Icon inside volume container | | `.popup-music .volume .icon` | Volume icon label inside popup box |
| `.popup-music .progress` | Progress (seek) bar container |
| `.popup-music .progress .slider` | Slider inside progress container |
| `.popup-music .progress .label` | Duration label inside progress container |
For more information on styling, please see the [styling guide](styling-guide). For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -28,13 +28,13 @@ Pango markup is supported.
"end": [ "end": [
{ {
"format": [ "format": [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", " {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)", " {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)", "| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", " {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", " {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}", " {load_average:1} | {load_average:5} | {load_average:15}",
"󰥔 {uptime}" " {uptime}"
], ],
"interval": { "interval": {
"cpu": 1, "cpu": 1,
@@ -58,13 +58,13 @@ Pango markup is supported.
[[end]] [[end]]
type = 'sys_info' type = 'sys_info'
format = [ format = [
' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C', ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
' {memory_used} / {memory_total} GB ({memory_percent}%)', ' {memory_used} / {memory_total} GB ({memory_percent}%)',
'| {swap_used} / {swap_total} GB ({swap_percent}%)', '| {swap_used} / {swap_total} GB ({swap_percent}%)',
'󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)', ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
'󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps', ' {net_down:enp39s0} / {net_up:enp39s0} Mbps',
'󰖡 {load_average:1} | {load_average:5} | {load_average:15}', ' {load_average:1} | {load_average:5} | {load_average:15}',
'󰥔 {uptime}', ' {uptime}',
] ]
[end.interval] [end.interval]
@@ -85,13 +85,13 @@ temps = 5
```yaml ```yaml
end: end:
- format: - format:
- ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C' - ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C'
- ' {memory_used} / {memory_total} GB ({memory_percent}%)' - ' {memory_used} / {memory_total} GB ({memory_percent}%)'
- '| {swap_used} / {swap_total} GB ({swap_percent}%)' - '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- '󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)' - ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
- '󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps' - ' {net_down:enp39s0} / {net_up:enp39s0} Mbps'
- '󰖡 {load_average:1} | {load_average:5} | {load_average:15}' - ' {load_average:1} | {load_average:5} | {load_average:15}'
- '󰥔 {uptime}' - ' {uptime}'
interval: interval:
cpu: 1 cpu: 1
disks: 300 disks: 300
@@ -119,13 +119,13 @@ end:
interval.networks = 3 interval.networks = 3
format = [ format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C" " {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)" " {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)" "| {swap_used} / {swap_total} GB ({swap_percent}%)"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" " {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" " {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}" " {load_average:1} | {load_average:5} | {load_average:15}"
"󰥔 {uptime}" " {uptime}"
] ]
} }
] ]
@@ -168,7 +168,7 @@ The following tokens can be used in the `format` configuration option:
| `{load_average:15}` | 15-minute load average. | | `{load_average:15}` | 15-minute load average. |
| `{uptime}` | System uptime formatted as `HH:mm`. | | `{uptime}` | System uptime formatted as `HH:mm`. |
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp-Tccd1`. For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
## Styling ## Styling

View File

@@ -9,10 +9,8 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces` > Type: `workspaces`
| Name | Type | Default | Description | | Name | Type | Default | Description |
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | | `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | | `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. |
@@ -30,7 +28,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
"2": "", "2": "",
"3": "" "3": ""
}, },
"favorites": ["1", "2", "3"],
"all_monitors": false "all_monitors": false
} }
] ]
@@ -46,7 +43,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
[[end]] [[end]]
type = "workspaces" type = "workspaces"
all_monitors = false all_monitors = false
favorites = ["1", "2", "3"]
[[end.name_map]] [[end.name_map]]
1 = "" 1 = ""
@@ -67,10 +63,6 @@ end:
1: "" 1: ""
2: "" 2: ""
3: "" 3: ""
favorites:
- "1"
- "2"
- "3"
all_monitors: false all_monitors: false
``` ```
@@ -87,7 +79,6 @@ end:
name_map.1 = "" name_map.1 = ""
name_map.2 = "" name_map.2 = ""
name_map.3 = "" name_map.3 = ""
favorites = [ "1" "2" "3" ]
all_monitors = false all_monitors = false
} }
] ]
@@ -103,8 +94,6 @@ end:
| `.workspaces` | Workspaces widget box | | `.workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button | | `.workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) | | `.workspaces .item.focused` | Workspace button (workspace focused) |
| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) |
| `.workspaces .item.inactive` | Workspace button (favourite, not currently open)
| `.workspaces .item .icon` | Workspace button icon (any type) | | `.workspaces .item .icon` | Workspace button icon (any type) |
| `.workspaces .item .text-icon` | Workspace button icon (textual only) | | `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) | | `.workspaces .item .image` | Workspace button icon (image only) |

View File

@@ -3,7 +3,7 @@ let {
type = "workspaces" type = "workspaces"
all_monitors = false all_monitors = false
name_map = { name_map = {
1 = "󰙯" 1 = ""
2 = "icon:firefox" 2 = "icon:firefox"
3 = "" 3 = ""
Games = "icon:steam" Games = "icon:steam"
@@ -46,10 +46,10 @@ let {
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C" " {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)" " {memory_used} / {memory_total} GB ({memory_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)" "| {swap_used} / {swap_total} GB ({swap_percent}%)"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" " {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" " {net_down:enp39s0} / {net_up:enp39s0} Mbps"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}" " {load_average:1} | {load_average:5} | {load_average:15}"
"󰥔 {uptime}" " {uptime}"
] ]
} }
@@ -67,7 +67,7 @@ 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 FIXME}}" } $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" }

View File

@@ -29,10 +29,10 @@
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C", " {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)", " {memory_used} / {memory_total} GB ({memory_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)", "| {swap_used} / {swap_total} GB ({swap_percent}%)",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", " {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", " {net_down:enp39s0} / {net_up:enp39s0} Mbps",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}", " {load_average:1} | {load_average:5} | {load_average:15}",
"󰥔 {uptime}" " {uptime}"
], ],
"interval": { "interval": {
"cpu": 1, "cpu": 1,
@@ -109,7 +109,7 @@
{ {
"all_monitors": false, "all_monitors": false,
"name_map": { "name_map": {
"1": "󰙯", "1": "",
"2": "icon:firefox", "2": "icon:firefox",
"3": "", "3": "",
"Code": "", "Code": "",
@@ -128,7 +128,7 @@
"type": "launcher" "type": "launcher"
}, },
{ {
"label": "random num: {{500:echo FIXME}}", "label": "random num: {{500:echo $RANDOM}}",
"type": "label" "type": "label"
} }
] ]

View File

@@ -1,41 +1,41 @@
anchor_to_edges = true anchor_to_edges = true
icon_theme = "Paper" icon_theme = 'Paper'
position = "bottom" position = 'bottom'
[[end]] [[end]]
music_dir = "/home/jake/Music" music_dir = '/home/jake/Music'
player_type = "mpd" player_type = 'mpd'
type = "music" type = 'music'
[end.truncate] [end.truncate]
max_length = 100 max_length = 100
mode = "end" mode = 'end'
[[end]] [[end]]
host = "chloe:6600" host = 'chloe:6600'
player_type = "mpd" player_type = 'mpd'
truncate = "end" truncate = 'end'
type = "music" type = 'music'
[[end]] [[end]]
cmd = "/home/jake/bin/phone-battery" cmd = '/home/jake/bin/phone-battery'
type = "script" type = 'script'
[end.show_if] [end.show_if]
cmd = "/home/jake/bin/phone-connected" cmd = '/home/jake/bin/phone-connected'
interval = 500 interval = 500
[[end]] [[end]]
type = 'sys_info'
format = [ format = [
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C", ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
" {memory_used} / {memory_total} GB ({memory_percent}%)", ' {memory_used} / {memory_total} GB ({memory_percent}%)',
"| {swap_used} / {swap_total} GB ({swap_percent}%)", '| {swap_used} / {swap_total} GB ({swap_percent}%)',
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", '李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}", '猪 {load_average:1} | {load_average:5} | {load_average:15}',
"󰥔 {uptime}", ' {uptime}',
] ]
type = "sys_info"
[end.interval] [end.interval]
cpu = 1 cpu = 1
@@ -46,77 +46,77 @@ temps = 5
[[end]] [[end]]
max_items = 3 max_items = 3
type = "clipboard" type = 'clipboard'
[end.truncate] [end.truncate]
length = 50 length = 50
mode = "end" mode = 'end'
[[end]] [[end]]
class = "power-menu" class = 'power-menu'
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
type = "custom" type = 'custom'
[[end.bar]] [[end.bar]]
label = "" label = ''
name = "power-btn" name = 'power-btn'
on_click = "popup:toggle" on_click = 'popup:toggle'
type = "button" type = 'button'
[[end.popup]] [[end.popup]]
orientation = "vertical" orientation = 'vertical'
type = "box" type = 'box'
[[end.popup.widgets]] [[end.popup.widgets]]
label = "Power menu" label = 'Power menu'
name = "header" name = 'header'
type = "label" type = 'label'
[[end.popup.widgets]] [[end.popup.widgets]]
type = "box" type = 'box'
[[end.popup.widgets.widgets]] [[end.popup.widgets.widgets]]
class = "power-btn" class = 'power-btn'
label = "<span font-size='40pt'></span>" label = '''<span font-size='40pt'></span>'''
on_click = "!shutdown now" on_click = '!shutdown now'
type = "button" type = 'button'
[[end.popup.widgets.widgets]] [[end.popup.widgets.widgets]]
class = "power-btn" class = 'power-btn'
label = "<span font-size='40pt'></span>" label = '''<span font-size='40pt'></span>'''
on_click = "!reboot" on_click = '!reboot'
type = "button" type = 'button'
[[end.popup.widgets]] [[end.popup.widgets]]
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = "uptime" name = 'uptime'
type = "label" type = 'label'
[[end]] [[end]]
type = "clock" type = 'clock'
[[start]] [[start]]
all_monitors = false all_monitors = false
type = "workspaces" type = 'workspaces'
[start.name_map] [start.name_map]
1 = "󰙯" 1 = 'ﭮ'
2 = "icon:firefox" 2 = 'icon:firefox'
3 = "" 3 = ''
Code = "" Code = ''
Games = "icon:steam" Games = 'icon:steam'
[[start]] [[start]]
favorites = [
"firefox",
"discord",
"steam",
]
show_icons = true show_icons = true
show_names = false show_names = false
type = "launcher" type = 'launcher'
favorites = [
'firefox',
'discord',
'steam',
]
[[start]] [[start]]
label = "random num: {{500:echo FIXME}}" label = 'random num: {{500:echo $RANDOM}}'
type = "label" type = 'label'

View File

@@ -19,10 +19,10 @@ end:
-  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C -  {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%) -  {memory_used} / {memory_total} GB ({memory_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)' - '| {swap_used} / {swap_total} GB ({swap_percent}%)'
- 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%) - {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
- 󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps - {net_down:enp39s0} / {net_up:enp39s0} Mbps
- 󰖡 {load_average:1} | {load_average:5} | {load_average:15} - {load_average:1} | {load_average:5} | {load_average:15}
- 󰥔 {uptime} - {uptime}
interval: interval:
cpu: 1 cpu: 1
disks: 300 disks: 300
@@ -69,7 +69,7 @@ position: bottom
start: start:
- all_monitors: false - all_monitors: false
name_map: name_map:
'1': 󰙯 '1':
'2': icon:firefox '2': icon:firefox
'3': '3':
Code: Code:
@@ -82,6 +82,6 @@ start:
show_icons: true show_icons: true
show_names: false show_names: false
type: launcher type: launcher
- label: 'random num: {{500:echo FIXME}}' - label: 'random num: {{500:echo $RANDOM}}'
type: label type: label

View File

@@ -120,11 +120,7 @@ button:hover {
margin-right: 1em; margin-right: 1em;
} }
.popup-music .icon-box { .popup-music .title .icon *, .popup-music .title .label {
margin-right: 0.4em;
}
.popup-music .title .icon, .popup-music .title .label {
font-size: 1.7em; font-size: 1.7em;
} }
@@ -132,17 +128,15 @@ button:hover {
color: @color_border; color: @color_border;
} }
.popup-music .volume .slider slider { .popup-music .volume scale slider {
border-radius: 100%; border-radius: 100%;
} }
.popup-music .volume .icon { /* volume icon */
margin-left: 4px; .popup-music .volume > box:last-child label {
margin-left: 6px;
} }
.popup-music .progress .slider slider {
border-radius: 100%;
}
/* -- script -- */ /* -- script -- */

68
flake.lock generated
View File

@@ -1,25 +1,5 @@
{ {
"nodes": { "nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1703439018,
"narHash": "sha256-VT+06ft/x3eMZ1MJxWzQP3zXFGcrxGo5VR2rB7t88hs=",
"owner": "ipetkov",
"repo": "crane",
"rev": "afdcd41180e3dfe4dac46b5ee396e3b12ccc967a",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@@ -38,45 +18,13 @@
"type": "github" "type": "github"
} }
}, },
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1704008649, "lastModified": 1686960236,
"narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=", "narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1703637592,
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", "rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -88,9 +36,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane", "nixpkgs": "nixpkgs",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
}, },
@@ -102,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1703902408, "lastModified": 1686968542,
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=", "narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681", "rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -6,18 +6,11 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
rust-overlay, rust-overlay,
crane,
naersk,
... ...
}: let }: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
@@ -34,18 +27,10 @@
rust-overlay.overlays.default rust-overlay.overlays.default
]; ];
}; };
mkRustToolchain = pkgs: mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
in { in {
overlays.default = final: prev: let overlays.default = final: prev: let
rust = mkRustToolchain final; rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
rustPlatform = prev.makeRustPlatform { rustPlatform = prev.makeRustPlatform {
cargo = rust; cargo = rust;
@@ -57,32 +42,10 @@
(builtins.substring 4 2 longDate) (builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate) (builtins.substring 6 2 longDate)
]); ]);
builder = "naersk";
in { in {
ironbar = let ironbar = prev.callPackage ./nix/default.nix {
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in
if builder == "crane"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform; inherit rustPlatform;
builderName = builder;
builder = craneLib;
}
else if builder == "naersk"
then
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
builder = naersk';
}
else
prev.callPackage ./nix/default.nix {
inherit version;
inherit rustPlatform;
builderName = builder;
}; };
}; };
packages = genSystems ( packages = genSystems (
@@ -119,14 +82,6 @@
gtk-layer-shell gtk-layer-shell
pkg-config pkg-config
openssl openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
]; ];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
@@ -193,8 +148,8 @@
ExecStart = "${pkg}/bin/ironbar"; ExecStart = "${pkg}/bin/ironbar";
}; };
Install.WantedBy = [ Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target") (lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target") (lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
]; ];
}; };
}; };

View File

@@ -19,16 +19,24 @@
lib, lib,
version ? "git", version ? "git",
features ? [], features ? [],
builderName ? "nix", }:
builder ? {}, rustPlatform.buildRustPackage rec {
}: let
basePkg = rec {
inherit version; inherit version;
pname = "ironbar"; pname = "ironbar";
src = builtins.path { src = builtins.path {
name = "ironbar"; name = "ironbar";
path = lib.cleanSource ../.; path = lib.cleanSource ../.;
}; };
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {
lockFile = ../Cargo.lock;
};
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection]; nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl]; buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
propagatedBuildInputs = [ propagatedBuildInputs = [
@@ -56,40 +64,4 @@
platforms = platforms.linux; platforms = platforms.linux;
mainProgram = "ironbar"; mainProgram = "ironbar";
}; };
}; }
flags = let
noDefault =
if features == []
then ""
else "--no-default-features";
featuresStr =
if features == []
then ""
else ''-F "${builtins.concatStringsSep "," features}"'';
in [noDefault featuresStr];
in
if builderName == "naersk"
then
builder.buildPackage (basePkg
// {
cargoOptions = old: old ++ flags;
})
else if builderName == "crane"
then
builder.buildPackage (basePkg
// {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg
// {
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})

View File

@@ -1,17 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
cargo
clippy
rustfmt
gtk3
gtk-layer-shell
gcc
openssl
];
nativeBuildInputs = with pkgs; [
pkg-config
];
}

View File

@@ -3,57 +3,34 @@ use crate::modules::{
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation, create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
}; };
use crate::popup::Popup; use crate::popup::Popup;
use crate::{Config, Ironbar}; use crate::unique_id::get_unique_usize;
use crate::Config;
use color_eyre::Result; use color_eyre::Result;
use glib::Propagation;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType}; use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use gtk_layer_shell::LayerShell; use std::sync::{Arc, RwLock};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use tracing::{debug, info}; use tracing::{debug, info};
#[derive(Debug, Clone)] /// Creates a new window for a bar,
enum Inner { /// sets it up and adds its widgets.
New { config: Option<Config> }, pub fn create_bar(
Loaded { popup: Rc<RefCell<Popup>> }, app: &Application,
} monitor: &Monitor,
monitor_name: &str,
config: Config,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
#[derive(Debug, Clone)] setup_layer_shell(
pub struct Bar { &win,
name: String, monitor,
monitor_name: String, config.position,
position: BarPosition, config.anchor_to_edges,
config.margin,
);
window: ApplicationWindow, let orientation = config.position.get_orientation();
content: gtk::Box,
start: gtk::Box,
center: gtk::Box,
end: gtk::Box,
inner: Inner,
}
impl Bar {
pub fn new(app: &Application, monitor_name: String, config: Config) -> Self {
let window = ApplicationWindow::builder()
.application(app)
.type_(WindowType::Toplevel)
.build();
let name = config
.name
.clone()
.unwrap_or_else(|| format!("bar-{}", Ironbar::unique_id()));
window.set_widget_name(&name);
let position = config.position;
let orientation = position.get_orientation();
let content = gtk::Box::builder() let content = gtk::Box::builder()
.orientation(orientation) .orientation(orientation)
@@ -78,238 +55,75 @@ impl Bar {
content.set_center_widget(Some(&center)); content.set_center_widget(Some(&center));
content.pack_end(&end, false, false, 0); content.pack_end(&end, false, false, 0);
window.add(&content); load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
win.add(&content);
window.connect_destroy_event(|_, _| { win.connect_destroy_event(|_, _| {
info!("Shutting down"); info!("Shutting down");
gtk::main_quit(); gtk::main_quit();
Propagation::Proceed Inhibit(false)
}); });
Bar { debug!("Showing bar");
name,
monitor_name,
position,
window,
content,
start,
center,
end,
inner: Inner::New {
config: Some(config),
},
}
}
pub fn init(mut self, monitor: &Monitor) -> Result<Self> { // show each box but do not use `show_all`.
let Inner::New { ref mut config } = self.inner else { // this ensures `show_if` option works as intended.
return Ok(self); start.show();
}; center.show();
end.show();
content.show();
win.show();
let Some(config) = config.take() else { Ok(())
return Ok(self);
};
info!(
"Initializing bar '{}' on '{}'",
self.name, self.monitor_name
);
self.setup_layer_shell(
&self.window,
true,
config.anchor_to_edges,
config.margin,
monitor,
);
let start_hidden = config.start_hidden.unwrap_or(config.autohide.is_some());
if let Some(autohide) = config.autohide {
let hotspot_window = Window::new(WindowType::Toplevel);
Self::setup_autohide(&self.window, &hotspot_window, autohide);
self.setup_layer_shell(
&hotspot_window,
false,
config.anchor_to_edges,
config.margin,
monitor,
);
if start_hidden {
hotspot_window.show();
}
}
let load_result = self.load_modules(config, monitor)?;
self.show(!start_hidden);
self.inner = Inner::Loaded {
popup: load_result.popup,
};
Ok(self)
} }
/// Sets up GTK layer shell for a provided application window. /// Sets up GTK layer shell for a provided application window.
fn setup_layer_shell( fn setup_layer_shell(
&self, win: &ApplicationWindow,
win: &impl IsA<Window>, monitor: &Monitor,
exclusive_zone: bool, position: BarPosition,
anchor_to_edges: bool, anchor_to_edges: bool,
margin: MarginConfig, margin: MarginConfig,
monitor: &Monitor,
) { ) {
let position = self.position; gtk_layer_shell::init_for_window(win);
gtk_layer_shell::set_monitor(win, monitor);
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
gtk_layer_shell::auto_exclusive_zone_enable(win);
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
win.init_layer_shell(); gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
win.set_monitor(monitor); gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
win.set_layer(gtk_layer_shell::Layer::Top); gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
win.set_namespace(env!("CARGO_PKG_NAME")); gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
if exclusive_zone {
win.auto_exclusive_zone_enable();
}
win.set_layer_shell_margin(gtk_layer_shell::Edge::Top, margin.top);
win.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, margin.bottom);
win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left);
win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right);
let bar_orientation = position.get_orientation(); let bar_orientation = position.get_orientation();
win.set_anchor( gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Top, gtk_layer_shell::Edge::Top,
position == BarPosition::Top position == BarPosition::Top
|| (bar_orientation == Orientation::Vertical && anchor_to_edges), || (bar_orientation == Orientation::Vertical && anchor_to_edges),
); );
win.set_anchor( gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Bottom, gtk_layer_shell::Edge::Bottom,
position == BarPosition::Bottom position == BarPosition::Bottom
|| (bar_orientation == Orientation::Vertical && anchor_to_edges), || (bar_orientation == Orientation::Vertical && anchor_to_edges),
); );
win.set_anchor( gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Left, gtk_layer_shell::Edge::Left,
position == BarPosition::Left position == BarPosition::Left
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges), || (bar_orientation == Orientation::Horizontal && anchor_to_edges),
); );
win.set_anchor( gtk_layer_shell::set_anchor(
win,
gtk_layer_shell::Edge::Right, gtk_layer_shell::Edge::Right,
position == BarPosition::Right position == BarPosition::Right
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges), || (bar_orientation == Orientation::Horizontal && anchor_to_edges),
); );
} }
fn setup_autohide(window: &ApplicationWindow, hotspot_window: &Window, timeout: u64) {
hotspot_window.hide();
hotspot_window.set_opacity(0.0);
hotspot_window.set_decorated(false);
hotspot_window.set_size_request(0, 1);
{
let hotspot_window = hotspot_window.clone();
window.connect_leave_notify_event(move |win, _| {
let win = win.clone();
let hotspot_window = hotspot_window.clone();
glib::timeout_add_local_once(Duration::from_millis(timeout), move || {
win.hide();
hotspot_window.show();
});
Propagation::Proceed
});
}
{
let win = window.clone();
hotspot_window.connect_enter_notify_event(move |hotspot_win, _| {
hotspot_win.hide();
win.show();
Propagation::Proceed
});
}
}
/// Loads the configured modules onto a bar.
fn load_modules(&self, config: Config, monitor: &Monitor) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
let app = &self.window.application().expect("to exist");
macro_rules! info {
($location:expr) => {
ModuleInfo {
app,
bar_position: config.position,
monitor,
output_name: &self.monitor_name,
location: $location,
icon_theme: &icon_theme,
}
};
}
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = Rc::new(RefCell::new(popup));
if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left);
add_modules(&self.start, modules, &info, &popup)?;
}
if let Some(modules) = config.center {
let info = info!(ModuleLocation::Center);
add_modules(&self.center, modules, &info, &popup)?;
}
if let Some(modules) = config.end {
let info = info!(ModuleLocation::Right);
add_modules(&self.end, modules, &info, &popup)?;
}
let result = BarLoadResult { popup };
Ok(result)
}
fn show(&self, include_window: bool) {
debug!("Showing bar: {}", self.name);
// show each box but do not use `show_all`.
// this ensures `show_if` option works as intended.
self.start.show();
self.center.show();
self.end.show();
self.content.show();
if include_window {
self.window.show();
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn popup(&self) -> Rc<RefCell<Popup>> {
match &self.inner {
Inner::New { .. } => {
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
}
Inner::Loaded { popup } => popup.clone(),
}
}
}
/// Creates a `gtk::Box` container to place widgets inside. /// Creates a `gtk::Box` container to place widgets inside.
fn create_container(name: &str, orientation: Orientation) -> gtk::Box { fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
let container = gtk::Box::builder() let container = gtk::Box::builder()
@@ -322,9 +136,54 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
container container
} }
#[derive(Debug)] /// Loads the configured modules onto a bar.
struct BarLoadResult { fn load_modules(
popup: Rc<RefCell<Popup>>, left: &gtk::Box,
center: &gtk::Box,
right: &gtk::Box,
app: &Application,
config: Config,
monitor: &Monitor,
output_name: &str,
) -> Result<()> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
macro_rules! info {
($location:expr) => {
ModuleInfo {
app,
bar_position: config.position,
monitor,
output_name,
location: $location,
icon_theme: &icon_theme,
}
};
}
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = Arc::new(RwLock::new(popup));
if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left);
add_modules(left, modules, &info, &popup)?;
}
if let Some(modules) = config.center {
let info = info!(ModuleLocation::Center);
add_modules(center, modules, &info, &popup)?;
}
if let Some(modules) = config.end {
let info = info!(ModuleLocation::Right);
add_modules(right, modules, &info, &popup)?;
}
Ok(())
} }
/// Adds modules into a provided GTK box, /// Adds modules into a provided GTK box,
@@ -333,20 +192,14 @@ fn add_modules(
content: &gtk::Box, content: &gtk::Box,
modules: Vec<ModuleConfig>, modules: Vec<ModuleConfig>,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>, popup: &Arc<RwLock<Popup>>,
) -> Result<()> { ) -> Result<()> {
let orientation = info.bar_position.get_orientation(); 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 to exist"); let common = $module.common.take().expect("Common config did not exist");
let widget_parts = create_module( let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
*$module,
$id,
common.name.clone(),
&info,
&Rc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common); set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation); let container = wrap_widget(&widget_parts.widget, common, orientation);
@@ -355,7 +208,7 @@ fn add_modules(
} }
for config in modules { for config in modules {
let id = Ironbar::unique_id(); let id = get_unique_usize();
match config { match config {
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
ModuleConfig::Clipboard(mut module) => add_module!(module, id), ModuleConfig::Clipboard(mut module) => add_module!(module, id),
@@ -381,13 +234,3 @@ fn add_modules(
Ok(()) Ok(())
} }
pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: String,
config: Config,
) -> Result<Bar> {
let bar = Bar::new(app, monitor_name, config);
bar.init(monitor)
}

44
src/bridge_channel.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::send;
use tokio::spawn;
use tokio::sync::mpsc;
/// MPSC async -> GTK sync channel.
/// The sender uses `tokio::sync::mpsc`
/// while the receiver uses `glib::MainContext::channel`.
///
/// This makes it possible to send events asynchronously
/// and receive them on the main thread,
/// allowing UI updates to be handled on the receiving end.
pub struct BridgeChannel<T> {
async_tx: mpsc::Sender<T>,
sync_rx: glib::Receiver<T>,
}
impl<T: Send + 'static> BridgeChannel<T> {
/// Creates a new channel
pub fn new() -> Self {
let (async_tx, mut async_rx) = mpsc::channel(32);
let (sync_tx, sync_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
while let Some(val) = async_rx.recv().await {
send!(sync_tx, val);
}
});
Self { async_tx, sync_rx }
}
/// Gets a clone of the sender.
pub fn create_sender(&self) -> mpsc::Sender<T> {
self.async_tx.clone()
}
/// Attaches a callback to the receiver.
pub fn recv<F>(self, f: F) -> glib::SourceId
where
F: FnMut(T) -> glib::Continue + 'static,
{
self.sync_rx.attach(None, f)
}
}

View File

@@ -1,9 +1,10 @@
use super::wayland::{self, ClipboardItem}; use super::wayland::{self, ClipboardItem};
use crate::{arc_mut, lock, spawn, try_send}; use crate::{lock, try_send};
use indexmap::map::Iter; use indexmap::map::Iter;
use indexmap::IndexMap; use indexmap::IndexMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, trace}; use tracing::{debug, trace};
@@ -27,9 +28,9 @@ impl ClipboardClient {
fn new() -> Self { fn new() -> Self {
trace!("Initializing clipboard client"); trace!("Initializing clipboard client");
let senders = arc_mut!(Vec::<(EventSender, usize)>::new()); let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
let cache = arc_mut!(ClipboardCache::new()); let cache = Arc::new(Mutex::new(ClipboardCache::new()));
{ {
let senders = senders.clone(); let senders = senders.clone();

View File

@@ -1,13 +1,15 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{arc_mut, lock, send, spawn_blocking}; use crate::{lock, send};
use color_eyre::Result; use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces}; use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial}; use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListener; use hyprland::event_listener::EventListenerMutable as EventListener;
use hyprland::prelude::*; use hyprland::prelude::*;
use hyprland::shared::{HyprDataVec, WorkspaceType}; use hyprland::shared::WorkspaceType;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::task::spawn_blocking;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
pub struct EventClient { pub struct EventClient {
@@ -34,25 +36,28 @@ impl EventClient {
let mut event_listener = EventListener::new(); let mut event_listener = EventListener::new();
// we need a lock to ensure events don't run at the same time // we need a lock to ensure events don't run at the same time
let lock = arc_mut!(()); let lock = Arc::new(Mutex::new(()));
// cache the active workspace since Hyprland doesn't give us the prev active // cache the active workspace since Hyprland doesn't give us the prev active
let active = Self::get_active_workspace().expect("Failed to get active workspace"); let active = Self::get_active_workspace().expect("Failed to get active workspace");
let active = arc_mut!(Some(active)); let active = Arc::new(Mutex::new(Some(active)));
{ {
let tx = tx.clone(); let tx = tx.clone();
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_workspace_added_handler(move |workspace_type| { event_listener.add_workspace_added_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
debug!("Added workspace: {workspace_type:?}"); debug!("Added workspace: {workspace_type:?}");
let workspace_name = get_workspace_name(workspace_type); let workspace_name = get_workspace_name(workspace_type);
let prev_workspace = lock!(active); let prev_workspace = lock!(active);
let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); let workspace = Self::get_workspace(&workspace_name, focused);
if let Some(workspace) = workspace { if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Add(workspace)); send!(tx, WorkspaceUpdate::Add(workspace));
@@ -65,7 +70,7 @@ impl EventClient {
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type| { event_listener.add_workspace_change_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
@@ -76,7 +81,10 @@ impl EventClient {
); );
let workspace_name = get_workspace_name(workspace_type); let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
workspace.map_or_else( workspace.map_or_else(
|| { || {
@@ -84,7 +92,8 @@ impl EventClient {
}, },
|workspace| { |workspace| {
// there may be another type of update so dispatch that regardless of focus change // there may be another type of update so dispatch that regardless of focus change
if !workspace.visibility.is_focused() { send!(tx, WorkspaceUpdate::Update(workspace.clone()));
if !focused {
Self::send_focus_change(&mut prev_workspace, workspace, &tx); Self::send_focus_change(&mut prev_workspace, workspace, &tx);
} }
}, },
@@ -97,9 +106,9 @@ impl EventClient {
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data| { event_listener.add_active_monitor_change_handler(move |event_data, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let workspace_type = event_data.workspace; let workspace_type = event_data.1;
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
@@ -109,11 +118,12 @@ impl EventClient {
); );
let workspace_name = get_workspace_name(workspace_type); let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
if let Some((false, workspace)) = if let (Some(workspace), false) = (workspace, focused) {
workspace.map(|w| (w.visibility.is_focused(), w))
{
Self::send_focus_change(&mut prev_workspace, workspace, &tx); Self::send_focus_change(&mut prev_workspace, workspace, &tx);
} else { } else {
error!("Unable to locate workspace"); error!("Unable to locate workspace");
@@ -125,20 +135,23 @@ impl EventClient {
let tx = tx.clone(); let tx = tx.clone();
let lock = lock.clone(); let lock = lock.clone();
event_listener.add_workspace_moved_handler(move |event_data| { event_listener.add_workspace_moved_handler(move |event_data, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let workspace_type = event_data.workspace; let workspace_type = event_data.1;
debug!("Received workspace move: {workspace_type:?}"); debug!("Received workspace move: {workspace_type:?}");
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
let workspace_name = get_workspace_name(workspace_type); let workspace_name = get_workspace_name(workspace_type);
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); let focused = prev_workspace
.as_ref()
.map_or(false, |w| w.name == workspace_name);
let workspace = Self::get_workspace(&workspace_name, focused);
if let Some(workspace) = workspace { if let Some(workspace) = workspace {
send!(tx, WorkspaceUpdate::Move(workspace.clone())); send!(tx, WorkspaceUpdate::Move(workspace.clone()));
if !workspace.visibility.is_focused() { if !focused {
Self::send_focus_change(&mut prev_workspace, workspace, &tx); Self::send_focus_change(&mut prev_workspace, workspace, &tx);
} }
} }
@@ -146,7 +159,7 @@ impl EventClient {
} }
{ {
event_listener.add_workspace_destroy_handler(move |workspace_type| { event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
debug!("Received workspace destroy: {workspace_type:?}"); debug!("Received workspace destroy: {workspace_type:?}");
@@ -168,28 +181,32 @@ impl EventClient {
workspace: Workspace, workspace: Workspace,
tx: &Sender<WorkspaceUpdate>, tx: &Sender<WorkspaceUpdate>,
) { ) {
let old = prev_workspace
.as_ref()
.map(|w| w.name.clone())
.unwrap_or_default();
send!( send!(
tx, tx,
WorkspaceUpdate::Focus { WorkspaceUpdate::Focus {
old: prev_workspace.take(), old,
new: workspace.clone(), new: workspace.name.clone(),
} }
); );
prev_workspace.replace(workspace); prev_workspace.replace(workspace);
} }
/// Gets a workspace by name from the server, given the active workspace if known. /// Gets a workspace by name from the server.
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> { ///
/// Use `focused` to manually mark the workspace as focused,
/// as this is not automatically checked.
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
Workspaces::get() Workspaces::get()
.expect("Failed to get workspaces") .expect("Failed to get workspaces")
.find_map(|w| { .find_map(|w| {
if w.name == name { if w.name == name {
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| { Some(Workspace::from((focused, w)))
create_is_visible()(w)
}));
Some(Workspace::from((vis, w)))
} else { } else {
None None
} }
@@ -198,19 +215,16 @@ impl EventClient {
/// Gets the active workspace from the server. /// Gets the active workspace from the server.
fn get_active_workspace() -> Result<Workspace> { fn get_active_workspace() -> Result<Workspace> {
let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?; let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?;
Ok(w) Ok(w)
} }
} }
impl WorkspaceClient for EventClient { impl WorkspaceClient for EventClient {
fn focus(&self, id: String) -> Result<()> { fn focus(&self, id: String) -> Result<()> {
let identifier = match id.parse::<i32>() { Dispatch::call(DispatchType::Workspace(
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum), WorkspaceIdentifierWithSpecial::Name(&id),
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id), ))?;
};
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(()) Ok(())
} }
@@ -220,16 +234,13 @@ impl WorkspaceClient for EventClient {
{ {
let tx = self.workspace_tx.clone(); let tx = self.workspace_tx.clone();
let active_id = HWorkspace::get_active().ok().map(|active| active.name); let active_name = HWorkspace::get_active()
let is_visible = create_is_visible(); .map(|active| active.name)
.unwrap_or_default();
let workspaces = Workspaces::get() let workspaces = Workspaces::get()
.expect("Failed to get workspaces") .expect("Failed to get workspaces")
.map(|w| { .map(|w| Workspace::from((w.name == active_name, w)))
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect(); .collect();
send!(tx, WorkspaceUpdate::Init(workspaces)); send!(tx, WorkspaceUpdate::Init(workspaces));
@@ -258,39 +269,13 @@ fn get_workspace_name(name: WorkspaceType) -> String {
} }
} }
/// Creates a function which determines if a workspace is visible. impl From<(bool, hyprland::data::Workspace)> for Workspace {
/// fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
/// This function makes a Hyprland call that allocates so it should be cached when possible,
/// but it is only valid so long as workspaces do not change so it should not be stored long term
fn create_is_visible() -> impl Fn(&HWorkspace) -> bool {
let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), HyprDataVec::to_vec);
move |w| monitors.iter().any(|m| m.active_workspace.id == w.id)
}
impl From<(Visibility, HWorkspace)> for Workspace {
fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self {
Self { Self {
id: workspace.id.to_string(), id: workspace.id.to_string(),
name: workspace.name, name: workspace.name,
monitor: workspace.monitor, monitor: workspace.monitor,
visibility, focused,
}
}
}
impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility
where
F: FnOnce(&'f HWorkspace) -> bool,
'a: 'f,
{
fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self {
if Some(workspace.name.as_str()) == active_name {
Self::focused()
} else if is_visible(workspace) {
Self::visible()
} else {
Self::Hidden
} }
} }
} }

View File

@@ -75,38 +75,8 @@ pub struct Workspace {
pub name: String, pub name: String,
/// Name of the monitor (output) the workspace is located on /// Name of the monitor (output) the workspace is located on
pub monitor: String, pub monitor: String,
/// How visible the workspace is /// Whether the workspace is in focus
pub visibility: Visibility, pub focused: bool,
}
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
#[derive(Debug, Copy, Clone)]
pub enum Visibility {
Visible(bool),
Hidden,
}
impl Visibility {
pub fn visible() -> Self {
Self::Visible(false)
}
pub fn focused() -> Self {
Self::Visible(true)
}
pub fn is_visible(self) -> bool {
matches!(self, Self::Visible(_))
}
pub fn is_focused(self) -> bool {
if let Self::Visible(focused) = self {
focused
} else {
false
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -116,17 +86,13 @@ pub enum WorkspaceUpdate {
Init(Vec<Workspace>), Init(Vec<Workspace>),
Add(Workspace), Add(Workspace),
Remove(String), Remove(String),
Update(Workspace),
Move(Workspace), Move(Workspace),
/// Declares focus moved from the old workspace to the new. /// Declares focus moved from the old workspace to the new.
Focus { Focus {
old: Option<Workspace>, old: String,
new: Workspace, new: String,
}, },
/// An update was triggered by the compositor but this was not mapped by Ironbar.
///
/// This is purely used for ergonomics within the compositor clients
/// and should be ignored by consumers.
Unknown,
} }
pub trait WorkspaceClient { pub trait WorkspaceClient {

View File

@@ -1,11 +1,12 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send, spawn}; use crate::{await_sync, send};
use async_once::AsyncOnce; use async_once::AsyncOnce;
use color_eyre::Report; use color_eyre::Report;
use futures_util::StreamExt; use futures_util::StreamExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent}; use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
use tokio::spawn;
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{info, trace}; use tracing::{info, trace};
@@ -31,11 +32,8 @@ impl SwayEventClient {
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
trace!("event: {:?}", event); trace!("event: {:?}", event);
if let Event::Workspace(event) = event? { if let Event::Workspace(ev) = event? {
let event = WorkspaceUpdate::from(*event); workspace_tx.send(WorkspaceUpdate::from(*ev))?;
if !matches!(event, WorkspaceUpdate::Unknown) {
workspace_tx.send(event)?;
}
}; };
} }
@@ -107,50 +105,22 @@ pub fn get_sub_client() -> &'static SwayEventClient {
impl From<Node> for Workspace { impl From<Node> for Workspace {
fn from(node: Node) -> Self { fn from(node: Node) -> Self {
let visibility = Visibility::from(&node);
Self { Self {
id: node.id.to_string(), id: node.id.to_string(),
name: node.name.unwrap_or_default(), name: node.name.unwrap_or_default(),
monitor: node.output.unwrap_or_default(), monitor: node.output.unwrap_or_default(),
visibility, focused: node.focused,
} }
} }
} }
impl From<swayipc_async::Workspace> for Workspace { impl From<swayipc_async::Workspace> for Workspace {
fn from(workspace: swayipc_async::Workspace) -> Self { fn from(workspace: swayipc_async::Workspace) -> Self {
let visibility = Visibility::from(&workspace);
Self { Self {
id: workspace.id.to_string(), id: workspace.id.to_string(),
name: workspace.name, name: workspace.name,
monitor: workspace.output, monitor: workspace.output,
visibility, focused: workspace.focused,
}
}
}
impl From<&Node> for Visibility {
fn from(node: &Node) -> Self {
if node.focused {
Self::focused()
} else if node.visible.unwrap_or(false) {
Self::visible()
} else {
Self::Hidden
}
}
}
impl From<&swayipc_async::Workspace> for Visibility {
fn from(workspace: &swayipc_async::Workspace) -> Self {
if workspace.focused {
Self::focused()
} else if workspace.visible {
Self::visible()
} else {
Self::Hidden
} }
} }
} }
@@ -169,13 +139,21 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
.unwrap_or_default(), .unwrap_or_default(),
), ),
WorkspaceChange::Focus => Self::Focus { WorkspaceChange::Focus => Self::Focus {
old: event.old.map(Workspace::from), old: event
new: Workspace::from(event.current.expect("Missing current workspace")), .old
.expect("Missing old workspace")
.name
.unwrap_or_default(),
new: event
.current
.expect("Missing current workspace")
.name
.unwrap_or_default(),
}, },
WorkspaceChange::Move => { WorkspaceChange::Move => {
Self::Move(event.current.expect("Missing current workspace").into()) Self::Move(event.current.expect("Missing current workspace").into())
} }
_ => Self::Unknown, _ => Self::Update(event.current.expect("Missing current workspace").into()),
} }
} }
} }

View File

@@ -9,17 +9,9 @@ pub mod mpd;
#[cfg(feature = "music+mpris")] #[cfg(feature = "music+mpris")]
pub mod mpris; pub mod mpris;
pub const TICK_INTERVAL_MS: u64 = 200;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum PlayerUpdate { pub enum PlayerUpdate {
/// Triggered when the track or player state notably changes,
/// such as a new track playing, the player being paused, or a volume change.
Update(Box<Option<Track>>, Status), Update(Box<Option<Track>>, Status),
/// Triggered at regular intervals while a track is playing.
/// Used to keep track of the progress through the current track.
ProgressTick(ProgressTick),
/// Triggered when the client disconnects from the player.
Disconnect, Disconnect,
} }
@@ -35,25 +27,21 @@ pub struct Track {
pub cover_path: Option<String>, pub cover_path: Option<String>,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Debug)]
pub enum PlayerState { pub enum PlayerState {
Playing, Playing,
Paused, Paused,
Stopped, Stopped,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Debug)]
pub struct Status { pub struct Status {
pub state: PlayerState, pub state: PlayerState,
pub volume_percent: Option<u8>, pub volume_percent: u8,
pub playlist_position: u32,
pub playlist_length: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct ProgressTick {
pub duration: Option<Duration>, pub duration: Option<Duration>,
pub elapsed: Option<Duration>, pub elapsed: Option<Duration>,
pub playlist_position: u32,
pub playlist_length: u32,
} }
pub trait MusicClient { pub trait MusicClient {
@@ -63,7 +51,6 @@ pub trait MusicClient {
fn prev(&self) -> Result<()>; fn prev(&self) -> Result<()>;
fn set_volume_percent(&self, vol: u8) -> Result<()>; fn set_volume_percent(&self, vol: u8) -> Result<()>;
fn seek(&self, duration: Duration) -> Result<()>;
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>; fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
} }

View File

@@ -1,11 +1,9 @@
use super::{ use super::{MusicClient, Status, Track};
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS, use crate::await_sync;
}; use crate::clients::music::{PlayerState, PlayerUpdate};
use crate::{await_sync, send, spawn};
use color_eyre::Result; use color_eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mpd_client::client::{Connection, ConnectionEvent, Subsystem}; use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
use mpd_client::commands::SeekMode;
use mpd_client::protocol::MpdProtocolError; use mpd_client::protocol::MpdProtocolError;
use mpd_client::responses::{PlayState, Song}; use mpd_client::responses::{PlayState, Song};
use mpd_client::tag::Tag; use mpd_client::tag::Tag;
@@ -17,7 +15,8 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::net::{TcpStream, UnixStream}; use tokio::net::{TcpStream, UnixStream};
use tokio::sync::broadcast; use tokio::spawn;
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
@@ -30,8 +29,8 @@ lazy_static! {
pub struct MpdClient { pub struct MpdClient {
client: Client, client: Client,
music_dir: PathBuf, music_dir: PathBuf,
tx: broadcast::Sender<PlayerUpdate>, tx: Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>, _rx: Receiver<PlayerUpdate>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -58,7 +57,7 @@ impl MpdClient {
let (client, mut state_changes) = let (client, mut state_changes) =
wait_for_connection(host, Duration::from_secs(5), None).await?; wait_for_connection(host, Duration::from_secs(5), None).await?;
let (tx, rx) = broadcast::channel(16); let (tx, rx) = channel(16);
{ {
let music_dir = music_dir.clone(); let music_dir = music_dir.clone();
@@ -79,19 +78,7 @@ impl MpdClient {
} }
} }
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(()) Ok::<(), SendError<(Option<Track>, Status)>>(())
});
}
{
let client = client.clone();
let tx = tx.clone();
spawn(async move {
loop {
Self::send_tick_update(&client, &tx).await;
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
}
}); });
} }
@@ -105,9 +92,9 @@ impl MpdClient {
async fn send_update( async fn send_update(
client: &Client, client: &Client,
tx: &broadcast::Sender<PlayerUpdate>, tx: &Sender<PlayerUpdate>,
music_dir: &Path, music_dir: &Path,
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> { ) -> Result<(), SendError<PlayerUpdate>> {
let current_song = client.command(commands::CurrentSong).await; let current_song = client.command(commands::CurrentSong).await;
let status = client.command(commands::Status).await; let status = client.command(commands::Status).await;
@@ -115,33 +102,17 @@ impl MpdClient {
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir)); let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
let status = Status::from(status); let status = Status::from(status);
let update = PlayerUpdate::Update(Box::new(track), status); tx.send(PlayerUpdate::Update(Box::new(track), status))?;
send!(tx, update);
} }
Ok(()) Ok(())
} }
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
let status = client.command(commands::Status).await;
if let Ok(status) = status {
if status.state == PlayState::Playing {
let update = PlayerUpdate::ProgressTick(ProgressTick {
duration: status.duration,
elapsed: status.elapsed,
});
send!(tx, update);
}
}
}
fn is_connected(&self) -> bool { fn is_connected(&self) -> bool {
!self.client.is_connection_closed() !self.client.is_connection_closed()
} }
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> { fn send_disconnect_update(&self) -> Result<(), SendError<PlayerUpdate>> {
info!("Connection to MPD server lost"); info!("Connection to MPD server lost");
self.tx.send(PlayerUpdate::Disconnect)?; self.tx.send(PlayerUpdate::Disconnect)?;
Ok(()) Ok(())
@@ -211,12 +182,7 @@ impl MusicClient for MpdClient {
Ok(()) Ok(())
} }
fn seek(&self, duration: Duration) -> Result<()> { fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
Ok(())
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
let rx = self.tx.subscribe(); let rx = self.tx.subscribe();
await_sync(async { await_sync(async {
Self::send_update(&self.client, &self.tx, &self.music_dir) Self::send_update(&self.client, &self.tx, &self.music_dir)
@@ -325,7 +291,9 @@ impl From<mpd_client::responses::Status> for Status {
fn from(status: mpd_client::responses::Status) -> Self { fn from(status: mpd_client::responses::Status) -> Self {
Self { Self {
state: PlayerState::from(status.state), state: PlayerState::from(status.state),
volume_percent: Some(status.volume), volume_percent: status.volume,
duration: status.duration,
elapsed: status.elapsed,
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32), playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
playlist_length: status.playlist_length as u32, playlist_length: status.playlist_length as u32,
} }

View File

@@ -1,15 +1,16 @@
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS}; use super::{MusicClient, PlayerUpdate, Status, Track};
use crate::clients::music::ProgressTick; use crate::clients::music::PlayerState;
use crate::{arc_mut, lock, send, spawn_blocking}; use crate::{lock, send};
use color_eyre::Result; use color_eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::collections::HashSet; use std::collections::HashSet;
use std::string;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use std::{cmp, string}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::sync::broadcast; use tokio::task::spawn_blocking;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
lazy_static! { lazy_static! {
@@ -18,18 +19,18 @@ lazy_static! {
pub struct Client { pub struct Client {
current_player: Arc<Mutex<Option<String>>>, current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>, tx: Sender<PlayerUpdate>,
_rx: broadcast::Receiver<PlayerUpdate>, _rx: Receiver<PlayerUpdate>,
} }
impl Client { impl Client {
fn new() -> Self { fn new() -> Self {
let (tx, rx) = broadcast::channel(32); let (tx, rx) = channel(32);
let current_player = arc_mut!(None); let current_player = Arc::new(Mutex::new(None));
{ {
let players_list = arc_mut!(HashSet::new()); let players_list = Arc::new(Mutex::new(HashSet::new()));
let current_player = current_player.clone(); let current_player = current_player.clone();
let tx = tx.clone(); let tx = tx.clone();
@@ -83,20 +84,6 @@ impl Client {
}); });
} }
{
let current_player = current_player.clone();
let tx = tx.clone();
spawn_blocking(move || {
let player_finder = PlayerFinder::new().expect("to get new player finder");
loop {
Self::send_tick_update(&player_finder, &current_player, &tx);
sleep(Duration::from_millis(TICK_INTERVAL_MS));
}
});
}
Self { Self {
current_player, current_player,
tx, tx,
@@ -108,7 +95,7 @@ impl Client {
player_id: String, player_id: String,
players: Arc<Mutex<HashSet<String>>>, players: Arc<Mutex<HashSet<String>>>,
current_player: Arc<Mutex<Option<String>>>, current_player: Arc<Mutex<Option<String>>>,
tx: broadcast::Sender<PlayerUpdate>, tx: Sender<PlayerUpdate>,
) { ) {
spawn_blocking(move || { spawn_blocking(move || {
let player_finder = PlayerFinder::new()?; let player_finder = PlayerFinder::new()?;
@@ -151,7 +138,7 @@ impl Client {
}); });
} }
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> { fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
debug!("Sending update using '{}'", player.identity()); debug!("Sending update using '{}'", player.identity());
let metadata = player.get_metadata()?; let metadata = player.get_metadata()?;
@@ -161,7 +148,10 @@ impl Client {
let track_list = player.get_track_list(); let track_list = player.get_track_list();
let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok(); let volume_percent = player
.get_volume()
.map(|vol| (vol * 100.0) as u8)
.unwrap_or(0);
let status = Status { let status = Status {
// MRPIS doesn't seem to provide playlist info reliably, // MRPIS doesn't seem to provide playlist info reliably,
@@ -169,6 +159,8 @@ impl Client {
playlist_position: 1, playlist_position: 1,
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX), playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
state: PlayerState::from(playback_status), state: PlayerState::from(playback_status),
elapsed: player.get_position().ok(),
duration: metadata.length(),
volume_percent, volume_percent,
}; };
@@ -189,26 +181,6 @@ impl Client {
player_finder.find_by_name(player_name).ok() player_finder.find_by_name(player_name).ok()
}) })
} }
fn send_tick_update(
player_finder: &PlayerFinder,
current_player: &Mutex<Option<String>>,
tx: &broadcast::Sender<PlayerUpdate>,
) {
if let Some(player) = lock!(current_player)
.as_ref()
.and_then(|name| player_finder.find_by_name(name).ok())
{
if let Ok(metadata) = player.get_metadata() {
let update = PlayerUpdate::ProgressTick(ProgressTick {
elapsed: player.get_position().ok(),
duration: metadata.length(),
});
send!(tx, update);
}
}
}
} }
macro_rules! command { macro_rules! command {
@@ -244,30 +216,14 @@ impl MusicClient for Client {
fn set_volume_percent(&self, vol: u8) -> Result<()> { fn set_volume_percent(&self, vol: u8) -> Result<()> {
if let Some(player) = Self::get_player(self) { if let Some(player) = Self::get_player(self) {
player.set_volume(f64::from(vol) / 100.0)?; player.set_volume(vol as f64 / 100.0)?;
} else { } else {
error!("Could not find player"); error!("Could not find player");
} }
Ok(()) Ok(())
} }
fn seek(&self, duration: Duration) -> Result<()> { fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
if let Some(player) = Self::get_player(self) {
let pos = player.get_position().unwrap_or_default();
let duration = duration.as_micros() as i64;
let position = pos.as_micros() as i64;
let seek = cmp::max(duration, 0) - position;
player.seek(seek)?;
} else {
error!("Could not find player");
}
Ok(())
}
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
debug!("Creating new subscription"); debug!("Creating new subscription");
let rx = self.tx.subscribe(); let rx = self.tx.subscribe();
@@ -280,7 +236,9 @@ impl MusicClient for Client {
playlist_position: 0, playlist_position: 0,
playlist_length: 0, playlist_length: 0,
state: PlayerState::Stopped, state: PlayerState::Stopped,
volume_percent: None, elapsed: None,
duration: None,
volume_percent: 0,
}; };
send!(self.tx, PlayerUpdate::Update(Box::new(None), status)); send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
} }
@@ -299,18 +257,9 @@ impl From<Metadata> for Track {
const KEY_GENRE: &str = "xesam:genre"; const KEY_GENRE: &str = "xesam:genre";
Self { Self {
title: value title: value.title().map(std::string::ToString::to_string),
.title() album: value.album_name().map(std::string::ToString::to_string),
.map(std::string::ToString::to_string) artist: value.artists().map(|artists| artists.join(", ")),
.and_then(replace_empty_none),
album: value
.album_name()
.map(std::string::ToString::to_string)
.and_then(replace_empty_none),
artist: value
.artists()
.map(|artists| artists.join(", "))
.and_then(replace_empty_none),
date: value date: value
.get(KEY_DATE) .get(KEY_DATE)
.and_then(mpris::MetadataValue::as_string) .and_then(mpris::MetadataValue::as_string)
@@ -335,11 +284,3 @@ impl From<PlaybackStatus> for PlayerState {
} }
} }
} }
fn replace_empty_none(string: String) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
}
}

View File

@@ -1,13 +1,15 @@
use crate::{arc_mut, lock, send, spawn, Ironbar}; use crate::unique_id::get_unique_usize;
use crate::{lock, send};
use async_once::AsyncOnce; use async_once::AsyncOnce;
use color_eyre::Report; use color_eyre::Report;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use system_tray::message::menu::TrayMenu; use stray::message::menu::TrayMenu;
use system_tray::message::tray::StatusNotifierItem; use stray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage}; use stray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::StatusNotifierWatcher; use stray::StatusNotifierWatcher;
use tokio::spawn;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -22,8 +24,8 @@ pub struct TrayEventReceiver {
} }
impl TrayEventReceiver { impl TrayEventReceiver {
async fn new() -> system_tray::error::Result<Self> { async fn new() -> stray::error::Result<Self> {
let id = format!("ironbar-{}", Ironbar::unique_id()); let id = format!("ironbar-{}", get_unique_usize());
let (tx, rx) = mpsc::channel(16); let (tx, rx) = mpsc::channel(16);
let (b_tx, b_rx) = broadcast::channel(16); let (b_tx, b_rx) = broadcast::channel(16);
@@ -31,7 +33,7 @@ impl TrayEventReceiver {
let tray = StatusNotifierWatcher::new(rx).await?; let tray = StatusNotifierWatcher::new(rx).await?;
let mut host = Box::pin(tray.create_notifier_host(&id)).await?; let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
let tray = arc_mut!(BTreeMap::new()); let tray = Arc::new(Mutex::new(BTreeMap::new()));
{ {
let b_tx = b_tx.clone(); let b_tx = b_tx.clone();

View File

@@ -3,29 +3,29 @@ use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
use super::wlr_foreign_toplevel::ToplevelEvent; use super::wlr_foreign_toplevel::ToplevelEvent;
use super::Environment; use super::Environment;
use crate::error::ERR_CHANNEL_RECV; use crate::error::ERR_CHANNEL_RECV;
use crate::{send, spawn_blocking}; use crate::send;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::Report; use color_eyre::Report;
use smithay_client_toolkit::output::{OutputInfo, OutputState}; use smithay_client_toolkit::output::{OutputInfo, OutputState};
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender}; use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
use smithay_client_toolkit::reexports::calloop::EventLoop; use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::registry::RegistryState; use smithay_client_toolkit::registry::RegistryState;
use smithay_client_toolkit::seat::SeatState; use smithay_client_toolkit::seat::SeatState;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::mpsc; use std::sync::mpsc;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::task::spawn_blocking;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use wayland_client::globals::registry_queue_init; use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::Connection; use wayland_client::{Connection, WaylandSource};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
use super::ClipboardItem; use super::ClipboardItem;
use super::wlr_data_control::manager::DataControlDeviceManagerState; use super::wlr_data_control::manager::DataControlDeviceManagerState;
use crate::lock; use crate::lock;
use std::sync::Arc; use std::sync::{Arc, Mutex};
} }
} }
@@ -106,7 +106,8 @@ impl WaylandClient {
let mut event_loop = let mut event_loop =
EventLoop::<Environment>::try_new().expect("Failed to create new event loop"); EventLoop::<Environment>::try_new().expect("Failed to create new event loop");
WaylandSource::new(conn, queue) WaylandSource::new(queue)
.expect("Failed to create Wayland source from queue")
.insert(event_loop.handle()) .insert(event_loop.handle())
.expect("Failed to insert Wayland event queue into event loop"); .expect("Failed to insert Wayland event queue into event loop");
@@ -137,7 +138,7 @@ impl WaylandClient {
seats: vec![], seats: vec![],
handles: HashMap::new(), handles: HashMap::new(),
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard: crate::arc_mut!(None), clipboard: Arc::new(Mutex::new(None)),
toplevel_tx, toplevel_tx,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx, clipboard_tx,

View File

@@ -6,7 +6,7 @@ mod wl_seat;
mod wlr_foreign_toplevel; mod wlr_foreign_toplevel;
use self::wlr_foreign_toplevel::manager::ToplevelManagerState; use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
use crate::{arc_mut, delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager}; use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use smithay_client_toolkit::output::OutputState; use smithay_client_toolkit::output::OutputState;
@@ -105,7 +105,7 @@ impl ProvidesRegistryState for Environment {
} }
lazy_static! { lazy_static! {
static ref CLIENT: Arc<Mutex<WaylandClient>> = arc_mut!(WaylandClient::new()); static ref CLIENT: Arc<Mutex<WaylandClient>> = Arc::new(Mutex::new(WaylandClient::new()));
} }
pub fn get_client() -> Arc<Mutex<WaylandClient>> { pub fn get_client() -> Arc<Mutex<WaylandClient>> {

View File

@@ -7,13 +7,14 @@ use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer}; use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
use self::source::DataControlSourceHandler; use self::source::DataControlSourceHandler;
use crate::clients::wayland::Environment; use crate::clients::wayland::Environment;
use crate::{lock, send, Ironbar}; use crate::unique_id::get_unique_usize;
use crate::{lock, send};
use device::DataControlDevice; use device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ}; use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags}; use nix::sys::epoll::{epoll_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp};
use smithay_client_toolkit::data_device_manager::WritePipe; use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken}; use smithay_client_toolkit::reexports::calloop::RegistrationToken;
use std::cmp::min; use std::cmp::min;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::fs::File; use std::fs::File;
@@ -144,7 +145,7 @@ impl Environment {
}; };
Ok(ClipboardItem { Ok(ClipboardItem {
id: Ironbar::unique_id(), id: get_unique_usize(),
value, value,
mime_type: mime_type.value.clone(), mime_type: mime_type.value.clone(),
}) })
@@ -195,9 +196,9 @@ impl DataControlDeviceHandler for Environment {
let tx = self.clipboard_tx.clone(); let tx = self.clipboard_tx.clone();
let clipboard = self.clipboard.clone(); let clipboard = self.clipboard.clone();
let token = let token = self
self.loop_handle .loop_handle
.insert_source(read_pipe, move |(), file, state| unsafe { .insert_source(read_pipe, move |_, file, state| {
let item = state let item = state
.selection_offers .selection_offers
.iter() .iter()
@@ -205,7 +206,7 @@ impl DataControlDeviceHandler for Environment {
.map(|p| state.selection_offers.remove(p)) .map(|p| state.selection_offers.remove(p))
.expect("Failed to find selection offer item"); .expect("Failed to find selection offer item");
match Self::read_file(&mime_type, file.get_mut()) { match Self::read_file(&mime_type, file) {
Ok(item) => { Ok(item) => {
let item = Arc::new(item); let item = Arc::new(item);
lock!(clipboard).replace(item.clone()); lock!(clipboard).replace(item.clone());
@@ -217,8 +218,6 @@ impl DataControlDeviceHandler for Environment {
state state
.loop_handle .loop_handle
.remove(item.token.expect("Missing item token")); .remove(item.token.expect("Missing item token"));
PostAction::Remove
}); });
match token { match token {
@@ -240,7 +239,7 @@ impl DataControlOfferHandler for Environment {
_offer: &mut DataControlDeviceOffer, _offer: &mut DataControlDeviceOffer,
_mime_type: String, _mime_type: String,
) { ) {
trace!("Handler received offer"); debug!("Handler received offer");
} }
} }
@@ -290,12 +289,15 @@ impl DataControlSourceHandler for Environment {
trace!("Num bytes: {}", bytes.len()); trace!("Num bytes: {}", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>(); let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0); let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll_fd = let epoll_fd = epoll_create().expect("to get valid file descriptor");
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor"); epoll_ctl(
epoll_fd epoll_fd,
.add(fd, epoll_event) EpollOp::EpollCtlAdd,
fd.as_raw_fd(),
&mut epoll_event,
)
.expect("to send valid epoll operation"); .expect("to send valid epoll operation");
while !bytes.is_empty() { while !bytes.is_empty() {
@@ -303,9 +305,7 @@ impl DataControlSourceHandler for Environment {
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len()); trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
epoll_fd epoll_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
.wait(&mut events, 100)
.expect("Failed to wait to epoll");
match file.write(chunk) { match file.write(chunk) {
Ok(_) => bytes = &bytes[chunk.len()..], Ok(_) => bytes = &bytes[chunk.len()..],
@@ -315,6 +315,22 @@ impl DataControlSourceHandler for Environment {
} }
} }
} }
// for chunk in bytes.chunks(pipe_size as usize) {
// trace!("Writing chunk");
// file.write(chunk).expect("Failed to write chunk to buffer");
// file.flush().expect("Failed to flush to file");
// }
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
// match file.write_all(bytes) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
} else { } else {
error!("Failed to find source"); error!("Failed to find source");
} }
@@ -359,14 +375,11 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
let new_size = if size > curr_size { let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}"); trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?; let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
trace!("New pipe size: {res}"); trace!("New pipe size: {res}");
if res < size as i32 { if res < size as i32 {
return Err(io::Error::last_os_error()); return Err(io::Error::last_os_error());
} }
res res
} else { } else {
size as i32 size as i32

View File

@@ -5,9 +5,9 @@ use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError; use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use smithay_client_toolkit::data_device_manager::ReadPipe; use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::os::fd::{BorrowedFd, FromRawFd}; use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::{trace, warn}; use tracing::{debug, warn};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1, Event, ZwlrDataControlOfferV1,
@@ -37,7 +37,7 @@ impl PartialEq for SelectionOffer {
impl SelectionOffer { impl SelectionOffer {
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> { pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
unsafe { receive(&self.data_offer, mime_type) }.map_err(DataOfferError::Io) receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
} }
} }
@@ -149,7 +149,7 @@ where
let data = data.data_control_offer_data(); let data = data.data_control_offer_data();
if let Event::Offer { mime_type } = event { if let Event::Offer { mime_type } = event {
trace!("Adding new offer with type '{mime_type}'"); debug!("Adding new offer with type '{mime_type}'");
data.push_mime_type(mime_type.clone()); data.push_mime_type(mime_type.clone());
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type); state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
} }
@@ -169,18 +169,15 @@ where
/// ///
/// Fails if too many file descriptors were already open and a pipe /// Fails if too many file descriptors were already open and a pipe
/// could not be created. /// could not be created.
pub unsafe fn receive( pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
offer: &ZwlrDataControlOfferV1,
mime_type: String,
) -> std::io::Result<ReadPipe> {
// create a pipe // create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?; let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
offer.receive(mime_type, BorrowedFd::borrow_raw(writefd)); offer.receive(mime_type, writefd);
if let Err(err) = close(writefd) { if let Err(err) = close(writefd) {
warn!("Failed to close write pipe: {}", err); warn!("Failed to close write pipe: {}", err);
} }
Ok(FromRawFd::from_raw_fd(readfd)) Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
} }

View File

@@ -1,5 +1,6 @@
use super::manager::ToplevelManagerState; use super::manager::ToplevelManagerState;
use crate::{lock, Ironbar}; use crate::lock;
use crate::unique_id::get_unique_usize;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::trace; use tracing::trace;
@@ -67,7 +68,7 @@ pub struct ToplevelInfo {
impl Default for ToplevelInfo { impl Default for ToplevelInfo {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: Ironbar::unique_id(), id: get_unique_usize(),
app_id: String::new(), app_id: String::new(),
title: String::new(), title: String::new(),
fullscreen: false, fullscreen: false,

View File

@@ -30,7 +30,7 @@ impl ToplevelManagerHandler for Environment {
impl ToplevelHandleHandler for Environment { impl ToplevelHandleHandler for Environment {
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) { fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) {
trace!("Handler received new handle"); debug!("Handler received new handle");
match handle.info() { match handle.info() {
Some(info) => { Some(info) => {
@@ -50,7 +50,7 @@ impl ToplevelHandleHandler for Environment {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
handle: ToplevelHandle, handle: ToplevelHandle,
) { ) {
trace!("Handler received handle update"); debug!("Handler received handle update");
match handle.info() { match handle.info() {
Some(info) => { Some(info) => {

View File

@@ -1,6 +1,5 @@
use crate::dynamic_value::{dynamic_string, DynamicBool}; use crate::dynamic_value::{dynamic_string, DynamicBool};
use crate::script::{Script, ScriptInput}; use crate::script::{Script, ScriptInput};
use glib::Propagation;
use gtk::gdk::ScrollDirection; use gtk::gdk::ScrollDirection;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType}; use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
@@ -76,7 +75,7 @@ impl CommonConfig {
script.run_as_oneshot(None); script.run_as_oneshot(None);
} }
Propagation::Proceed Inhibit(false)
}); });
let scroll_up_script = self.on_scroll_up.map(Script::new_polling); let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
@@ -94,7 +93,7 @@ impl CommonConfig {
script.run_as_oneshot(None); script.run_as_oneshot(None);
} }
Propagation::Proceed Inhibit(false)
}); });
macro_rules! install_oneshot { macro_rules! install_oneshot {
@@ -102,7 +101,7 @@ impl CommonConfig {
$option.map(Script::new_polling).map(|script| { $option.map(Script::new_polling).map(|script| {
container.$method(move |_, _| { container.$method(move |_, _| {
script.run_as_oneshot(None); script.run_as_oneshot(None);
Propagation::Proceed Inhibit(false)
}); });
}) })
}; };
@@ -115,6 +114,7 @@ impl CommonConfig {
let container = container.clone(); let container = container.clone();
dynamic_string(&tooltip, move |string| { dynamic_string(&tooltip, move |string| {
container.set_tooltip_text(Some(&string)); container.set_tooltip_text(Some(&string));
Continue(true)
}); });
} }
} }
@@ -136,6 +136,7 @@ impl CommonConfig {
container.show_all(); container.show_all();
} }
revealer.set_reveal_child(success); revealer.set_reveal_child(success);
Continue(true)
}); });
} }

View File

@@ -21,17 +21,16 @@ use crate::modules::tray::TrayModule;
use crate::modules::upower::UpowerModule; use crate::modules::upower::UpowerModule;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule; use crate::modules::workspaces::WorkspacesModule;
use cfg_if::cfg_if;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
pub use self::common::{CommonConfig, TransitionType}; pub use self::common::{CommonConfig, TransitionType};
pub use self::truncate::TruncateMode; pub use self::truncate::{EllipsizeMode, TruncateMode};
#[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 {
#[cfg(feature = "clipboard")] #[cfg(feature = "clock")]
Clipboard(Box<ClipboardModule>), Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
Clock(Box<ClockModule>), Clock(Box<ClockModule>),
@@ -97,12 +96,6 @@ pub struct Config {
pub margin: MarginConfig, pub margin: MarginConfig,
#[serde(default = "default_popup_gap")] #[serde(default = "default_popup_gap")]
pub popup_gap: i32, pub popup_gap: i32,
pub name: Option<String>,
#[serde(default)]
pub start_hidden: Option<bool>,
#[serde(default)]
pub autohide: Option<u64>,
/// GTK icon theme to use. /// GTK icon theme to use.
pub icon_theme: Option<String>, pub icon_theme: Option<String>,
@@ -116,38 +109,6 @@ pub struct Config {
pub monitors: Option<HashMap<String, MonitorConfig>>, pub monitors: Option<HashMap<String, MonitorConfig>>,
} }
impl Default for Config {
fn default() -> Self {
cfg_if! {
if #[cfg(feature = "clock")] {
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
}
else {
let end = None;
}
}
Self {
position: BarPosition::default(),
height: default_bar_height(),
margin: MarginConfig::default(),
name: None,
start_hidden: None,
autohide: None,
popup_gap: default_popup_gap(),
icon_theme: None,
ironvar_defaults: None,
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
)]),
center: Some(vec![ModuleConfig::Focused(Box::default())]),
end,
anchor_to_edges: default_true(),
monitors: None,
}
}
}
const fn default_bar_height() -> i32 { const fn default_bar_height() -> i32 {
42 42
} }

View File

@@ -1,7 +1,8 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::fs::File;
use std::fs; use std::io;
use std::io::BufRead;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
use tracing::warn; use tracing::warn;
@@ -28,14 +29,6 @@ fn find_application_dirs() -> Vec<PathBuf> {
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
]; ];
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
if let Some(xdg_dirs) = xdg_dirs {
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
xdg_dir.push("applications");
dirs.push(xdg_dir);
}
}
let user_dir = dirs::data_local_dir(); // user installed apps let user_dir = dirs::data_local_dir(); // user installed apps
if let Some(mut user_dir) = user_dir { if let Some(mut user_dir) = user_dir {
user_dir.push("applications"); user_dir.push("applications");
@@ -65,40 +58,33 @@ pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
// this is necessary to invalidate the cache // this is necessary to invalidate the cache
let files = find_desktop_files(); let files = find_desktop_files();
find_desktop_file_by_filename(app_id, &files) if let Some(path) = find_desktop_file_by_filename(app_id, &files) {
.or_else(|| find_desktop_file_by_filedata(app_id, &files)) return Some(path);
}
find_desktop_file_by_filedata(app_id, &files)
} }
/// Finds the correct desktop file using a simple condition check /// Finds the correct desktop file using a simple condition check
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> { fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let with_names = files let app_id = app_id.to_lowercase();
.iter()
.map(|f| {
(
f,
f.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase(),
)
})
.collect::<Vec<_>>();
with_names files
.iter() .iter()
// first pass - check for exact match .find(|file| {
.find(|(_, name)| name.eq_ignore_ascii_case(app_id)) let file_name: String = file
// second pass - check for substring .file_name()
.or_else(|| { .expect("file name doesn't end with ...")
with_names.iter().find(|(_, name)| { .to_string_lossy()
// this will attempt to find flatpak apps that are in the format .to_lowercase();
file_name.contains(&app_id)
|| app_id
.split(&['-', ' ', ':', '@', '.', '_'][..])
.any(|part| file_name.contains(part)) // this will attempt to find flatpak apps that are like this
// `com.company.app` or `com.app.something` // `com.company.app` or `com.app.something`
app_id
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part))
}) })
}) .map(ToOwned::to_owned)
.map(|(file, _)| file.into())
} }
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS` /// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
@@ -106,68 +92,38 @@ fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<Path
let app_id = &app_id.to_lowercase(); let app_id = &app_id.to_lowercase();
let mut desktop_files_cache = lock!(DESKTOP_FILES); let mut desktop_files_cache = lock!(DESKTOP_FILES);
let files = files files
.iter() .iter()
.filter_map(|file| { .filter_map(|file| {
let Some(parsed_desktop_file) = parse_desktop_file(file) else { let Some(parsed_desktop_file) = parse_desktop_file(file) else { return None };
return None;
};
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone()); desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
Some((file.clone(), parsed_desktop_file)) Some((file.clone(), parsed_desktop_file))
}) })
.collect::<Vec<_>>();
let file = files
.iter()
// first pass - check name key for exact match
.find(|(_, desktop_file)| { .find(|(_, desktop_file)| {
desktop_file
.get("Name")
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
.unwrap_or_default()
})
// second pass - check name key for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file
.get("Name")
.map(|names| {
names
.iter()
.any(|name| name.to_lowercase().contains(app_id))
})
.unwrap_or_default()
})
})
// third pass - check all keys for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file desktop_file
.values() .values()
.flatten() .flatten()
.any(|value| value.to_lowercase().contains(app_id)) .any(|value| value.to_lowercase().contains(app_id))
}) })
}); .map(|(path, _)| path)
file.map(|(path, _)| path).cloned()
} }
/// Parses a desktop file into a hashmap of keys/vector(values). /// Parses a desktop file into a hashmap of keys/vector(values).
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> { fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
let Ok(file) = fs::read_to_string(path) else { let Ok(file) = File::open(path) else {
warn!("Couldn't Open File: {}", path.display()); warn!("Couldn't Open File: {}", path.display());
return None; return None;
}; };
let lines = io::BufReader::new(file).lines();
let mut desktop_file: DesktopFile = DesktopFile::new(); let mut desktop_file: DesktopFile = DesktopFile::new();
file.lines() let _ = lines.flatten().map(|line| {
.filter_map(|line| { line.split_once('=')
let Some((key, value)) = line.split_once('=') else { .iter()
return None; .filter_map(|(key, value)| {
};
let key = key.trim(); let key = key.trim();
let value = value.trim(); let value = value.trim();
@@ -180,18 +136,17 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
.for_each(|(key, value)| { .for_each(|(key, value)| {
desktop_file desktop_file
.entry(key.to_string()) .entry(key.to_string())
.or_default() .or_insert_with(Vec::new)
.push(value.to_string()); .push(value.to_string());
}); });
});
Some(desktop_file) Some(desktop_file)
} }
/// Attempts to get the icon name from the app's `.desktop` file. /// Attempts to get the icon name from the app's `.desktop` file.
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> { pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
let Some(path) = find_desktop_file(app_id) else { let Some(path) = find_desktop_file(app_id) else { return None };
return None;
};
let mut desktop_files_cache = lock!(DESKTOP_FILES); let mut desktop_files_cache = lock!(DESKTOP_FILES);

View File

@@ -1,10 +1,11 @@
use crate::script::Script;
use crate::{glib_recv_mpsc, spawn, try_send};
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
use crate::{send_async, Ironbar}; use crate::ironvar::get_variable_manager;
use crate::script::Script;
use crate::send;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use glib::Continue;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc; use tokio::spawn;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(untagged)] #[serde(untagged)]
@@ -17,9 +18,9 @@ pub enum DynamicBool {
} }
impl DynamicBool { impl DynamicBool {
pub fn subscribe<F>(self, mut f: F) pub fn subscribe<F>(self, f: F)
where where
F: FnMut(bool) + 'static, F: FnMut(bool) -> Continue + 'static,
{ {
let value = match self { let value = match self {
Self::Unknown(input) => { Self::Unknown(input) => {
@@ -39,29 +40,29 @@ impl DynamicBool {
_ => self, _ => self,
}; };
let (tx, rx) = mpsc::channel(32); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
glib_recv_mpsc!(rx, val => f(val)); rx.attach(None, f);
spawn(async move { spawn(async move {
match value { match value {
DynamicBool::Script(script) => { DynamicBool::Script(script) => {
script script
.run(None, |_, success| { .run(None, |_, success| {
try_send!(tx, success); send!(tx, success);
}) })
.await; .await;
} }
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
DynamicBool::Variable(variable) => { DynamicBool::Variable(variable) => {
let variable_manager = Ironbar::variable_manager(); let variable_manager = get_variable_manager();
let variable_name = variable[1..].into(); // remove hash let variable_name = variable[1..].into(); // remove hash
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name); let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
while let Ok(value) = rx.recv().await { while let Ok(value) = rx.recv().await {
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default(); let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
send_async!(tx, has_value); send!(tx, has_value);
} }
} }
DynamicBool::Unknown(_) => unreachable!(), DynamicBool::Unknown(_) => unreachable!(),
@@ -70,10 +71,7 @@ impl DynamicBool {
} }
} }
/// Check if a string ironvar is 'truthy', /// Check if a string ironvar is 'truthy'
/// i.e should be evaluated to true.
///
/// This loosely follows the common JavaScript cases.
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
fn is_truthy(string: &str) -> bool { fn is_truthy(string: &str) -> bool {
!(string.is_empty() || string == "0" || string == "false") !(string.is_empty() || string == "0" || string == "false")

View File

@@ -1,8 +1,10 @@
use crate::script::{OutputStream, Script};
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
use crate::Ironbar; use crate::ironvar::get_variable_manager;
use crate::{arc_mut, glib_recv_mpsc, lock, spawn, try_send}; use crate::script::{OutputStream, Script};
use tokio::sync::mpsc; use crate::{lock, send};
use gtk::prelude::*;
use std::sync::{Arc, Mutex};
use tokio::spawn;
/// A segment of a dynamic string, /// A segment of a dynamic string,
/// containing either a static string /// containing either a static string
@@ -23,16 +25,17 @@ enum DynamicStringSegment {
/// ```rs /// ```rs
/// dynamic_string(&text, move |string| { /// dynamic_string(&text, move |string| {
/// label.set_markup(&string); /// label.set_markup(&string);
/// Continue(true)
/// }); /// });
/// ``` /// ```
pub fn dynamic_string<F>(input: &str, mut f: F) pub fn dynamic_string<F>(input: &str, f: F)
where where
F: FnMut(String) + 'static, F: FnMut(String) -> Continue + 'static,
{ {
let tokens = parse_input(input); let tokens = parse_input(input);
let label_parts = arc_mut!(vec![]); let label_parts = Arc::new(Mutex::new(Vec::new()));
let (tx, rx) = mpsc::channel(32); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in tokens.into_iter().enumerate() { for (i, segment) in tokens.into_iter().enumerate() {
match segment { match segment {
@@ -55,7 +58,7 @@ where
let _: String = std::mem::replace(&mut label_parts[i], out); let _: String = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join(""); let string = label_parts.join("");
try_send!(tx, string); send!(tx, string);
} }
}) })
.await; .await;
@@ -70,7 +73,7 @@ where
lock!(label_parts).push(String::new()); lock!(label_parts).push(String::new());
spawn(async move { spawn(async move {
let variable_manager = Ironbar::variable_manager(); let variable_manager = get_variable_manager();
let mut rx = crate::write_lock!(variable_manager).subscribe(name); let mut rx = crate::write_lock!(variable_manager).subscribe(name);
while let Ok(value) = rx.recv().await { while let Ok(value) = rx.recv().await {
@@ -80,7 +83,7 @@ where
let _: String = std::mem::replace(&mut label_parts[i], value); let _: String = std::mem::replace(&mut label_parts[i], value);
let string = label_parts.join(""); let string = label_parts.join("");
try_send!(tx, string); send!(tx, string);
} }
} }
}); });
@@ -88,12 +91,12 @@ where
} }
} }
glib_recv_mpsc!(rx , val => f(val)); rx.attach(None, f);
// initialize // initialize
{ {
let label_parts = lock!(label_parts).join(""); let label_parts = lock!(label_parts).join("");
try_send!(tx, label_parts); send!(tx, label_parts);
} }
} }
@@ -142,7 +145,7 @@ fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
.map(|w| w[0]) .map(|w| w[0])
.collect::<String>(); .collect::<String>();
let len = str.chars().count() + SKIP_BRACKETS; let len = str.len() + SKIP_BRACKETS;
let script = Script::from(str.as_str()); let script = Script::from(str.as_str());
(DynamicStringSegment::Script(script), len) (DynamicStringSegment::Script(script), len)
@@ -158,7 +161,7 @@ fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
.take_while(|&c| !c.is_whitespace()) .take_while(|&c| !c.is_whitespace())
.collect::<String>(); .collect::<String>();
let len = str.chars().count() + SKIP_HASH; let len = str.len() + SKIP_HASH;
let value = str.into(); let value = str.into();
(DynamicStringSegment::Variable(value), len) (DynamicStringSegment::Variable(value), len)
@@ -171,16 +174,15 @@ fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
.map(|w| w[0]) .map(|w| w[0])
.collect::<String>(); .collect::<String>();
let mut char_count = str.chars().count();
// if segment is at end of string, last char gets missed above due to uneven window. // if segment is at end of string, last char gets missed above due to uneven window.
if chars.len() == char_count + 1 { if chars.len() == str.len() + 1 {
let remaining_char = *chars.get(char_count).expect("Failed to find last char"); let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
str.push(remaining_char); str.push(remaining_char);
char_count += 1;
} }
(DynamicStringSegment::Static(str), char_count) let len = str.len();
(DynamicStringSegment::Static(str), len)
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -2,6 +2,7 @@
pub enum ExitCode { pub enum ExitCode {
GtkDisplay = 1, GtkDisplay = 1,
CreateBars = 2, CreateBars = 2,
Config = 3,
} }
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen"; pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen";

View File

@@ -1,77 +1,8 @@
use glib::IsA; use glib::IsA;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Orientation, Widget}; use gtk::Widget;
/// Represents a widget's size /// Adds a new CSS class to a widget.
/// and location relative to the bar's start edge. pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
#[derive(Debug, Copy, Clone)] widget.style_context().add_class(class);
pub struct WidgetGeometry {
/// Position of the start edge of the widget
/// from the start edge of the bar.
pub position: i32,
/// The length of the widget.
pub size: i32,
/// The length of the bar.
pub bar_size: i32,
}
pub trait IronbarGtkExt {
/// Adds a new CSS class to the widget.
fn add_class(&self, class: &str);
/// Gets the geometry for the widget
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
/// Gets a data tag on a widget, if it exists.
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
/// Sets a data tag on a widget.
fn set_tag<V: 'static>(&self, key: &str, value: V);
}
impl<W: IsA<Widget>> IronbarGtkExt for W {
fn add_class(&self, class: &str) {
self.style_context().add_class(class);
}
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
let allocation = self.allocation();
let widget_size = if orientation == Orientation::Horizontal {
allocation.width()
} else {
allocation.height()
};
let top_level = self.toplevel().expect("Failed to get top-level widget");
let top_level_allocation = top_level.allocation();
let bar_size = if orientation == Orientation::Horizontal {
top_level_allocation.width()
} else {
top_level_allocation.height()
};
let (widget_x, widget_y) = self
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_pos = if orientation == Orientation::Horizontal {
widget_x
} else {
widget_y
};
WidgetGeometry {
position: widget_pos,
size: widget_size,
bar_size,
}
}
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
unsafe { self.data(key).map(|val| val.as_ref()) }
}
fn set_tag<V: 'static>(&self, key: &str, value: V) {
unsafe { self.set_data(key, value) }
}
} }

View File

@@ -1,5 +1,5 @@
use super::ImageProvider; use super::ImageProvider;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::add_class;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation}; use gtk::{Button, IconTheme, Image, Label, Orientation};
@@ -9,10 +9,10 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
image.add_class("image"); add_class(&image, "image");
image.add_class("icon"); add_class(&image, "icon");
match ImageProvider::parse(input, icon_theme, false, size) match ImageProvider::parse(input, icon_theme, size)
.map(|provider| provider.load_into_image(image.clone())) .map(|provider| provider.load_into_image(image.clone()))
{ {
Some(_) => { Some(_) => {
@@ -36,17 +36,17 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
image.add_class("icon"); add_class(&image, "icon");
image.add_class("image"); add_class(&image, "image");
container.add(&image); container.add(&image);
ImageProvider::parse(input, icon_theme, false, size) ImageProvider::parse(input, icon_theme, size)
.map(|provider| provider.load_into_image(image)); .map(|provider| provider.load_into_image(image));
} else { } else {
let label = Label::new(Some(input)); let label = Label::new(Some(input));
label.add_class("icon"); add_class(&label, "icon");
label.add_class("text-icon"); add_class(&label, "text-icon");
container.add(&label); container.add(&label);
} }

View File

@@ -1,6 +1,4 @@
use crate::desktop_file::get_desktop_icon_name; use crate::desktop_file::get_desktop_icon_name;
#[cfg(feature = "http")]
use crate::{glib_recv_mpsc, send_async, spawn};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::{Help, Report, Result}; use color_eyre::{Help, Report, Result};
use gtk::cairo::Surface; use gtk::cairo::Surface;
@@ -9,13 +7,13 @@ use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme}; use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(feature = "http")]
use tokio::sync::mpsc;
use tracing::warn; use tracing::warn;
cfg_if!( cfg_if!(
if #[cfg(feature = "http")] { if #[cfg(feature = "http")] {
use crate::send;
use gtk::gio::{Cancellable, MemoryInputStream}; use gtk::gio::{Cancellable, MemoryInputStream};
use tokio::spawn;
use tracing::error; use tracing::error;
} }
); );
@@ -43,44 +41,23 @@ impl<'a> ImageProvider<'a> {
/// ///
/// Note this checks that icons exist in theme, or files exist on disk /// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed. /// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> { pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?; let location = Self::get_location(input, theme, size)?;
Some(Self { location, size }) Some(Self { location, size })
} }
/// Returns true if the input starts with a prefix /// Returns true if the input starts with a prefix
/// that is supported by the parser /// that is supported by the parser
/// (ie the parser would not fallback to checking the input). /// (ie the parser would not fallback to checking the input).
#[cfg(any(feature = "music", feature = "workspaces"))]
pub fn is_definitely_image_input(input: &str) -> bool { pub fn is_definitely_image_input(input: &str) -> bool {
input.starts_with("icon:") input.starts_with("icon:")
|| input.starts_with("file://") || input.starts_with("file://")
|| input.starts_with("http://") || input.starts_with("http://")
|| input.starts_with("https://") || input.starts_with("https://")
|| input.starts_with('/')
} }
fn get_location( fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
input: &str,
theme: &'a IconTheme,
size: i32,
use_fallback: bool,
recurse_depth: usize,
) -> Option<ImageLocation<'a>> {
macro_rules! fallback {
() => {
if use_fallback {
Some(Self::get_fallback_icon(theme))
} else {
None
}
};
}
const MAX_RECURSE_DEPTH: usize = 2;
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
let (input_type, input_name) = input let (input_type, input_name) = input
.split_once(':') .split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n)); .map_or((None, input), |(t, n)| (Some(t), n));
@@ -115,26 +92,21 @@ impl<'a> ImageProvider<'a> {
Report::msg(format!("Unsupported image type: {input_type}")) Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available") .note("You may need to recompile with support if available")
); );
fallback!() None
} }
None if PathBuf::from(input_name).is_file() => { None if PathBuf::from(input_name).is_file() => {
Some(ImageLocation::Local(PathBuf::from(input_name))) Some(ImageLocation::Local(PathBuf::from(input_name)))
} }
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(), None => {
None if should_parse_desktop_file => { if let Some(location) = get_desktop_icon_name(input_name)
if let Some(location) = get_desktop_icon_name(input_name).map(|input| { .map(|input| Self::get_location(&input, theme, size))
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1) {
}) {
location location
} else { } else {
warn!("Failed to find image: {input}"); warn!("Failed to find image: {input}");
fallback!() None
} }
} }
None => {
warn!("Failed to find image: {input}");
fallback!()
}
} }
} }
@@ -145,18 +117,18 @@ impl<'a> ImageProvider<'a> {
#[cfg(feature = "http")] #[cfg(feature = "http")]
if let ImageLocation::Remote(url) = &self.location { if let ImageLocation::Remote(url) = &self.location {
let url = url.clone(); let url = url.clone();
let (tx, rx) = mpsc::channel(64); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
let bytes = Self::get_bytes_from_http(url).await; let bytes = Self::get_bytes_from_http(url).await;
if let Ok(bytes) = bytes { if let Ok(bytes) = bytes {
send_async!(tx, bytes); send!(tx, bytes);
} }
}); });
{ {
let size = self.size; let size = self.size;
glib_recv_mpsc!(rx, bytes => { rx.attach(None, move |bytes| {
let stream = MemoryInputStream::from_bytes(&bytes); let stream = MemoryInputStream::from_bytes(&bytes);
let scale = image.scale_factor(); let scale = image.scale_factor();
@@ -177,6 +149,8 @@ impl<'a> ImageProvider<'a> {
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
_ => {} _ => {}
} }
Continue(false)
}); });
} }
} else { } else {
@@ -274,11 +248,4 @@ impl<'a> ImageProvider<'a> {
))) )))
} }
} }
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
ImageLocation::Icon {
name: "dialog-question-symbolic".to_string(),
theme,
}
}
} }

View File

@@ -1,10 +1,9 @@
use std::path::PathBuf;
use clap::Subcommand; use clap::Subcommand;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Subcommand, Debug, Serialize, Deserialize)] #[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type")]
pub enum Command { pub enum Command {
/// Return "ok" /// Return "ok"
Ping, Ping,
@@ -12,15 +11,12 @@ pub enum Command {
/// Open the GTK inspector /// Open the GTK inspector
Inspect, Inspect,
/// Reload the config
Reload,
/// Set an `ironvar` value. /// Set an `ironvar` value.
/// This creates it if it does not already exist, and updates it if it does. /// This creates it if it does not already exist, and updates it if it does.
/// Any references to this variable are automatically and immediately updated. /// Any references to this variable are automatically and immediately updated.
/// Keys and values can be any valid UTF-8 string. /// Keys and values can be any valid UTF-8 string.
Set { Set {
/// Variable key. Can be any alphanumeric ASCII string. /// Variable key. Can be any valid UTF-8 string.
key: Box<str>, key: Box<str>,
/// Variable value. Can be any valid UTF-8 string. /// Variable value. Can be any valid UTF-8 string.
value: String, value: String,
@@ -38,42 +34,4 @@ pub enum Command {
/// The path to the sheet. /// The path to the sheet.
path: PathBuf, path: PathBuf,
}, },
/// Set the visibility of the bar with the given name.
SetVisible {
///Bar name to target.
bar_name: String,
/// The visibility status.
#[arg(short, long)]
visible: bool,
},
/// Get the visibility of the bar with the given name.
GetVisible {
/// Bar name to target.
bar_name: String,
},
/// Toggle a popup open/closed.
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
TogglePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Open a popup, regardless of current state.
OpenPopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Close a popup, regardless of current state.
ClosePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
},
} }

View File

@@ -3,7 +3,7 @@ pub mod commands;
pub mod responses; pub mod responses;
mod server; mod server;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use tracing::warn; use tracing::warn;
pub use commands::Command; pub use commands::Command;
@@ -30,8 +30,4 @@ impl Ipc {
path: ipc_socket_file, path: ipc_socket_file,
} }
} }
pub fn path(&self) -> &Path {
self.path.as_path()
}
} }

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type")]
pub enum Response { pub enum Response {
Ok, Ok,
OkValue { value: String }, OkValue { value: String },

View File

@@ -1,28 +1,26 @@
use std::fs; use super::Ipc;
use std::path::Path; use crate::bridge_channel::BridgeChannel;
use std::rc::Rc; use crate::ipc::{Command, Response};
use crate::ironvar::get_variable_manager;
use crate::style::load_css;
use crate::{read_lock, send_async, try_send, write_lock};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use glib::Continue;
use gtk::Application; use std::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream}; use tokio::net::{UnixListener, UnixStream};
use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::ipc::{Command, Response};
use crate::modules::PopupButton;
use crate::style::load_css;
use crate::{glib_recv_mpsc, read_lock, send_async, spawn, try_send, write_lock, Ironbar};
use super::Ipc;
impl Ipc { impl Ipc {
/// Starts the IPC server on its socket. /// Starts the IPC server on its socket.
/// ///
/// Once started, the server will begin accepting connections. /// Once started, the server will begin accepting connections.
pub fn start(&self, application: &Application, ironbar: Rc<Ironbar>) { pub fn start(&self) {
let (cmd_tx, cmd_rx) = mpsc::channel(32); let bridge = BridgeChannel::<Command>::new();
let cmd_tx = bridge.create_sender();
let (res_tx, mut res_rx) = mpsc::channel(32); let (res_tx, mut res_rx) = mpsc::channel(32);
let path = self.path.clone(); let path = self.path.clone();
@@ -30,7 +28,7 @@ impl Ipc {
if path.exists() { if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?"); warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address"); warn!("Attempting IPC shutdown to allow binding to address");
Self::shutdown(&path); self.shutdown();
} }
spawn(async move { spawn(async move {
@@ -63,10 +61,10 @@ impl Ipc {
} }
}); });
let application = application.clone(); bridge.recv(move |command| {
glib_recv_mpsc!(cmd_rx, command => { let res = Self::handle_command(command);
let res = Self::handle_command(command, &application, &ironbar);
try_send!(res_tx, res); try_send!(res_tx, res);
Continue(true)
}); });
} }
@@ -104,33 +102,22 @@ impl Ipc {
/// Takes an input command, runs it and returns with the appropriate response. /// Takes an input command, runs it and returns with the appropriate response.
/// ///
/// This runs on the main thread, allowing commands to interact with GTK. /// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(command: Command, application: &Application, ironbar: &Ironbar) -> Response { fn handle_command(command: Command) -> Response {
match command { match command {
Command::Inspect => { Command::Inspect => {
gtk::Window::set_interactive_debugging(true); gtk::Window::set_interactive_debugging(true);
Response::Ok Response::Ok
} }
Command::Reload => {
info!("Closing existing bars");
let windows = application.windows();
for window in windows {
window.close();
}
*ironbar.bars.borrow_mut() = crate::load_interface(application);
Response::Ok
}
Command::Set { key, value } => { Command::Set { key, value } => {
let variable_manager = Ironbar::variable_manager(); let variable_manager = get_variable_manager();
let mut variable_manager = write_lock!(variable_manager); let mut variable_manager = write_lock!(variable_manager);
match variable_manager.set(key, value) { match variable_manager.set(key, value) {
Ok(()) => Response::Ok, Ok(_) => Response::Ok,
Err(err) => Response::error(&format!("{err}")), Err(err) => Response::error(&format!("{err}")),
} }
} }
Command::Get { key } => { Command::Get { key } => {
let variable_manager = Ironbar::variable_manager(); let variable_manager = get_variable_manager();
let value = read_lock!(variable_manager).get(&key); let value = read_lock!(variable_manager).get(&key);
match value { match value {
Some(value) => Response::OkValue { value }, Some(value) => Response::OkValue { value },
@@ -145,124 +132,13 @@ impl Ipc {
Response::error("File not found") Response::error("File not found")
} }
} }
Command::TogglePopup { bar_name, name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
let current_widget = popup.borrow().current_widget();
popup.borrow_mut().hide();
let data = popup
.borrow()
.cache
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
match data {
Some((id, Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
} else {
popup.show(id, button_id);
}
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
Some(_) => Response::Ok,
None => Response::error("Invalid module name"),
}
}
None => Response::error("Invalid bar name"),
}
}
Command::OpenPopup { bar_name, name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
// only one popup per bar, so hide if open for another widget
popup.borrow_mut().hide();
let data = popup
.borrow()
.cache
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
match data {
Some((id, Some(button))) => {
let button_id = button.popup_id();
popup.borrow_mut().show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
None => Response::error("Invalid module name"),
}
}
None => Response::error("Invalid bar name"),
}
}
Command::ClosePopup { bar_name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
popup.borrow_mut().hide();
Response::Ok
}
None => Response::error("Invalid bar name"),
}
}
Command::Ping => Response::Ok, Command::Ping => Response::Ok,
Command::SetVisible { bar_name, visible } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
window.set_visible(visible);
Response::Ok
} else {
Response::error("Bar not found")
}
}
Command::GetVisible { bar_name } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
Response::OkValue {
value: window.is_visible().to_string(),
}
} else {
Response::error("Bar not found")
}
}
} }
} }
/// Shuts down the IPC server, /// Shuts down the IPC server,
/// removing the socket file in the process. /// removing the socket file in the process.
/// pub fn shutdown(&self) {
/// Note this is static as the `Ipc` struct is not `Send`. fs::remove_file(&self.path).ok();
pub fn shutdown<P: AsRef<Path>>(path: P) {
fs::remove_file(&path).ok();
} }
} }

View File

@@ -1,21 +1,25 @@
#![doc = include_str!("../docs/Ironvars.md")] #![doc = include_str!("../docs/Ironvars.md")]
use crate::send; use crate::{arc_rw, send};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio::sync::broadcast; use tokio::sync::broadcast;
lazy_static! {
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
}
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
VARIABLE_MANAGER.clone()
}
/// Global singleton manager for `IronVar` variables. /// Global singleton manager for `IronVar` variables.
pub struct VariableManager { pub struct VariableManager {
variables: HashMap<Box<str>, IronVar>, variables: HashMap<Box<str>, IronVar>,
} }
impl Default for VariableManager {
fn default() -> Self {
Self::new()
}
}
impl VariableManager { impl VariableManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -43,59 +43,6 @@ macro_rules! try_send {
}; };
} }
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
/// in a loop.
///
/// This allows use of `GObjects` and futures in the same context.
///
/// For use with receivers which return a `Result`.
///
/// # Example
///
/// ```rs
/// let (tx, mut rx) = broadcast::channel(32);
/// glib_recv(rx, msg => println!("{msg}"));
/// ```
#[macro_export]
macro_rules! glib_recv {
($rx:expr, $val:ident => $expr:expr) => {{
glib::spawn_future_local(async move {
// re-delcare in case ie `context.subscribe()` is passed directly
let mut rx = $rx;
while let Ok($val) = rx.recv().await {
$expr
}
});
}};
}
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
/// in a loop.
///
/// This allows use of `GObjects` and futures in the same context.
///
/// For use with receivers which return an `Option`,
/// such as Tokio's `mpsc` channel.
///
/// # Example
///
/// ```rs
/// let (tx, mut rx) = broadcast::channel(32);
/// glib_recv_mpsc(rx, msg => println!("{msg}"));
/// ```
#[macro_export]
macro_rules! glib_recv_mpsc {
($rx:expr, $val:ident => $expr:expr) => {{
glib::spawn_future_local(async move {
// re-delcare in case ie `context.subscribe()` is passed directly
let mut rx = $rx;
while let Some($val) = rx.recv().await {
$expr
}
});
}};
}
/// Locks a `Mutex`. /// Locks a `Mutex`.
/// Panics if the `Mutex` cannot be locked. /// Panics if the `Mutex` cannot be locked.
/// ///
@@ -153,7 +100,7 @@ macro_rules! write_lock {
#[macro_export] #[macro_export]
macro_rules! arc_mut { macro_rules! arc_mut {
($val:expr) => { ($val:expr) => {
std::sync::Arc::new(std::sync::Mutex::new($val)) std::sync::Arc::new(std::Sync::Mutex::new($val))
}; };
} }

View File

@@ -1,41 +1,7 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use std::cell::RefCell;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[cfg(feature = "ipc")]
use std::sync::RwLock;
use std::sync::{mpsc, Arc};
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use glib::PropertySet;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use tokio::runtime::Runtime;
use tokio::task::{block_in_place, JoinHandle};
use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader;
use clients::wayland;
use crate::bar::{create_bar, Bar};
use crate::config::{Config, MonitorConfig};
use crate::error::ExitCode;
#[cfg(feature = "ipc")]
use crate::ironvar::VariableManager;
use crate::style::load_css;
mod bar; mod bar;
mod bridge_channel;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
mod cli; mod cli;
mod clients; mod clients;
@@ -55,16 +21,45 @@ mod modules;
mod popup; mod popup;
mod script; mod script;
mod style; mod style;
mod unique_id;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use std::cell::Cell;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use tokio::runtime::Handle;
use tokio::task::{block_in_place, spawn_blocking};
use crate::error::ExitCode;
use clients::wayland;
use tracing::{debug, error, info};
use universal_config::ConfigLoader;
const GTK_APP_ID: &str = "dev.jstanger.ironbar"; const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() { #[tokio::main]
async fn main() {
let _guard = logging::install_logging(); let _guard = logging::install_logging();
cfg_if! { cfg_if! {
if #[cfg(feature = "cli")] { if #[cfg(feature = "cli")] {
run_with_args(); run_with_args().await;
} else { } else {
start_ironbar(); start_ironbar();
} }
@@ -72,63 +67,32 @@ fn main() {
} }
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
fn run_with_args() { async fn run_with_args() {
let args = cli::Args::parse(); let args = cli::Args::parse();
match args.command { match args.command {
Some(command) => { Some(command) => {
let rt = create_runtime();
rt.block_on(async move {
let ipc = ipc::Ipc::new(); let ipc = ipc::Ipc::new();
match ipc.send(command).await { match ipc.send(command).await {
Ok(res) => cli::handle_response(res), Ok(res) => cli::handle_response(res),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}; };
});
} }
None => start_ironbar(), None => start_ironbar(),
} }
} }
static COUNTER: AtomicUsize = AtomicUsize::new(1); fn start_ironbar() {
lazy_static::lazy_static! {
static ref RUNTIME: Arc<Runtime> = Arc::new(create_runtime());
}
#[cfg(feature = "ipc")]
lazy_static::lazy_static! {
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
}
#[derive(Debug)]
pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
}
impl Ironbar {
fn new() -> Self {
Self {
bars: Rc::new(RefCell::new(vec![])),
}
}
fn start(self) {
info!("Ironbar version {}", VERSION); info!("Ironbar version {}", VERSION);
info!("Starting application"); info!("Starting application");
let app = Application::builder().application_id(GTK_APP_ID).build(); let app = Application::builder().application_id(GTK_APP_ID).build();
let _ = wayland::get_client(); // force-init
let running = AtomicBool::new(false); let running = Rc::new(Cell::new(false));
let instance = Rc::new(self);
// force start wayland client ahead of ui
let wl = wayland::get_client();
lock!(wl).roundtrip();
app.connect_activate(move |app| { app.connect_activate(move |app| {
if running.load(Ordering::Relaxed) { if running.get() {
info!("Ironbar already running, returning"); info!("Ironbar already running, returning");
return; return;
} }
@@ -138,11 +102,50 @@ impl Ironbar {
cfg_if! { cfg_if! {
if #[cfg(feature = "ipc")] { if #[cfg(feature = "ipc")] {
let ipc = ipc::Ipc::new(); let ipc = ipc::Ipc::new();
ipc.start(app, instance.clone()); ipc.start();
} }
} }
*instance.bars.borrow_mut() = load_interface(app); let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
);
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
);
let mut config: Config = match config_res {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
exit(ExitCode::Config as i32)
}
};
debug!("Loaded config file");
#[cfg(feature = "ipc")]
if let Some(ironvars) = config.ironvar_defaults.take() {
let variable_manager = ironvar::get_variable_manager();
for (k, v) in ironvars {
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
tracing::warn!("Ignoring invalid ironvar: '{k}'");
}
}
}
if let Err(err) = create_bars(app, &display, &config) {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
debug!("Created bars");
let style_path = env::var("IRONBAR_CSS").ok().map_or_else( let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| { || {
@@ -164,24 +167,19 @@ impl Ironbar {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
#[cfg(feature = "ipc")]
let ipc_path = ipc.path().to_path_buf();
spawn_blocking(move || { spawn_blocking(move || {
rx.recv().expect("to receive from channel"); rx.recv().expect("to receive from channel");
info!("Shutting down"); info!("Shutting down");
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
ipc::Ipc::shutdown(ipc_path); ipc.shutdown();
exit(0); exit(0);
}); });
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
// TODO: Start wayland client - listen for outputs
// All bar loading should happen as an event response to this
}); });
// Ignore CLI args // Ignore CLI args
@@ -189,95 +187,8 @@ impl Ironbar {
app.run_with_args(&Vec::<&str>::new()); app.run_with_args(&Vec::<&str>::new());
} }
/// Gets the current Tokio runtime.
#[must_use]
pub fn runtime() -> Arc<Runtime> {
RUNTIME.clone()
}
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
/// This is just a static `AtomicUsize` that increments every time this function is called.
pub fn unique_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
/// Gets the `Ironvar` manager singleton.
#[cfg(feature = "ipc")]
#[must_use]
pub fn variable_manager() -> Arc<RwLock<VariableManager>> {
VARIABLE_MANAGER.clone()
}
/// Gets a clone of a bar by its unique name.
///
/// Since the bar contains mostly GTK objects,
/// the clone is cheap enough to not worry about.
#[must_use]
pub fn bar_by_name(&self, name: &str) -> Option<Bar> {
self.bars
.borrow()
.iter()
.find(|&bar| bar.name() == name)
.cloned()
}
}
fn start_ironbar() {
let ironbar = Ironbar::new();
ironbar.start();
}
/// Loads the Ironbar config and interface.
pub fn load_interface(app: &Application) -> Vec<Bar> {
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
);
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
debug!("Loaded config file");
#[cfg(feature = "ipc")]
if let Some(ironvars) = config.ironvar_defaults.take() {
let variable_manager = Ironbar::variable_manager();
for (k, v) in ironvars {
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
warn!("Ignoring invalid ironvar: '{k}'");
}
}
}
match create_bars(app, &display, &config) {
Ok(bars) => {
debug!("Created {} bars", bars.len());
bars
}
Err(err) => {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
}
}
/// Creates each of the bars across each of the (configured) outputs. /// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<Vec<Bar>> { fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
let wl = wayland::get_client(); let wl = wayland::get_client();
let outputs = lock!(wl).get_outputs(); let outputs = lock!(wl).get_outputs();
@@ -286,10 +197,6 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
let num_monitors = display.n_monitors(); let num_monitors = display.n_monitors();
let show_default_bar =
config.start.is_some() || config.center.is_some() || config.end.is_some();
let mut all_bars = vec![];
for i in 0..num_monitors { for i in 0..num_monitors {
let monitor = display let monitor = display
.monitor(i) .monitor(i)
@@ -298,65 +205,35 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
.get(i as usize) .get(i as usize)
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?; .ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
let Some(monitor_name) = &output.name else { let Some(monitor_name) = &output.name else { continue };
continue;
};
let mut bars = match config config.monitors.as_ref().map_or_else(
.monitors || {
.as_ref() info!("Creating bar on '{}'", monitor_name);
.and_then(|config| config.get(monitor_name)) create_bar(app, &monitor, monitor_name, config.clone())
{ },
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => { Some(MonitorConfig::Single(config)) => {
vec![create_bar( info!("Creating bar on '{}'", monitor_name);
app, create_bar(app, &monitor, monitor_name, config.clone())
&monitor,
monitor_name.to_string(),
config.clone(),
)?]
} }
Some(MonitorConfig::Multiple(configs)) => configs Some(MonitorConfig::Multiple(configs)) => {
.iter() for config in configs {
.map(|config| create_bar(app, &monitor, monitor_name.to_string(), config.clone())) info!("Creating bar on '{}'", monitor_name);
.collect::<Result<_>>()?, create_bar(app, &monitor, monitor_name, config.clone())?;
None if show_default_bar => vec![create_bar(
app,
&monitor,
monitor_name.to_string(),
config.clone(),
)?],
None => vec![],
};
all_bars.append(&mut bars);
} }
Ok(all_bars) Ok(())
}
_ => Ok(()),
}
},
)?;
} }
fn create_runtime() -> Runtime { Ok(())
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio to create a valid runtime")
}
/// Calls `spawn` on the Tokio runtime.
pub fn spawn<F>(f: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
Ironbar::runtime().spawn(f)
}
/// Calls `spawn_blocking` on the Tokio runtime.
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
Ironbar::runtime().spawn_blocking(f)
} }
/// Blocks on a `Future` until it resolves. /// Blocks on a `Future` until it resolves.
@@ -370,5 +247,5 @@ where
/// ///
/// TODO: remove all instances of this once async trait funcs are stable /// TODO: remove all instances of this once async trait funcs are stable
pub fn await_sync<F: Future>(f: F) -> F::Output { pub fn await_sync<F: Future>(f: F) -> F::Output {
block_in_place(|| Ironbar::runtime().block_on(f)) block_in_place(|| Handle::current().block_on(f))
} }

View File

@@ -2,11 +2,9 @@ use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue}; use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, use crate::popup::Popup;
}; use crate::try_send;
use crate::{glib_recv, spawn, try_send};
use glib::Propagation;
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::*;
@@ -14,7 +12,8 @@ use gtk::{Button, EventBox, 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 tokio::sync::{broadcast, mpsc}; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error}; use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@@ -72,8 +71,8 @@ impl Module<Button> for ClipboardModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()> { ) -> color_eyre::Result<()> {
let max_items = self.max_items; let max_items = self.max_items;
@@ -125,27 +124,31 @@ impl Module<Button> for ClipboardModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<Button>> { ) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size); 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");
let tx = context.tx.clone();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id())); let pos = Popup::widget_geometry(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
}); });
let rx = context.subscribe(); // we need to bind to the receiver as the channel does not open
let popup = self // until the popup is first opened.
.into_popup(context.controller_tx, rx, info) context.widget_rx.attach(None, |_| Continue(true));
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleWidget {
widget: button,
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
} }
fn into_popup( fn into_popup(
self, self,
tx: mpsc::Sender<Self::ReceiveMessage>, tx: Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where
@@ -163,7 +166,7 @@ impl Module<Button> for ClipboardModule {
{ {
let hidden_option = hidden_option.clone(); let hidden_option = hidden_option.clone();
glib_recv!(rx, event => { rx.attach(None, move |event| {
match event { match event {
ControllerEvent::Add(id, item) => { ControllerEvent::Add(id, item) => {
debug!("Adding new value with ID {}", id); debug!("Adding new value with ID {}", id);
@@ -229,7 +232,7 @@ impl Module<Button> for ClipboardModule {
try_send!(tx, UIEvent::Copy(id)); try_send!(tx, UIEvent::Copy(id));
} }
Propagation::Stop Inhibit(true)
}, },
); );
} }
@@ -288,6 +291,8 @@ impl Module<Button> for ClipboardModule {
hidden_option.set_active(true); hidden_option.set_active(true);
} }
} }
Continue(true)
}); });
} }

View File

@@ -1,20 +1,18 @@
use std::env; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use chrono::{DateTime, Local, Locale}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use chrono::{DateTime, Local};
use color_eyre::Result; use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Align, Button, Calendar, Label, Orientation}; use gtk::{Align, Button, Calendar, Label, Orientation};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::{broadcast, mpsc}; use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep; use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ClockModule { pub struct ClockModule {
/// Date/time format string. /// Date/time format string.
@@ -25,48 +23,14 @@ pub struct ClockModule {
#[serde(default = "default_format")] #[serde(default = "default_format")]
format: String, format: String,
#[serde(default = "default_popup_format")]
format_popup: String,
#[serde(default = "default_locale")]
locale: String,
#[serde(flatten)] #[serde(flatten)]
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
impl Default for ClockModule {
fn default() -> Self {
ClockModule {
format: default_format(),
format_popup: default_popup_format(),
locale: default_locale(),
common: Some(CommonConfig::default()),
}
}
}
fn default_format() -> String { fn default_format() -> String {
String::from("%d/%m/%Y %H:%M") String::from("%d/%m/%Y %H:%M")
} }
fn default_popup_format() -> String {
String::from("%H:%M:%S")
}
fn default_locale() -> String {
env::var("LC_TIME")
.or_else(|_| env::var("LANG"))
.map_or_else(|_| "POSIX".to_string(), strip_tail)
}
fn strip_tail(string: String) -> String {
string
.split_once('.')
.map(|(head, _)| head.to_string())
.unwrap_or(string)
}
impl Module<Button> for ClockModule { impl Module<Button> for ClockModule {
type SendMessage = DateTime<Local>; type SendMessage = DateTime<Local>;
type ReceiveMessage = (); type ReceiveMessage = ();
@@ -96,57 +60,62 @@ impl Module<Button> for ClockModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<Button>> { ) -> Result<ModuleWidget<Button>> {
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.add(&label);
let tx = context.tx.clone(); let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id())); try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
);
}); });
let format = self.format.clone(); let format = self.format.clone();
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX); {
context.widget_rx.attach(None, move |date| {
let rx = context.subscribe(); let date_string = format!("{}", date.format(&format));
glib_recv!(rx, date => {
let date_string = format!("{}", date.format_localized(&format, locale));
label.set_label(&date_string); label.set_label(&date_string);
Continue(true)
}); });
}
let popup = self let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleWidget {
widget: button,
popup,
})
} }
fn into_popup( fn into_popup(
self, self,
_tx: mpsc::Sender<Self::ReceiveMessage>, _tx: mpsc::Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0); let container = gtk::Box::new(Orientation::Vertical, 0);
let clock = Label::builder().halign(Align::Center).build(); let clock = Label::builder().halign(Align::Center).build();
clock.add_class("calendar-clock"); add_class(&clock, "calendar-clock");
let format = "%H:%M:%S";
container.add(&clock); container.add(&clock);
let calendar = Calendar::new(); let calendar = Calendar::new();
calendar.add_class("calendar"); add_class(&calendar, "calendar");
container.add(&calendar); container.add(&calendar);
let format = self.format_popup; {
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX); rx.attach(None, move |date| {
let date_string = format!("{}", date.format(format));
glib_recv!(rx, date => {
let date_string = format!("{}", date.format_localized(&format, locale));
clock.set_label(&date_string); clock.set_label(&date_string);
Continue(true)
}); });
}
container.show_all(); container.show_all();

View File

@@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
if let Some(widgets) = self.widgets { if let Some(widgets) = self.widgets {
for widget in widgets { for widget in widgets {
widget.widget.add_to(&container, &context, widget.common); widget.widget.add_to(&container, context, widget.common);
} }
} }

View File

@@ -1,13 +1,11 @@
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use crate::dynamic_value::dynamic_string;
use crate::popup::Popup;
use crate::{build, try_send};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Label}; use gtk::{Button, Label};
use serde::Deserialize; use serde::Deserialize;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget { pub struct ButtonWidget {
name: Option<String>, name: Option<String>,
@@ -21,7 +19,6 @@ impl CustomWidget for ButtonWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let button = build!(self, Self::Widget); let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label { if let Some(text) = self.label {
let label = Label::new(None); let label = Label::new(None);
@@ -30,10 +27,12 @@ impl CustomWidget for ButtonWidget {
dynamic_string(&text, move |string| { dynamic_string(&text, move |string| {
label.set_markup(&string); label.set_markup(&string);
Continue(true)
}); });
} }
if let Some(exec) = self.on_click { if let Some(exec) = self.on_click {
let bar_orientation = context.bar_orientation;
let tx = context.tx.clone(); let tx = context.tx.clone();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
@@ -42,7 +41,7 @@ impl CustomWidget for ButtonWidget {
ExecEvent { ExecEvent {
cmd: exec.clone(), cmd: exec.clone(),
args: None, args: None,
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button geometry: Popup::widget_geometry(button, bar_orientation),
} }
); );
}); });

View File

@@ -1,12 +1,10 @@
use gtk::prelude::*; use super::{CustomWidget, CustomWidgetContext};
use gtk::Image;
use serde::Deserialize;
use crate::build; use crate::build;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use gtk::prelude::*;
use super::{CustomWidget, CustomWidgetContext}; use gtk::Image;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ImageWidget { pub struct ImageWidget {
@@ -32,8 +30,10 @@ impl CustomWidget for ImageWidget {
let icon_theme = context.icon_theme.clone(); let icon_theme = context.icon_theme.clone();
dynamic_string(&self.src, move |src| { dynamic_string(&self.src, move |src| {
ImageProvider::parse(&src, &icon_theme, false, self.size) ImageProvider::parse(&src, &icon_theme, self.size)
.map(|image| image.load_into_image(gtk_image.clone())); .map(|image| image.load_into_image(gtk_image.clone()));
Continue(true)
}); });
} }

View File

@@ -1,12 +1,10 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_value::dynamic_string;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget { pub struct LabelWidget {
name: Option<String>, name: Option<String>,
@@ -26,6 +24,7 @@ impl CustomWidget for LabelWidget {
let label = label.clone(); let label = label.clone();
dynamic_string(&self.label, move |string| { dynamic_string(&self.label, move |string| {
label.set_markup(&string); label.set_markup(&string);
Continue(true)
}); });
} }

View File

@@ -13,17 +13,17 @@ use crate::config::CommonConfig;
use crate::modules::custom::button::ButtonWidget; use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget; use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{ use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext, wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
}; };
use crate::popup::WidgetGeometry;
use crate::script::Script; use crate::script::Script;
use crate::{send_async, spawn}; use crate::send_async;
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation}; use gtk::{IconTheme, Orientation};
use serde::Deserialize; use serde::Deserialize;
use std::cell::RefCell; use tokio::spawn;
use std::rc::Rc; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::{broadcast, mpsc};
use tracing::{debug, error}; use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@@ -56,12 +56,11 @@ pub enum Widget {
Progress(ProgressWidget), Progress(ProgressWidget),
} }
#[derive(Clone)] #[derive(Clone, Copy)]
struct CustomWidgetContext<'a> { struct CustomWidgetContext<'a> {
tx: &'a mpsc::Sender<ExecEvent>, tx: &'a Sender<ExecEvent>,
bar_orientation: Orientation, bar_orientation: Orientation,
icon_theme: &'a IconTheme, icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
} }
trait CustomWidget { trait CustomWidget {
@@ -116,11 +115,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
impl Widget { impl Widget {
/// Creates this widget and adds it to the parent container /// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) { fn add_to(self, parent: &gtk::Box, context: CustomWidgetContext, common: CommonConfig) {
macro_rules! create { macro_rules! create {
($widget:expr) => { ($widget:expr) => {
wrap_widget( wrap_widget(
&$widget.into_widget(context.clone()), &$widget.into_widget(context),
common, common,
context.bar_orientation, context.bar_orientation,
) )
@@ -144,7 +143,7 @@ impl Widget {
pub struct ExecEvent { pub struct ExecEvent {
cmd: String, cmd: String,
args: Option<Vec<String>>, args: Option<Vec<String>>,
id: usize, geometry: WidgetGeometry,
} }
impl Module<gtk::Box> for CustomModule { impl Module<gtk::Box> for CustomModule {
@@ -158,8 +157,8 @@ impl Module<gtk::Box> for CustomModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
spawn(async move { spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
@@ -174,9 +173,9 @@ impl Module<gtk::Box> for CustomModule {
error!("{err:?}"); error!("{err:?}");
} }
} else if event.cmd == "popup:toggle" { } else if event.cmd == "popup:toggle" {
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id)); send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
} else if event.cmd == "popup:open" { } else if event.cmd == "popup:open" {
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id)); send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
} else if event.cmd == "popup:close" { } else if event.cmd == "popup:close" {
send_async!(tx, ModuleUpdateEvent::ClosePopup); send_async!(tx, ModuleUpdateEvent::ClosePopup);
} else { } else {
@@ -192,30 +191,25 @@ impl Module<gtk::Box> for CustomModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build(); let container = gtk::Box::builder().orientation(orientation).build();
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext { let custom_context = CustomWidgetContext {
tx: &context.controller_tx, tx: &context.controller_tx,
bar_orientation: orientation, bar_orientation: orientation,
icon_theme: info.icon_theme, icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
}; };
self.bar.clone().into_iter().for_each(|widget| { self.bar.clone().into_iter().for_each(|widget| {
widget widget
.widget .widget
.add_to(&container, &custom_context, widget.common); .add_to(&container, custom_context, widget.common);
}); });
let popup = self let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup, popup,
}) })
@@ -223,8 +217,8 @@ impl Module<gtk::Box> for CustomModule {
fn into_popup( fn into_popup(
self, self,
tx: mpsc::Sender<Self::ReceiveMessage>, tx: Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>, _rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where
@@ -237,13 +231,12 @@ impl Module<gtk::Box> for CustomModule {
tx: &tx, tx: &tx,
bar_orientation: info.bar_position.get_orientation(), bar_orientation: info.bar_position.get_orientation(),
icon_theme: info.icon_theme, icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
}; };
for widget in popup { for widget in popup {
widget widget
.widget .widget
.add_to(&container, &custom_context, widget.common); .add_to(&container, custom_context, widget.common);
} }
} }

View File

@@ -1,15 +1,13 @@
use gtk::prelude::*; use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use gtk::ProgressBar;
use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::error;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length; use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput}; use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send}; use crate::{build, send};
use gtk::prelude::*;
use super::{try_get_orientation, CustomWidget, CustomWidgetContext}; use gtk::ProgressBar;
use serde::Deserialize;
use tokio::spawn;
use tracing::error;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget { pub struct ProgressWidget {
@@ -47,13 +45,13 @@ impl CustomWidget for ProgressWidget {
let script = Script::from(value); let script = Script::from(value);
let progress = progress.clone(); let progress = progress.clone();
let (tx, rx) = mpsc::channel(128); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
script script
.run(None, move |stream, _success| match stream { .run(None, move |stream, _success| match stream {
OutputStream::Stdout(out) => match out.parse::<f64>() { OutputStream::Stdout(out) => match out.parse::<f64>() {
Ok(value) => try_send!(tx, value), Ok(value) => send!(tx, value),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}, },
OutputStream::Stderr(err) => error!("{err:?}"), OutputStream::Stderr(err) => error!("{err:?}"),
@@ -61,7 +59,10 @@ impl CustomWidget for ProgressWidget {
.await; .await;
}); });
glib_recv_mpsc!(rx, value => progress.set_fraction(value / self.max)); rx.attach(None, move |value| {
progress.set_fraction(value / self.max);
Continue(true)
});
} }
if let Some(text) = self.label { if let Some(text) = self.label {
@@ -70,6 +71,7 @@ impl CustomWidget for ProgressWidget {
dynamic_string(&text, move |string| { dynamic_string(&text, move |string| {
progress.set_text(Some(&string)); progress.set_text(Some(&string));
Continue(true)
}); });
} }

View File

@@ -1,19 +1,16 @@
use glib::Propagation; use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use std::cell::Cell; use crate::modules::custom::set_length;
use std::ops::Neg; use crate::popup::Popup;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Scale; use gtk::Scale;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc; use std::cell::Cell;
use std::ops::Neg;
use tokio::spawn;
use tracing::error; use tracing::error;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, glib_recv_mpsc, spawn, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget { pub struct SliderWidget {
name: Option<String>, name: Option<String>,
@@ -78,10 +75,10 @@ impl CustomWidget for SliderWidget {
}; };
scale.set_value(value + delta); scale.set_value(value + delta);
Propagation::Proceed Inhibit(false)
}); });
scale.connect_change_value(move |_, _, val| { scale.connect_change_value(move |scale, _, val| {
// GTK will send values outside min/max range // GTK will send values outside min/max range
let val = val.clamp(min, max); let val = val.clamp(min, max);
@@ -91,14 +88,14 @@ impl CustomWidget for SliderWidget {
ExecEvent { ExecEvent {
cmd: on_change.clone(), cmd: on_change.clone(),
args: Some(vec![val.to_string()]), args: Some(vec![val.to_string()]),
id: usize::MAX // ignored geometry: Popup::widget_geometry(scale, context.bar_orientation),
} }
); );
prev_value.set(val); prev_value.set(val);
} }
Propagation::Proceed Inhibit(false)
}); });
} }
@@ -106,13 +103,13 @@ impl CustomWidget for SliderWidget {
let script = Script::from(value); let script = Script::from(value);
let scale = scale.clone(); let scale = scale.clone();
let (tx, rx) = mpsc::channel(128); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
script script
.run(None, move |stream, _success| match stream { .run(None, move |stream, _success| match stream {
OutputStream::Stdout(out) => match out.parse() { OutputStream::Stdout(out) => match out.parse() {
Ok(value) => try_send!(tx, value), Ok(value) => send!(tx, value),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}, },
OutputStream::Stderr(err) => error!("{err:?}"), OutputStream::Stderr(err) => error!("{err:?}"),
@@ -120,7 +117,10 @@ impl CustomWidget for SliderWidget {
.await; .await;
}); });
glib_recv_mpsc!(rx, value => scale.set_value(value)); rx.attach(None, move |value| {
scale.set_value(value);
Continue(true)
});
} }
scale scale

View File

@@ -1,13 +1,15 @@
use crate::clients::wayland::{self, ToplevelEvent}; use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::add_class;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{glib_recv, lock, send_async, spawn, try_send}; use crate::{lock, send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::debug; use tracing::debug;
@@ -30,24 +32,12 @@ pub struct FocusedModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
impl Default for FocusedModule {
fn default() -> Self {
Self {
show_icon: crate::config::default_true(),
show_title: crate::config::default_true(),
icon_size: default_icon_size(),
truncate: None,
common: Some(CommonConfig::default()),
}
}
}
const fn default_icon_size() -> i32 { const fn default_icon_size() -> i32 {
32 32
} }
impl Module<gtk::Box> for FocusedModule { impl Module<gtk::Box> for FocusedModule {
type SendMessage = Option<(String, String)>; type SendMessage = (String, String);
type ReceiveMessage = (); type ReceiveMessage = ();
fn name() -> &'static str { fn name() -> &'static str {
@@ -76,37 +66,22 @@ impl Module<gtk::Box> for FocusedModule {
if let Some(focused) = focused { if let Some(focused) = focused {
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id))) ModuleUpdateEvent::Update((focused.title.clone(), focused.app_id))
); );
}; };
while let Ok(event) = wlrx.recv().await { while let Ok(event) = wlrx.recv().await {
match event { if let ToplevelEvent::Update(handle) = event {
ToplevelEvent::Update(handle) => {
let info = handle.info().unwrap_or_default(); let info = handle.info().unwrap_or_default();
if info.focused { if info.focused {
debug!("Changing focus"); debug!("Changing focus");
send_async!( send_async!(
tx, tx,
ModuleUpdateEvent::Update(Some(( ModuleUpdateEvent::Update((info.title.clone(), info.app_id.clone()))
info.title.clone(),
info.app_id.clone()
)))
); );
} else {
send_async!(tx, ModuleUpdateEvent::Update(None));
} }
} }
ToplevelEvent::Remove(handle) => {
let info = handle.info().unwrap_or_default();
if info.focused {
debug!("Clearing focus");
send_async!(tx, ModuleUpdateEvent::Update(None));
}
}
ToplevelEvent::New(_) => {}
}
} }
}); });
@@ -117,19 +92,19 @@ impl Module<gtk::Box> for FocusedModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5); let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::new(); let icon = gtk::Image::new();
if self.show_icon { if self.show_icon {
icon.add_class("icon"); add_class(&icon, "icon");
container.add(&icon); container.add(&icon);
} }
let label = Label::new(None); let label = Label::new(None);
label.add_class("label"); add_class(&label, "label");
if let Some(truncate) = self.truncate { if let Some(truncate) = self.truncate {
truncate.truncate_label(&label); truncate.truncate_label(&label);
@@ -139,29 +114,25 @@ impl Module<gtk::Box> for FocusedModule {
{ {
let icon_theme = icon_theme.clone(); let icon_theme = icon_theme.clone();
glib_recv!(context.subscribe(), data => { context.widget_rx.attach(None, move |(name, id)| {
if let Some((name, id)) = data {
if self.show_icon { if self.show_icon {
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size) match ImageProvider::parse(&id, &icon_theme, self.icon_size)
.map(|image| image.load_into_image(icon.clone())) .map(|image| image.load_into_image(icon.clone()))
{ {
Some(Ok(())) => icon.show(), Some(Ok(_)) => icon.show(),
_ => icon.hide(), _ => icon.hide(),
} }
} }
if self.show_title { if self.show_title {
label.show();
label.set_label(&name); label.set_label(&name);
} }
} else {
icon.hide(); Continue(true)
label.hide();
}
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,8 +1,9 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{glib_recv, try_send}; use crate::try_send;
use color_eyre::Result; use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
@@ -16,15 +17,6 @@ pub struct LabelModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
impl LabelModule {
pub(crate) fn new(label: String) -> Self {
Self {
label,
common: Some(CommonConfig::default()),
}
}
}
impl Module<Label> for LabelModule { impl Module<Label> for LabelModule {
type SendMessage = String; type SendMessage = String;
type ReceiveMessage = (); type ReceiveMessage = ();
@@ -41,6 +33,7 @@ impl Module<Label> for LabelModule {
) -> Result<()> { ) -> Result<()> {
dynamic_string(&self.label, move |string| { dynamic_string(&self.label, move |string| {
try_send!(tx, ModuleUpdateEvent::Update(string)); try_send!(tx, ModuleUpdateEvent::Update(string));
Continue(true)
}); });
Ok(()) Ok(())
@@ -50,16 +43,18 @@ impl Module<Label> for LabelModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Result<ModuleParts<Label>> { ) -> Result<ModuleWidget<Label>> {
let label = Label::new(None); let label = Label::new(None);
label.set_use_markup(true);
{ {
let label = label.clone(); let label = label.clone();
glib_recv!(context.subscribe(), string => label.set_markup(&string)); context.widget_rx.attach(None, move |string| {
label.set_label(&string);
Continue(true)
});
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: label, widget: label,
popup: None, popup: None,
}) })

View File

@@ -1,15 +1,13 @@
use super::open_state::OpenState; use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle; use crate::clients::wayland::ToplevelHandle;
use crate::config::BarPosition;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent; use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send}; use crate::{read_lock, try_send};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use glib::Propagation;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme}; 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;
@@ -178,7 +176,7 @@ impl ItemButton {
item: &Item, item: &Item,
appearance: AppearanceOptions, appearance: AppearanceOptions,
icon_theme: &IconTheme, icon_theme: &IconTheme,
bar_position: BarPosition, orientation: Orientation,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>, tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>, controller_tx: &Sender<ItemEvent>,
) -> Self { ) -> Self {
@@ -193,7 +191,7 @@ impl ItemButton {
if appearance.show_icons { if appearance.show_icons {
let gtk_image = gtk::Image::new(); let gtk_image = gtk::Image::new();
let image = let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size); ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
if let Some(image) = image { if let Some(image) = image {
button.set_image(Some(&gtk_image)); button.set_image(Some(&gtk_image));
button.set_always_show_image(true); button.set_always_show_image(true);
@@ -251,40 +249,13 @@ impl ItemButton {
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::OpenPopupAt( ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
button.geometry(bar_position.get_orientation())
)
); );
} else { } else {
try_send!(tx, ModuleUpdateEvent::ClosePopup); try_send!(tx, ModuleUpdateEvent::ClosePopup);
} }
Propagation::Proceed Inhibit(false)
});
}
{
let tx = tx.clone();
button.connect_leave_notify_event(move |button, ev| {
const THRESHOLD: f64 = 5.0;
let alloc = button.allocation();
let (x, y) = ev.position();
let close = match bar_position {
BarPosition::Top => y + THRESHOLD < f64::from(alloc.height()),
BarPosition::Bottom => y > THRESHOLD,
BarPosition::Left => x + THRESHOLD < f64::from(alloc.width()),
BarPosition::Right => x > THRESHOLD,
};
if close {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Propagation::Proceed
}); });
} }

View File

@@ -7,18 +7,18 @@ use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file; use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions; use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext, use crate::{lock, send_async, try_send, write_lock};
};
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::Deserialize; use serde::Deserialize;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::Arc; use std::sync::{Arc, Mutex};
use tokio::sync::{broadcast, mpsc}; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@@ -90,8 +90,8 @@ impl Module<gtk::Box> for LauncherModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let items = self let items = self
.favorites .favorites
@@ -108,7 +108,7 @@ impl Module<gtk::Box> for LauncherModule {
.collect::<IndexMap<_, _>>() .collect::<IndexMap<_, _>>()
}); });
let items = arc_mut!(items); let items = Arc::new(Mutex::new(items));
let items2 = Arc::clone(&items); let items2 = Arc::clone(&items);
let tx2 = tx.clone(); let tx2 = tx.clone();
@@ -163,7 +163,6 @@ impl Module<gtk::Box> for LauncherModule {
match item { match item {
None => { None => {
let item: Item = handle.try_into()?; let item: Item = handle.try_into()?;
items.insert(info.app_id.clone(), item.clone()); items.insert(info.app_id.clone(), item.clone());
ItemOrWindow::Item(item) ItemOrWindow::Item(item)
@@ -314,7 +313,7 @@ impl Module<gtk::Box> for LauncherModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> crate::Result<ModuleParts<gtk::Box>> { ) -> crate::Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0); let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
@@ -332,13 +331,11 @@ impl Module<gtk::Box> for LauncherModule {
}; };
let show_names = self.show_names; let show_names = self.show_names;
let bar_position = info.bar_position; let orientation = info.bar_position.get_orientation();
let mut buttons = IndexMap::<String, ItemButton>::new(); let mut buttons = IndexMap::<String, ItemButton>::new();
let tx = context.tx.clone(); context.widget_rx.attach(None, move |event| {
let rx = context.subscribe();
glib_recv!(rx, event => {
match event { match event {
LauncherUpdate::AddItem(item) => { LauncherUpdate::AddItem(item) => {
debug!("Adding item with id {}", item.app_id); debug!("Adding item with id {}", item.app_id);
@@ -350,8 +347,8 @@ impl Module<gtk::Box> for LauncherModule {
&item, &item,
appearance_options, appearance_options,
&icon_theme, &icon_theme,
bar_position, orientation,
&tx, &context.tx,
&controller_tx, &controller_tx,
); );
@@ -359,10 +356,9 @@ impl Module<gtk::Box> for LauncherModule {
buttons.insert(item.app_id, button); buttons.insert(item.app_id, button);
} }
} }
LauncherUpdate::AddWindow(app_id, win) => { LauncherUpdate::AddWindow(app_id, _) => {
if let Some(button) = buttons.get(&app_id) { if let Some(button) = buttons.get(&app_id) {
button.set_open(true); button.set_open(true);
button.set_focused(win.open_state.is_focused());
let mut menu_state = write_lock!(button.menu_state); let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows += 1; menu_state.num_windows += 1;
@@ -383,12 +379,8 @@ impl Module<gtk::Box> for LauncherModule {
} }
} }
} }
LauncherUpdate::RemoveWindow(app_id, win_id) => { LauncherUpdate::RemoveWindow(app_id, _) => {
debug!("Removing window {win_id} with id {app_id}");
if let Some(button) = buttons.get(&app_id) { if let Some(button) = buttons.get(&app_id) {
button.set_focused(false);
let mut menu_state = write_lock!(button.menu_state); let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows -= 1; menu_state.num_windows -= 1;
} }
@@ -411,15 +403,13 @@ impl Module<gtk::Box> for LauncherModule {
} }
LauncherUpdate::Hover(_) => {} LauncherUpdate::Hover(_) => {}
}; };
Continue(true)
}); });
} }
let rx = context.subscribe(); let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self Ok(ModuleWidget {
.into_popup(context.controller_tx, rx, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
widget: container, widget: container,
popup, popup,
}) })
@@ -427,8 +417,8 @@ impl Module<gtk::Box> for LauncherModule {
fn into_popup( fn into_popup(
self, self,
controller_tx: mpsc::Sender<Self::ReceiveMessage>, controller_tx: Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250; const MAX_WIDTH: i32 = 250;
@@ -444,7 +434,7 @@ impl Module<gtk::Box> for LauncherModule {
{ {
let container = container.clone(); let container = container.clone();
glib_recv!(rx, event => { rx.attach(None, move |event| {
match event { match event {
LauncherUpdate::AddItem(item) => { LauncherUpdate::AddItem(item) => {
let app_id = item.app_id.clone(); let app_id = item.app_id.clone();
@@ -531,6 +521,8 @@ impl Module<gtk::Box> for LauncherModule {
} }
_ => {} _ => {}
} }
Continue(true)
}); });
} }

View File

@@ -1,20 +1,3 @@
use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send};
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
/// Displays the current date and time. /// Displays the current date and time.
@@ -41,6 +24,19 @@ pub mod upower;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub mod workspaces; pub mod workspaces;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::popup::{Popup, WidgetGeometry};
use crate::{read_lock, send, write_lock};
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Clone)] #[derive(Clone)]
pub enum ModuleLocation { pub enum ModuleLocation {
Left, Left,
@@ -56,98 +52,30 @@ pub struct ModuleInfo<'a> {
pub icon_theme: &'a IconTheme, pub icon_theme: &'a IconTheme,
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub enum ModuleUpdateEvent<T: Clone> { 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.
/// Takes the button ID. TogglePopup(WidgetGeometry),
TogglePopup(usize),
/// Force sets the popup open. /// Force sets the popup open.
/// Takes the button ID. /// Takes the button X position and width.
OpenPopup(usize), OpenPopup(WidgetGeometry),
OpenPopupAt(WidgetGeometry),
/// Force sets the popup closed. /// Force sets the popup closed.
ClosePopup, ClosePopup,
} }
pub struct WidgetContext<TSend, TReceive> pub struct WidgetContext<TSend, TReceive> {
where
TSend: Clone,
{
pub id: usize, pub id: usize,
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>, pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
pub update_tx: broadcast::Sender<TSend>,
pub controller_tx: mpsc::Sender<TReceive>, pub controller_tx: mpsc::Sender<TReceive>,
pub widget_rx: glib::Receiver<TSend>,
_update_rx: broadcast::Receiver<TSend>, pub popup_rx: glib::Receiver<TSend>,
} }
impl<TSend, TReceive> WidgetContext<TSend, TReceive> pub struct ModuleWidget<W: IsA<Widget>> {
where
TSend: Clone,
{
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
self.update_tx.subscribe()
}
}
pub struct ModuleParts<W: IsA<Widget>> {
pub widget: W, pub widget: W,
pub popup: Option<ModulePopupParts>, pub popup: Option<gtk::Box>,
}
impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
}
#[derive(Debug, Clone)]
pub struct ModulePopupParts {
/// The popup container, with all its contents
pub container: gtk::Box,
/// An array of buttons which can be used for opening the popup.
/// For most modules, this will only be a single button.
/// For some advanced modules, such as `Launcher`, this is all item buttons.
pub buttons: Vec<Button>,
}
pub trait ModulePopup {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
}
impl ModulePopup for Option<gtk::Box> {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
}
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
self.map(|container| ModulePopupParts { container, buttons })
}
}
pub trait PopupButton {
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
self.get_tag("popup-id").copied()
}
/// Gets the popup ID associated with this button.
/// This should only be called on buttons which are known to be associated with popups.
///
/// # Panics
/// Will panic if an ID has not been set.
fn popup_id(&self) -> usize {
self.try_popup_id().expect("id to exist")
}
} }
pub trait Module<W> pub trait Module<W>
@@ -164,22 +92,18 @@ where
info: &ModuleInfo, info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
rx: mpsc::Receiver<Self::ReceiveMessage>, rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> ) -> Result<()>;
where
<Self as Module<W>>::SendMessage: Clone;
fn into_widget( fn into_widget(
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<W>> ) -> Result<ModuleWidget<W>>;
where
<Self as Module<W>>::SendMessage: Clone;
fn into_popup( fn into_popup(
self, self,
_tx: mpsc::Sender<Self::ReceiveMessage>, _tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: broadcast::Receiver<Self::SendMessage>, _rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where
@@ -194,59 +118,53 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>( pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule, module: TModule,
id: usize, id: usize,
name: Option<String>,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>, popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleParts<TWidget>> ) -> Result<ModuleWidget<TWidget>>
where where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>, TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>, TWidget: IsA<Widget>,
TSend: Debug + Clone + Send + 'static, TSend: Clone + Send + 'static,
{ {
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64); let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64); let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
let (tx, rx) = broadcast::channel(64); let channel = BridgeChannel::<ModuleUpdateEvent<TSend>>::new();
let (ui_tx, ui_rx) = mpsc::channel::<TRec>(16);
module.spawn_controller(info, ui_tx.clone(), controller_rx)?; module.spawn_controller(info, channel.create_sender(), ui_rx)?;
let context = WidgetContext { let context = WidgetContext {
id, id,
tx: ui_tx, widget_rx: w_rx,
update_tx: tx.clone(), popup_rx: p_rx,
controller_tx, tx: channel.create_sender(),
_update_rx: rx, controller_tx: ui_tx,
}; };
let module_name = TModule::name(); let name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
let module_parts = module.into_widget(context, info)?; let module_parts = module.into_widget(context, info)?;
module_parts.widget.add_class("widget"); module_parts.widget.style_context().add_class(name);
module_parts.widget.add_class(module_name);
let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() { if let Some(popup_content) = module_parts.popup.clone() {
popup_content popup_content
.container
.style_context() .style_context()
.add_class(&format!("popup-{module_name}")); .add_class(&format!("popup-{name}"));
register_popup_content(popup, id, instance_name, popup_content); register_popup_content(popup, id, popup_content);
has_popup = true;
} }
setup_receiver(tx, ui_rx, popup.clone(), module_name, id); setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
Ok(module_parts) Ok(module_parts)
} }
/// Registers the popup content with the popup. /// Registers the popup content with the popup.
fn register_popup_content( fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
popup: &Rc<RefCell<Popup>>, write_lock!(popup).register_content(id, popup_content);
id: usize,
name: String,
popup_content: ModulePopupParts,
) {
popup.borrow_mut().register_content(id, name, popup_content);
} }
/// Sets up the bridge channel receiver /// Sets up the bridge channel receiver
@@ -255,83 +173,80 @@ fn register_popup_content(
/// Handles opening/closing popups /// Handles opening/closing popups
/// and communicating update messages between controllers and widgets/popups. /// and communicating update messages between controllers and widgets/popups.
fn setup_receiver<TSend>( fn setup_receiver<TSend>(
tx: broadcast::Sender<TSend>, channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>, w_tx: glib::Sender<TSend>,
popup: Rc<RefCell<Popup>>, p_tx: glib::Sender<TSend>,
popup: Arc<RwLock<Popup>>,
name: &'static str, name: &'static str,
id: usize, id: usize,
has_popup: bool,
) where ) where
TSend: Debug + Clone + Send + 'static, TSend: Clone + Send + 'static,
{ {
// some rare cases can cause the popup to incorrectly calculate its size on first open. // 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. // we can fix that by just force re-rendering it on its first open.
let mut has_popup_opened = false; let mut has_popup_opened = false;
glib_recv_mpsc!(rx, ev => { channel.recv(move |ev| {
match ev { match ev {
ModuleUpdateEvent::Update(update) => { ModuleUpdateEvent::Update(update) => {
send!(tx, update); if has_popup {
send!(p_tx, update.clone());
} }
ModuleUpdateEvent::TogglePopup(button_id) => {
send!(w_tx, update);
}
ModuleUpdateEvent::TogglePopup(geometry) => {
debug!("Toggling popup for {} [#{}]", name, id); debug!("Toggling popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut(); let popup = read_lock!(popup);
if popup.is_visible() { if popup.is_visible() {
popup.hide(); popup.hide();
} else { } else {
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened { if !has_popup_opened {
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
has_popup_opened = true; has_popup_opened = true;
} }
} }
} }
ModuleUpdateEvent::OpenPopup(button_id) => { ModuleUpdateEvent::OpenPopup(geometry) => {
debug!("Opening popup for {} [#{}]", name, id); debug!("Opening popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut(); let popup = read_lock!(popup);
popup.hide(); popup.hide();
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened { if !has_popup_opened {
popup.show(id, button_id); popup.show_content(id);
has_popup_opened = true; popup.show(geometry);
}
}
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_at(id, geometry);
has_popup_opened = true; has_popup_opened = true;
} }
} }
ModuleUpdateEvent::ClosePopup => { ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id); debug!("Closing popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut(); let popup = read_lock!(popup);
popup.hide(); popup.hide();
} }
} }
Continue(true)
}); });
} }
pub fn set_widget_identifiers<TWidget: IsA<Widget>>( pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleParts<TWidget>, widget_parts: &ModuleWidget<TWidget>,
common: &CommonConfig, common: &CommonConfig,
) { ) {
if let Some(ref name) = common.name { if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name); widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup { if let Some(ref popup) = widget_parts.popup {
popup.container.set_widget_name(&format!("popup-{name}")); popup.set_widget_name(&format!("popup-{name}"));
} }
} }
@@ -343,10 +258,7 @@ pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
if let Some(ref popup) = widget_parts.popup { if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') { for part in class.split(' ') {
popup popup.style_context().add_class(&format!("popup-{part}"));
.container
.style_context()
.add_class(&format!("popup-{part}"));
} }
} }
} }
@@ -374,8 +286,6 @@ pub fn wrap_widget<W: IsA<Widget>>(
revealer.set_reveal_child(true); revealer.set_reveal_child(true);
let container = EventBox::new(); let container = EventBox::new();
container.add_class("widget-container");
container.add_events(EventMask::SCROLL_MASK); container.add_events(EventMask::SCROLL_MASK);
container.add(&revealer); container.add(&revealer);

View File

@@ -1,32 +1,26 @@
use std::path::PathBuf; mod config;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::gtk_helpers::add_class;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use glib::{Propagation, PropertySet}; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Label, Orientation, Scale}; use gtk::{Button, IconTheme, Label, Orientation, Scale};
use regex::Regex; use regex::Regex;
use tokio::sync::{broadcast, mpsc}; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error; use tracing::error;
use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{glib_recv, send_async, spawn, try_send};
pub use self::config::MusicModule; pub use self::config::MusicModule;
use self::config::PlayerType; use self::config::PlayerType;
mod config;
#[derive(Debug)] #[derive(Debug)]
pub enum PlayerCommand { pub enum PlayerCommand {
Previous, Previous,
@@ -34,7 +28,6 @@ pub enum PlayerCommand {
Pause, Pause,
Next, Next,
Volume(u8), Volume(u8),
Seek(Duration),
} }
/// Formats a duration given in seconds /// Formats a duration given in seconds
@@ -54,12 +47,6 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
#[derive(Clone, Debug)]
pub enum ControllerEvent {
Update(Option<SongUpdate>),
UpdateProgress(ProgressTick),
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SongUpdate { pub struct SongUpdate {
song: Track, song: Track,
@@ -80,7 +67,7 @@ async fn get_client(
} }
impl Module<Button> for MusicModule { impl Module<Button> for MusicModule {
type SendMessage = ControllerEvent; type SendMessage = Option<SongUpdate>;
type ReceiveMessage = PlayerCommand; type ReceiveMessage = PlayerCommand;
fn name() -> &'static str { fn name() -> &'static str {
@@ -90,8 +77,8 @@ impl Module<Button> for MusicModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>, mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let format = self.format.clone(); let format = self.format.clone();
@@ -116,7 +103,7 @@ impl Module<Button> for MusicModule {
PlayerUpdate::Update(track, status) => match *track { PlayerUpdate::Update(track, status) => match *track {
Some(track) => { Some(track) => {
let display_string = let display_string =
replace_tokens(format.as_str(), &tokens, &track); replace_tokens(format.as_str(), &tokens, &track, &status);
let update = SongUpdate { let update = SongUpdate {
song: track, song: track,
@@ -124,24 +111,10 @@ impl Module<Button> for MusicModule {
display_string, display_string,
}; };
send_async!( send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
tx,
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
update
)))
);
} }
None => send_async!( None => send_async!(tx, ModuleUpdateEvent::Update(None)),
tx,
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
),
}, },
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
tx,
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
progress_tick
))
),
PlayerUpdate::Disconnect => break, PlayerUpdate::Disconnect => break,
} }
} }
@@ -164,7 +137,6 @@ impl Module<Button> for MusicModule {
PlayerCommand::Pause => client.pause(), PlayerCommand::Pause => client.pause(),
PlayerCommand::Next => client.next(), PlayerCommand::Next => client.next(),
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
PlayerCommand::Seek(duration) => client.seek(duration),
}; };
if let Err(err) = res { if let Err(err) = res {
@@ -181,10 +153,10 @@ impl Module<Button> for MusicModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<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_contents.add_class("contents"); add_class(&button_contents, "contents");
button.add(&button_contents); button.add(&button_contents);
@@ -202,25 +174,24 @@ impl Module<Button> for MusicModule {
button_contents.add(&icon_play); button_contents.add(&icon_play);
button_contents.add(&label); button_contents.add(&label);
let orientation = info.bar_position.get_orientation();
{ {
let tx = context.tx.clone(); let tx = context.tx.clone();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id())); try_send!(
tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
);
}); });
} }
{ {
let button = button.clone(); let button = button.clone();
let tx = context.tx.clone(); let tx = context.tx.clone();
let rx = context.subscribe();
glib_recv!(rx, event => {
let ControllerEvent::Update(mut event) = event else {
continue;
};
context.widget_rx.attach(None, move |mut event| {
if let Some(event) = event.take() { if let Some(event) = event.take() {
label.set_label(&event.display_string); label.set_label(&event.display_string);
@@ -249,33 +220,34 @@ impl Module<Button> for MusicModule {
button.hide(); button.hide();
try_send!(tx, ModuleUpdateEvent::ClosePopup); try_send!(tx, ModuleUpdateEvent::ClosePopup);
} }
Continue(true)
}); });
}; };
let rx = context.subscribe(); let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleWidget {
widget: button,
popup,
})
} }
fn into_popup( fn into_popup(
self, self,
tx: mpsc::Sender<Self::ReceiveMessage>, tx: Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(Orientation::Vertical, 10); let container = gtk::Box::new(Orientation::Horizontal, 10);
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
let album_image = gtk::Image::builder() let album_image = gtk::Image::builder()
.width_request(128) .width_request(128)
.height_request(128) .height_request(128)
.build(); .build();
album_image.add_class("album-art"); add_class(&album_image, "album-art");
let icons = self.icons; let icons = self.icons;
@@ -284,28 +256,28 @@ impl Module<Button> for MusicModule {
let album_label = IconLabel::new(&icons.album, None, icon_theme); let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme); let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
title_label.container.add_class("title"); add_class(&title_label.container, "title");
album_label.container.add_class("album"); add_class(&album_label.container, "album");
artist_label.container.add_class("artist"); add_class(&artist_label.container, "artist");
info_box.add(&title_label.container); info_box.add(&title_label.container);
info_box.add(&album_label.container); info_box.add(&album_label.container);
info_box.add(&artist_label.container); info_box.add(&artist_label.container);
let controls_box = gtk::Box::new(Orientation::Horizontal, 0); let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
controls_box.add_class("controls"); add_class(&controls_box, "controls");
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size); let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
btn_prev.add_class("btn-prev"); add_class(&btn_prev, "btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size); let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
btn_play.add_class("btn-play"); add_class(&btn_play, "btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size); let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
btn_pause.add_class("btn-pause"); add_class(&btn_pause, "btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size); let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
btn_next.add_class("btn-next"); add_class(&btn_next, "btn-next");
controls_box.add(&btn_prev); controls_box.add(&btn_prev);
controls_box.add(&btn_play); controls_box.add(&btn_play);
@@ -315,22 +287,21 @@ impl Module<Button> for MusicModule {
info_box.add(&controls_box); info_box.add(&controls_box);
let volume_box = gtk::Box::new(Orientation::Vertical, 5); let volume_box = gtk::Box::new(Orientation::Vertical, 5);
volume_box.add_class("volume"); add_class(&volume_box, "volume");
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0); let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true); volume_slider.set_inverted(true);
volume_slider.add_class("slider"); add_class(&volume_slider, "slider");
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size); let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
volume_icon.add_class("icon"); add_class(&volume_icon, "icon");
volume_box.pack_start(&volume_slider, true, true, 0); volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0); volume_box.pack_end(&volume_icon, false, false, 0);
main_container.add(&album_image); container.add(&album_image);
main_container.add(&info_box); container.add(&info_box);
main_container.add(&volume_box); container.add(&volume_box);
container.add(&main_container);
let tx_prev = tx.clone(); let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| { btn_prev.connect_clicked(move |_| {
@@ -352,49 +323,12 @@ impl Module<Button> for MusicModule {
try_send!(tx_next, PlayerCommand::Next); try_send!(tx_next, PlayerCommand::Next);
}); });
let tx_vol = tx.clone(); let tx_vol = tx;
volume_slider.connect_change_value(move |_, _, val| { volume_slider.connect_change_value(move |_, _, val| {
try_send!(tx_vol, PlayerCommand::Volume(val as u8)); try_send!(tx_vol, PlayerCommand::Volume(val as u8));
Propagation::Proceed Inhibit(false)
}); });
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
progress_box.add_class("progress");
let progress_label = Label::new(None);
progress_label.add_class("label");
let progress = Scale::builder()
.orientation(Orientation::Horizontal)
.draw_value(false)
.hexpand(true)
.build();
progress.add_class("slider");
progress_box.add(&progress);
progress_box.add(&progress_label);
container.add(&progress_box);
let drag_lock = Arc::new(AtomicBool::new(false));
{
let drag_lock = drag_lock.clone();
progress.connect_button_press_event(move |_, _| {
drag_lock.set(true);
Propagation::Proceed
});
}
{
let drag_lock = drag_lock.clone();
progress.connect_button_release_event(move |scale, _| {
let value = scale.value();
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
drag_lock.set(false);
Propagation::Proceed
});
}
container.show_all(); container.show_all();
{ {
@@ -402,21 +336,18 @@ impl Module<Button> for MusicModule {
let image_size = self.cover_image_size; let image_size = self.cover_image_size;
let mut prev_cover = None; let mut prev_cover = None;
glib_recv!(rx, event => { rx.attach(None, move |update| {
match event { if let Some(update) = update {
ControllerEvent::Update(Some(update)) => {
// only update art when album changes // only update art when album changes
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 = if let Some(image) = new_cover.and_then(|cover_path| { let res = if let Some(image) = new_cover.and_then(|cover_path| {
ImageProvider::parse(&cover_path, &icon_theme, false, image_size) ImageProvider::parse(&cover_path, &icon_theme, image_size)
}) { }) {
album_image.show();
image.load_into_image(album_image.clone()) image.load_into_image(album_image.clone())
} else { } else {
album_image.set_from_pixbuf(None); album_image.set_from_pixbuf(None);
album_image.hide();
Ok(()) Ok(())
}; };
@@ -425,9 +356,15 @@ impl Module<Button> for MusicModule {
} }
} }
update_popup_metadata_label(update.song.title, &title_label); title_label
update_popup_metadata_label(update.song.album, &album_label); .label
update_popup_metadata_label(update.song.artist, &artist_label); .set_text(&update.song.title.unwrap_or_default());
album_label
.label
.set_text(&update.song.album.unwrap_or_default());
artist_label
.label
.set_text(&update.song.artist.unwrap_or_default());
match update.status.state { match update.status.state {
PlayerState::Stopped => { PlayerState::Stopped => {
@@ -459,34 +396,10 @@ impl Module<Button> for MusicModule {
btn_prev.set_sensitive(enable_prev); btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next); btn_next.set_sensitive(enable_next);
if let Some(volume) = update.status.volume_percent { volume_slider.set_value(update.status.volume_percent as f64);
volume_slider.set_value(f64::from(volume));
volume_box.show();
} else {
volume_box.hide();
} }
}
ControllerEvent::UpdateProgress(progress_tick)
if !drag_lock.load(Ordering::Relaxed) =>
{
if let (Some(elapsed), Some(duration)) =
(progress_tick.elapsed, progress_tick.duration)
{
progress_label.set_label(&format!(
"{}/{}",
format_time(elapsed),
format_time(duration)
));
progress.set_value(elapsed.as_secs_f64()); Continue(true)
progress.set_range(0.0, duration.as_secs_f64());
progress_box.show_all();
} else {
progress_box.hide();
}
}
_ => {}
};
}); });
} }
@@ -494,24 +407,17 @@ impl Module<Button> for MusicModule {
} }
} }
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
match text {
Some(value) => {
label.label.set_text(&value);
label.container.show_all();
}
None => {
label.container.hide();
}
}
}
/// Replaces each of the formatting tokens in the formatting string /// Replaces each of the formatting tokens in the formatting string
/// with actual data pulled from the music player /// with actual data pulled from the music player
fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> String { fn replace_tokens(
format_string: &str,
tokens: &Vec<String>,
song: &Track,
status: &Status,
) -> String {
let mut compiled_string = format_string.to_string(); let mut compiled_string = format_string.to_string();
for token in tokens { for token in tokens {
let value = get_token_value(song, token); let value = get_token_value(song, status, token);
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str()); compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
} }
compiled_string compiled_string
@@ -519,7 +425,7 @@ fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> St
/// Converts a string format token value /// Converts a string format token value
/// into its respective value. /// into its respective value.
fn get_token_value(song: &Track, token: &str) -> String { fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
match token { match token {
"title" => song.title.clone(), "title" => song.title.clone(),
"album" => song.album.clone(), "album" => song.album.clone(),
@@ -528,6 +434,8 @@ fn get_token_value(song: &Track, token: &str) -> String {
"disc" => song.disc.map(|x| x.to_string()), "disc" => song.disc.map(|x| x.to_string()),
"genre" => song.genre.clone(), "genre" => song.genre.clone(),
"track" => song.track.map(|x| x.to_string()), "track" => song.track.map(|x| x.to_string()),
"duration" => status.duration.map(format_time),
"elapsed" => status.elapsed.map(format_time),
_ => Some(token.to_string()), _ => Some(token.to_string()),
} }
.unwrap_or_default() .unwrap_or_default()
@@ -546,8 +454,8 @@ impl IconLabel {
let icon = new_icon_label(icon_input, icon_theme, 24); let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label); let label = Label::new(label);
icon.add_class("icon-box"); add_class(&icon, "icon-box");
label.add_class("label"); add_class(&label, "label");
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);

View File

@@ -1,11 +1,12 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode}; use crate::script::{OutputStream, Script, ScriptMode};
use crate::{glib_recv, spawn, try_send}; use crate::try_send;
use color_eyre::{Help, Report, Result}; use color_eyre::{Help, Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error; use tracing::error;
@@ -82,16 +83,19 @@ impl Module<Label> for ScriptModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<Label>> { ) -> Result<ModuleWidget<Label>> {
let label = Label::builder().use_markup(true).build(); let label = Label::builder().use_markup(true).build();
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
{ {
let label = label.clone(); let label = label.clone();
glib_recv!(context.subscribe(), s => label.set_markup(s.as_str())); context.widget_rx.attach(None, move |s| {
label.set_markup(s.as_str());
Continue(true)
});
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: label, widget: label,
popup: None, popup: None,
}) })

View File

@@ -1,7 +1,7 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{glib_recv, send_async, spawn}; use crate::send_async;
use color_eyre::Result; use color_eyre::Result;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
@@ -10,6 +10,7 @@ use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt}; use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::sleep; use tokio::time::sleep;
@@ -185,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?; let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10); let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
@@ -195,7 +196,7 @@ impl Module<gtk::Box> for SysInfoModule {
for format in &self.format { for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build(); let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item"); add_class(&label, "item");
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
container.add(&label); container.add(&label);
@@ -204,7 +205,7 @@ impl Module<gtk::Box> for SysInfoModule {
{ {
let formats = self.format; let formats = self.format;
glib_recv!(context.subscribe(), info => { context.widget_rx.attach(None, move |info| {
for (format, label) in formats.iter().zip(labels.clone()) { for (format, label) in formats.iter().zip(labels.clone()) {
let format_compiled = re.replace_all(format, |caps: &Captures| { let format_compiled = re.replace_all(format, |caps: &Captures| {
info.get(&caps[1]) info.get(&caps[1])
@@ -214,10 +215,12 @@ impl Module<gtk::Box> for SysInfoModule {
label.set_markup(format_compiled.as_ref()); label.set_markup(format_compiled.as_ref());
} }
Continue(true)
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,11 +1,8 @@
use crate::clients::system_tray::get_tray_event_client; use crate::clients::system_tray::get_tray_event_client;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, glib_recv, spawn, try_send}; use crate::{await_sync, try_send};
use color_eyre::Result; use color_eyre::Result;
use glib::ffi::g_strfreev;
use glib::translate::ToGlibPtr;
use gtk::ffi::gtk_icon_theme_get_search_path;
use gtk::gdk_pixbuf::{Colorspace, InterpType}; use gtk::gdk_pixbuf::{Colorspace, InterpType};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ use gtk::{
@@ -13,13 +10,11 @@ use gtk::{
SeparatorMenuItem, SeparatorMenuItem,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use std::ffi::CStr; use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
use std::os::raw::{c_char, c_int}; use stray::message::tray::StatusNotifierItem;
use std::ptr; use stray::message::{NotifierItemCommand, NotifierItemMessage};
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType}; use tokio::spawn;
use system_tray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
@@ -29,43 +24,21 @@ pub struct TrayModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
/// Gets the GTK icon theme search paths by calling the FFI function.
/// Conveniently returns the result as a `HashSet`.
fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
let mut gtk_paths: *mut *mut c_char = ptr::null_mut();
let mut n_elements: c_int = 0;
let mut paths = HashSet::new();
unsafe {
gtk_icon_theme_get_search_path(
icon_theme.to_glib_none().0,
&mut gtk_paths,
&mut n_elements,
);
// n_elements is never negative (that would be weird)
for i in 0..n_elements as usize {
let c_str = CStr::from_ptr(*gtk_paths.add(i));
if let Ok(str) = c_str.to_str() {
paths.insert(str.to_owned());
}
}
g_strfreev(gtk_paths);
}
paths
}
/// Attempts to get 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_image_from_icon_name(item: &StatusNotifierItem, icon_theme: &IconTheme) -> Option<Image> { fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> {
if let Some(path) = item.icon_theme_path.as_ref() { let theme = item
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) { .icon_theme_path
icon_theme.append_search_path(path); .as_ref()
} .map(|path| {
} let theme = IconTheme::new();
theme.append_search_path(path);
theme
})
.unwrap_or_default();
item.icon_name.as_ref().and_then(|icon_name| { item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
}) })
} }
@@ -198,17 +171,16 @@ impl Module<MenuBar> for TrayModule {
fn into_widget( fn into_widget(
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, _info: &ModuleInfo,
) -> Result<ModuleParts<MenuBar>> { ) -> Result<ModuleWidget<MenuBar>> {
let container = MenuBar::new(); let container = MenuBar::new();
{ {
let container = container.clone(); let container = container.clone();
let mut widgets = HashMap::new(); let mut widgets = HashMap::new();
let icon_theme = info.icon_theme.clone();
// listen for UI updates // listen for UI updates
glib_recv!(context.subscribe(), update => { context.widget_rx.attach(None, move |update| {
match update { match update {
NotifierItemMessage::Update { NotifierItemMessage::Update {
item, item,
@@ -220,7 +192,7 @@ impl Module<MenuBar> for TrayModule {
let menu_item = MenuItem::new(); let menu_item = MenuItem::new();
menu_item.style_context().add_class("item"); menu_item.style_context().add_class("item");
get_image_from_icon_name(&item, &icon_theme) get_image_from_icon_name(&item)
.or_else(|| get_image_from_pixmap(&item)) .or_else(|| get_image_from_pixmap(&item))
.map_or_else( .map_or_else(
|| { || {
@@ -261,10 +233,12 @@ impl Module<MenuBar> for TrayModule {
} }
} }
}; };
Continue(true)
}); });
}; };
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,22 +1,20 @@
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{await_sync, error, send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use futures_lite::stream::StreamExt; use futures_lite::stream::StreamExt;
use gtk::{prelude::*, Button}; use gtk::{prelude::*, Button};
use gtk::{Label, Orientation}; use gtk::{Label, Orientation};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::{broadcast, mpsc}; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use upower_dbus::BatteryState; use upower_dbus::BatteryState;
use zbus; use zbus;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{await_sync, error, glib_recv, send_async, spawn, try_send};
const DAY: i64 = 24 * 60 * 60; const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60; const HOUR: i64 = 60 * 60;
const MINUTE: i64 = 60; const MINUTE: i64 = 60;
@@ -61,8 +59,8 @@ impl Module<gtk::Button> for UpowerModule {
fn spawn_controller( fn spawn_controller(
&self, &self,
_info: &ModuleInfo, _info: &ModuleInfo,
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: mpsc::Receiver<Self::ReceiveMessage>, _rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
spawn(async move { spawn(async move {
// await_sync due to strange "higher-ranked lifetime error" // await_sync due to strange "higher-ranked lifetime error"
@@ -152,58 +150,61 @@ impl Module<gtk::Button> for UpowerModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<Button>> { ) -> Result<ModuleWidget<Button>> {
let icon_theme = info.icon_theme.clone(); let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new(); let icon = gtk::Image::new();
icon.add_class("icon"); add_class(&icon, "icon");
let label = Label::builder() let label = Label::builder()
.label(&self.format) .label(&self.format)
.use_markup(true) .use_markup(true)
.build(); .build();
label.add_class("label"); add_class(&label, "label");
let container = gtk::Box::new(Orientation::Horizontal, 5); let container = gtk::Box::new(Orientation::Horizontal, 5);
container.add_class("contents"); add_class(&container, "contents");
let button = Button::new(); let button = Button::new();
button.add_class("button"); add_class(&button, "button");
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);
button.add(&container); button.add(&container);
let tx = context.tx.clone(); let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id())); try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
);
}); });
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
let format = self.format.clone(); let format = self.format.clone();
let rx = context.subscribe(); context
glib_recv!(rx, properties => { .widget_rx
.attach(None, move |properties: UpowerProperties| {
let format = format.replace("{percentage}", &properties.percentage.to_string()); let format = format.replace("{percentage}", &properties.percentage.to_string());
let icon_name = String::from("icon:") + &properties.icon_name; let icon_name = String::from("icon:") + &properties.icon_name;
ImageProvider::parse(&icon_name, &icon_theme, self.icon_size)
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone())); .map(|provider| provider.load_into_image(icon.clone()));
label.set_markup(format.as_ref()); label.set_markup(format.as_ref());
Continue(true)
}); });
let rx = context.subscribe(); let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleWidget {
widget: button,
popup,
})
} }
fn into_popup( fn into_popup(
self, self,
_tx: mpsc::Sender<Self::ReceiveMessage>, _tx: Sender<Self::ReceiveMessage>,
rx: broadcast::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> ) -> Option<gtk::Box>
where where
@@ -214,10 +215,10 @@ impl Module<gtk::Button> for UpowerModule {
.build(); .build();
let label = Label::new(None); let label = Label::new(None);
label.add_class("upower-details"); add_class(&label, "upower-details");
container.add(&label); container.add(&label);
glib_recv!(rx, properties => { rx.attach(None, move |properties| {
let state = u32_to_battery_state(properties.state); let state = u32_to_battery_state(properties.state);
let format = match state { let format = match state {
Ok(BatteryState::Charging | BatteryState::PendingCharge) => { Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
@@ -244,6 +245,7 @@ impl Module<gtk::Button> for UpowerModule {
}; };
label.set_markup(&format); label.set_markup(&format);
Continue(true)
}); });
container.show_all(); container.show_all();

View File

@@ -1,16 +1,17 @@
use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate}; use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send}; use crate::{send_async, try_send};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme}; use gtk::{Button, IconTheme};
use serde::Deserialize; use serde::Deserialize;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, trace, warn}; use tracing::trace;
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@@ -28,32 +29,11 @@ impl Default for SortOrder {
} }
} }
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum Favorites {
ByMonitor(HashMap<String, Vec<String>>),
Global(Vec<String>),
}
impl Default for Favorites {
fn default() -> Self {
Self::Global(vec![])
}
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule { pub struct WorkspacesModule {
/// Map of actual workspace names to custom names. /// Map of actual workspace names to custom names.
name_map: Option<HashMap<String, String>>, name_map: Option<HashMap<String, String>>,
/// Array of always shown workspaces, and what monitor to show on
#[serde(default)]
favorites: Favorites,
/// List of workspace names to never show
#[serde(default)]
hidden: Vec<String>,
/// Whether to display buttons for all monitors. /// Whether to display buttons for all monitors.
#[serde(default = "crate::config::default_false")] #[serde(default = "crate::config::default_false")]
all_monitors: bool, all_monitors: bool,
@@ -75,7 +55,7 @@ const fn default_icon_size() -> i32 {
/// Creates a button from a workspace /// Creates a button from a workspace
fn create_button( fn create_button(
name: &str, name: &str,
visibility: Visibility, focused: bool,
name_map: &HashMap<String, String>, name_map: &HashMap<String, String>,
icon_theme: &IconTheme, icon_theme: &IconTheme,
icon_size: i32, icon_size: i32,
@@ -89,18 +69,10 @@ fn create_button(
let style_context = button.style_context(); let style_context = button.style_context();
style_context.add_class("item"); style_context.add_class("item");
if visibility.is_visible() { if focused {
style_context.add_class("visible");
}
if visibility.is_focused() {
style_context.add_class("focused"); style_context.add_class("focused");
} }
if !visibility.is_visible() {
style_context.add_class("inactive")
}
{ {
let tx = tx.clone(); let tx = tx.clone();
let name = name.to_string(); let name = name.to_string();
@@ -133,13 +105,6 @@ fn reorder_workspaces(container: &gtk::Box) {
} }
} }
impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
&& (self.all_monitors || output == &work.monitor)
}
}
impl Module<gtk::Box> for WorkspacesModule { impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate; type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String; type ReceiveMessage = String;
@@ -162,10 +127,9 @@ impl Module<gtk::Box> for WorkspacesModule {
client.subscribe_workspace_change() client.subscribe_workspace_change()
}; };
trace!("Set up workspace subscription"); trace!("Set up Sway workspace subscription");
while let Ok(payload) = srx.recv().await { while let Ok(payload) = srx.recv().await {
debug!("Received update: {payload:?}");
send_async!(tx, ModuleUpdateEvent::Update(payload)); send_async!(tx, ModuleUpdateEvent::Update(payload));
} }
}); });
@@ -190,12 +154,10 @@ impl Module<gtk::Box> for WorkspacesModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.get_orientation(), 0); let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let name_map = self.name_map.clone().unwrap_or_default(); let name_map = self.name_map.unwrap_or_default();
let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![];
let mut button_map: HashMap<String, Button> = HashMap::new(); let mut button_map: HashMap<String, Button> = HashMap::new();
@@ -209,53 +171,24 @@ impl Module<gtk::Box> for WorkspacesModule {
// since it fires for every workspace subscriber // since it fires for every workspace subscriber
let mut has_initialized = false; let mut has_initialized = false;
glib_recv!(context.subscribe(), event => { context.widget_rx.attach(None, move |event| {
match event { match event {
WorkspaceUpdate::Init(workspaces) => { WorkspaceUpdate::Init(workspaces) => {
if !has_initialized { if !has_initialized {
trace!("Creating workspace buttons"); trace!("Creating workspace buttons");
for workspace in workspaces {
let mut added = HashSet::new(); if self.all_monitors || workspace.monitor == output_name {
let mut add_workspace = |name: &str, visibility: Visibility| {
let item = create_button( let item = create_button(
name, &workspace.name,
visibility, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size, icon_size,
&context.controller_tx, &context.controller_tx,
); );
container.add(&item); container.add(&item);
button_map.insert(name.to_string(), item);
};
// add workspaces from client button_map.insert(workspace.name, item);
for workspace in &workspaces {
if self.show_workspace_check(&output_name, workspace) {
add_workspace(&workspace.name, workspace.visibility);
added.insert(workspace.name.to_string());
}
}
let mut add_favourites = |names: &Vec<String>| {
for name in names {
if !added.contains(name) {
add_workspace(name, Visibility::Hidden);
added.insert(name.to_string());
fav_names.push(name.to_string());
}
}
};
// add workspaces from favourites
match &favs {
Favorites::Global(names) => add_favourites(names),
Favorites::ByMonitor(map) => {
if let Some(to_add) = map.get(&output_name) {
add_favourites(to_add);
}
} }
} }
@@ -268,33 +201,22 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
WorkspaceUpdate::Focus { old, new } => { WorkspaceUpdate::Focus { old, new } => {
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) { let old = button_map.get(&old);
if Some(new.monitor) == old.map(|w| w.monitor) { if let Some(old) = old {
btn.style_context().remove_class("visible"); old.style_context().remove_class("focused");
} }
btn.style_context().remove_class("focused"); let new = button_map.get(&new);
} if let Some(new) = new {
new.style_context().add_class("focused");
let new = button_map.get(&new.name);
if let Some(btn) = new {
let style = btn.style_context();
style.add_class("visible");
style.add_class("focused");
} }
} }
WorkspaceUpdate::Add(workspace) => { WorkspaceUpdate::Add(workspace) => {
if fav_names.contains(&workspace.name) { if self.all_monitors || workspace.monitor == output_name {
let btn = button_map.get(&workspace.name);
if let Some(btn) = btn {
btn.style_context().remove_class("inactive");
}
} else if self.show_workspace_check(&output_name, &workspace) {
let name = workspace.name; let name = workspace.name;
let item = create_button( let item = create_button(
&name, &name,
workspace.visibility, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size, icon_size,
@@ -314,12 +236,12 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
WorkspaceUpdate::Move(workspace) => { WorkspaceUpdate::Move(workspace) => {
if !self.hidden.contains(&workspace.name) && !self.all_monitors { if !self.all_monitors {
if workspace.monitor == output_name { if workspace.monitor == output_name {
let name = workspace.name; let name = workspace.name;
let item = create_button( let item = create_button(
&name, &name,
workspace.visibility, workspace.focused,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size, icon_size,
@@ -345,19 +267,17 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Remove(workspace) => { WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace); let button = button_map.get(&workspace);
if let Some(item) = button { if let Some(item) = button {
if fav_names.contains(&workspace) {
item.style_context().add_class("inactive");
} else {
container.remove(item); container.remove(item);
} }
} }
} WorkspaceUpdate::Update(_) => {}
WorkspaceUpdate::Unknown => warn!("Received unknown type workspace event")
}; };
Continue(true)
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,30 +1,18 @@
use glib::Propagation;
use std::collections::HashMap; use std::collections::HashMap;
use crate::config::BarPosition;
use crate::modules::ModuleInfo;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation}; use gtk::{ApplicationWindow, Orientation};
use gtk_layer_shell::LayerShell;
use tracing::debug; use tracing::debug;
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::Ironbar;
#[derive(Debug, Clone)]
pub struct PopupCacheValue {
pub name: String,
pub content: ModulePopupParts,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Popup { pub struct Popup {
pub window: ApplicationWindow, pub window: ApplicationWindow,
pub cache: HashMap<usize, PopupCacheValue>, pub cache: HashMap<usize, gtk::Box>,
monitor: Monitor, monitor: Monitor,
pos: BarPosition, pos: BarPosition,
current_widget: Option<usize>,
} }
impl Popup { impl Popup {
@@ -39,38 +27,51 @@ impl Popup {
.application(module_info.app) .application(module_info.app)
.build(); .build();
win.init_layer_shell(); gtk_layer_shell::init_for_window(&win);
win.set_monitor(module_info.monitor); gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
win.set_layer(gtk_layer_shell::Layer::Overlay); gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
win.set_namespace(env!("CARGO_PKG_NAME"));
win.set_layer_shell_margin( gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Top, gtk_layer_shell::Edge::Top,
if pos == BarPosition::Top { gap } else { 0 }, if pos == BarPosition::Top { gap } else { 0 },
); );
win.set_layer_shell_margin( gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Bottom, gtk_layer_shell::Edge::Bottom,
if pos == BarPosition::Bottom { gap } else { 0 }, if pos == BarPosition::Bottom { gap } else { 0 },
); );
win.set_layer_shell_margin( gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Left, gtk_layer_shell::Edge::Left,
if pos == BarPosition::Left { gap } else { 0 }, if pos == BarPosition::Left { gap } else { 0 },
); );
win.set_layer_shell_margin( gtk_layer_shell::set_margin(
&win,
gtk_layer_shell::Edge::Right, gtk_layer_shell::Edge::Right,
if pos == BarPosition::Right { gap } else { 0 }, if pos == BarPosition::Right { gap } else { 0 },
); );
win.set_anchor( gtk_layer_shell::set_anchor(
&win,
gtk_layer_shell::Edge::Top, gtk_layer_shell::Edge::Top,
pos == BarPosition::Top || orientation == Orientation::Vertical, pos == BarPosition::Top || orientation == Orientation::Vertical,
); );
win.set_anchor(gtk_layer_shell::Edge::Bottom, pos == BarPosition::Bottom); gtk_layer_shell::set_anchor(
win.set_anchor( &win,
gtk_layer_shell::Edge::Bottom,
pos == BarPosition::Bottom,
);
gtk_layer_shell::set_anchor(
&win,
gtk_layer_shell::Edge::Left, gtk_layer_shell::Edge::Left,
pos == BarPosition::Left || orientation == Orientation::Horizontal, pos == BarPosition::Left || orientation == Orientation::Horizontal,
); );
win.set_anchor(gtk_layer_shell::Edge::Right, pos == BarPosition::Right); gtk_layer_shell::set_anchor(
&win,
gtk_layer_shell::Edge::Right,
pos == BarPosition::Right,
);
win.connect_leave_notify_event(move |win, ev| { win.connect_leave_notify_event(move |win, ev| {
const THRESHOLD: f64 = 3.0; const THRESHOLD: f64 = 3.0;
@@ -99,7 +100,7 @@ impl Popup {
win.hide(); win.hide();
} }
Propagation::Proceed Inhibit(false)
}); });
Self { Self {
@@ -107,54 +108,20 @@ impl Popup {
cache: HashMap::new(), cache: HashMap::new(),
monitor: module_info.monitor.clone(), monitor: module_info.monitor.clone(),
pos, pos,
current_widget: None,
} }
} }
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) { pub fn register_content(&mut self, key: usize, content: gtk::Box) {
debug!("Registered popup content for #{}", key); debug!("Registered popup content for #{}", key);
self.cache.insert(key, content);
for button in &content.buttons {
let id = Ironbar::unique_id();
button.set_tag("popup-id", id);
} }
self.cache.insert(key, PopupCacheValue { name, content }); pub fn show_content(&self, key: usize) {
}
pub fn show(&mut self, widget_id: usize, button_id: usize) {
self.clear_window(); self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) { if let Some(content) = self.cache.get(&key) {
self.current_widget = Some(widget_id); content.style_context().add_class("popup");
self.window.add(content);
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
self.set_pos(geometry);
} }
} }
@@ -165,9 +132,14 @@ impl Popup {
} }
} }
/// Shows the popup
pub fn show(&self, geometry: WidgetGeometry) {
self.window.show();
self.set_pos(geometry);
}
/// Hides the popover /// Hides the popover
pub fn hide(&mut self) { pub fn hide(&self) {
self.current_widget = None;
self.window.hide(); self.window.hide();
} }
@@ -176,10 +148,6 @@ impl Popup {
self.window.is_visible() self.window.is_visible()
} }
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
}
/// 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: WidgetGeometry) { fn set_pos(&self, geometry: WidgetGeometry) {
@@ -217,6 +185,50 @@ impl Popup {
gtk_layer_shell::Edge::Top gtk_layer_shell::Edge::Top
}; };
self.window.set_layer_shell_margin(edge, offset as i32); gtk_layer_shell::set_margin(&self.window, edge, offset as i32);
}
/// Gets the absolute X position of the button
/// and its width / height (depending on orientation).
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
where
W: IsA<gtk::Widget>,
{
let widget_size = if orientation == Orientation::Horizontal {
widget.allocation().width()
} else {
widget.allocation().height()
};
let top_level = widget.toplevel().expect("Failed to get top-level widget");
let bar_size = if orientation == Orientation::Horizontal {
top_level.allocation().width()
} else {
top_level.allocation().height()
};
let (widget_x, widget_y) = widget
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_pos = if orientation == Orientation::Horizontal {
widget_x
} else {
widget_y
};
WidgetGeometry {
position: widget_pos,
size: widget_size,
bar_size,
} }
} }
}
#[derive(Debug, Copy, Clone)]
pub struct WidgetGeometry {
position: i32,
size: i32,
bar_size: i32,
}

View File

@@ -205,7 +205,7 @@ impl Script {
Ok(output) => callback(output.0, output.1), Ok(output) => callback(output.0, output.1),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}, },
ScriptMode::Watch => match self.spawn() { 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);
@@ -264,7 +264,7 @@ impl Script {
/// Spawns a long-running process. /// Spawns a long-running process.
/// Returns a `mpsc::Receiver` that sends a message /// Returns a `mpsc::Receiver` that sends a message
/// every time a new line is written to `stdout` or `stderr`. /// every time a new line is written to `stdout` or `stderr`.
pub fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> { pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
let mut handle = Command::new("/bin/sh") let mut handle = Command::new("/bin/sh")
.args(["-c", &self.cmd]) .args(["-c", &self.cmd])
.stdout(Stdio::piped()) .stdout(Stdio::piped())

View File

@@ -1,13 +1,14 @@
use crate::{glib_recv_mpsc, spawn, try_send}; use crate::send;
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use glib::Continue;
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER; use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
use gtk::prelude::CssProviderExt; use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext}; use gtk::{gdk, gio, CssProvider, StyleContext};
use notify::event::ModifyKind; use notify::event::{DataChange, ModifyKind};
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher}; use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc; use tokio::spawn;
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
@@ -35,20 +36,14 @@ pub fn load_css(style_path: PathBuf) {
GTK_STYLE_PROVIDER_PRIORITY_USER as u32, GTK_STYLE_PROVIDER_PRIORITY_USER as u32,
); );
let (tx, rx) = mpsc::channel(8); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
let style_path2 = style_path.clone();
let mut watcher = recommended_watcher(move |res: Result<Event>| match res { let mut watcher = recommended_watcher(move |res: Result<Event>| match res {
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => { Ok(event) if event.kind == EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
debug!("{event:?}"); debug!("{event:?}");
if event if let Some(path) = event.paths.first() {
.paths send!(tx, path.clone());
.first()
.map(|p| p == &style_path2)
.unwrap_or_default()
{
try_send!(tx, style_path2.clone());
} }
} }
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e), Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
@@ -56,10 +51,8 @@ pub fn load_css(style_path: PathBuf) {
}) })
.expect("Failed to create CSS file watcher"); .expect("Failed to create CSS file watcher");
let dir_path = style_path.parent().expect("to exist");
watcher watcher
.watch(dir_path, RecursiveMode::NonRecursive) .watch(&style_path, RecursiveMode::NonRecursive)
.expect("Failed to start CSS file watcher"); .expect("Failed to start CSS file watcher");
debug!("Installed CSS file watcher on '{}'", style_path.display()); debug!("Installed CSS file watcher on '{}'", style_path.display());
@@ -69,14 +62,19 @@ pub fn load_css(style_path: PathBuf) {
} }
}); });
glib_recv_mpsc!(rx, path => { {
rx.attach(None, move |path| {
info!("Reloading CSS"); info!("Reloading CSS");
if let Err(err) = provider.load_from_file(&gio::File::for_path(path)) { if let Err(err) = provider
.load_from_file(&gio::File::for_path(path)) {
error!("{:?}", Report::new(err) error!("{:?}", Report::new(err)
.wrap_err("Failed to load CSS") .wrap_err("Failed to load CSS")
.suggestion("Check the CSS file for errors") .suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.") .suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
); );
} }
Continue(true)
}); });
} }
}

9
src/unique_id.rs Normal file
View File

@@ -0,0 +1,9 @@
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(1);
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
/// This is just an `AtomicUsize` that increments every time this function is called.
pub fn get_unique_usize() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}