Compare commits
1 Commits
update_fla
...
flatpak_ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e9bdea83 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -73,6 +73,4 @@ jobs:
|
||||
name: jakestanger
|
||||
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- run: nix build --print-build-logs
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -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/),
|
||||
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
|
||||
### :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))*:
|
||||
@@ -424,5 +372,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[v0.10.0]: https://github.com/JakeStanger/ironbar/compare/v0.9.0...v0.10.0
|
||||
[v0.11.0]: https://github.com/JakeStanger/ironbar/compare/v0.10.0...v0.11.0
|
||||
[v0.12.0]: https://github.com/JakeStanger/ironbar/compare/v0.11.0...v0.12.0
|
||||
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
|
||||
[v0.13.0]: https://github.com/JakeStanger/ironbar/compare/v0.12.1...v0.13.0
|
||||
[v0.12.1]: https://github.com/JakeStanger/ironbar/compare/v0.12.0...v0.12.1
|
||||
1381
Cargo.lock
generated
1381
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
71
Cargo.toml
71
Cargo.toml
@@ -1,12 +1,10 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.14.0-pre"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||
repository = "https://github.com/jakestanger/ironbar"
|
||||
categories = ["gui"]
|
||||
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
@@ -52,7 +50,7 @@ music = ["regex"]
|
||||
|
||||
sys_info = ["sysinfo", "regex"]
|
||||
|
||||
tray = ["system-tray"]
|
||||
tray = ["stray"]
|
||||
|
||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||
|
||||
@@ -63,10 +61,10 @@ workspaces = ["futures-util"]
|
||||
|
||||
[dependencies]
|
||||
# core
|
||||
gtk = "0.18.1"
|
||||
gtk-layer-shell = "0.8.0"
|
||||
glib = "0.18.5"
|
||||
tokio = { version = "1.35.1", features = [
|
||||
gtk = "0.17.0"
|
||||
gtk-layer-shell = "0.6.0"
|
||||
glib = "0.17.10"
|
||||
tokio = { version = "1.28.2", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
@@ -75,66 +73,69 @@ tokio = { version = "1.35.1", features = [
|
||||
"io-util",
|
||||
"net",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-appender = "0.2.3"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
tracing-appender = "0.2.2"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
color-eyre = "0.6.2"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
indexmap = "2.1.0"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
indexmap = "2.0.0"
|
||||
dirs = "5.0.1"
|
||||
walkdir = "2.4.0"
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
wayland-client = "0.31.1"
|
||||
wayland-protocols = { version = "0.31.0", features = ["unstable", "client"] }
|
||||
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.18.0", default-features = false, features = [
|
||||
walkdir = "2.3.2"
|
||||
notify = { version = "6.0.1", default-features = false }
|
||||
wayland-client = "0.30.2"
|
||||
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
|
||||
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
|
||||
"calloop",
|
||||
] }
|
||||
universal-config = { version = "0.4.3", default_features = false }
|
||||
ctrlc = "3.4.2"
|
||||
universal-config = { version = "0.4.0", default_features = false }
|
||||
ctrlc = "3.4.0"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
# cli
|
||||
clap = { version = "4.4.11", optional = true, features = ["derive"] }
|
||||
clap = { version = "4.2.7", optional = true, features = ["derive"] }
|
||||
|
||||
# ipc
|
||||
serde_json = { version = "1.0.108", optional = true }
|
||||
serde_json = { version = "1.0.96", optional = true }
|
||||
|
||||
# http
|
||||
reqwest = { version = "0.11.23", optional = true }
|
||||
reqwest = { version = "0.11.18", optional = true }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||
nix = { version = "0.26.2", optional = true, features = ["event"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.31", optional = true, features = ["unstable-locales"] }
|
||||
chrono = { version = "0.4.26", optional = true }
|
||||
|
||||
# music
|
||||
mpd_client = { version = "1.3.0", optional = true }
|
||||
mpd_client = { version = "1.0.0", optional = true }
|
||||
mpris = { version = "2.0.1", optional = true }
|
||||
|
||||
# sys_info
|
||||
sysinfo = { version = "0.29.11", optional = true }
|
||||
sysinfo = { version = "0.29.2", optional = true }
|
||||
|
||||
# tray
|
||||
system-tray = { version = "0.1.4", optional = true }
|
||||
stray = { version = "0.1.3", optional = true }
|
||||
|
||||
# upower
|
||||
upower_dbus = { version = "0.3.2", optional = true }
|
||||
futures-lite = { version = "2.1.0", optional = true }
|
||||
zbus = { version = "3.14.1", optional = true }
|
||||
futures-lite = { version = "1.12.0", optional = true }
|
||||
zbus = { version = "3.13.1", optional = true }
|
||||
|
||||
# workspaces
|
||||
swayipc-async = { version = "2.0.1", optional = true }
|
||||
hyprland = { version = "0.3.12", features = ["silent"], optional = true }
|
||||
futures-util = { version = "0.3.30", optional = true }
|
||||
hyprland = { version = "=0.3.1", optional = true }
|
||||
futures-util = { version = "0.3.21", optional = true }
|
||||
|
||||
# shared
|
||||
regex = { version = "1.10.2", default-features = false, features = [
|
||||
regex = { version = "1.8.4", default-features = false, features = [
|
||||
"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" }
|
||||
|
||||
12
README.md
12
README.md
@@ -75,15 +75,7 @@ cargo install ironbar
|
||||
yay -S ironbar-git
|
||||
```
|
||||
|
||||
### Nix
|
||||
|
||||
[nix package](https://search.nixos.org/packages?channel=unstable&show=ironbar)
|
||||
|
||||
```sh
|
||||
nix-shell -p ironbar
|
||||
```
|
||||
|
||||
#### Flake
|
||||
### Nix Flake
|
||||
|
||||
A flake is included with the repo which can be used with Home Manager.
|
||||
|
||||
@@ -178,4 +170,4 @@ All are welcome, but I ask a few basic things to help make things easier. Please
|
||||
- [Waybar](https://github.com/Alexays/Waybar) - A lot of the initial inspiration, and a pretty great bar.
|
||||
- [Rustbar](https://github.com/zeroeightysix/rustbar) - Served as a good demo for writing a basic GTK bar in Rust
|
||||
- [Smithay Client Toolkit](https://github.com/Smithay/client-toolkit) - Essential in being able to communicate to Wayland
|
||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
||||
- [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell) - Ironbar and many other projects would be impossible without this
|
||||
@@ -18,8 +18,6 @@ You also need rust; only the latest stable version is supported.
|
||||
|
||||
```shell
|
||||
pacman -S gtk3 gtk-layer-shell
|
||||
# for http support
|
||||
pacman -S openssl
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
@@ -33,9 +31,7 @@ apt install libssl-dev
|
||||
### Fedora
|
||||
|
||||
```shell
|
||||
dnf install gtk3-devel gtk-layer-shell-devel
|
||||
# for http support
|
||||
dnf install openssl-devel
|
||||
dnf install gtk3 gtk-layer-shell
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
@@ -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,
|
||||
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.
|
||||
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`.
|
||||
To find your output names, run `wayland-info | grep wl_output -A1`.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -271,24 +267,21 @@ Check [here](config) for an example config file for a fully configured bar in ea
|
||||
|
||||
The following table lists each of the top-level bar config options:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `start_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. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
|
||||
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
|
||||
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
|
||||
| `height` | `integer` | `42` | The bar's height in pixels. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `margin.top` | `integer` | `0` | The margin on the top of the bar |
|
||||
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
|
||||
| `margin.left` | `integer` | `0` | The margin on the left of the bar |
|
||||
| `margin.right` | `integer` | `0` | The margin on the right of the bar |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||
|
||||
### 3.2 Module-level options
|
||||
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
### `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`
|
||||
|
||||
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
|
||||
|
||||
### `ok`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
In some configuration locations, Ironbar supports dynamic values,
|
||||
meaning you can inject content into the bar from an external source.
|
||||
|
||||
Currently two dynamic content sources are supported - [scripts](scripts) (via shorthand syntax) and [ironvars](ironvars).
|
||||
Currently two dynamic content sources are supported - scripts and ironvars.
|
||||
|
||||
## Dynamic String
|
||||
|
||||
@@ -36,4 +36,4 @@ Example:
|
||||
```toml
|
||||
show_if = "exit 0" # script
|
||||
show_if = "#show_module" # variable
|
||||
```
|
||||
```
|
||||
@@ -10,34 +10,25 @@ which only includes a subset of the full web spec (plus a few non-standard prope
|
||||
The below table describes the selectors provided by the bar itself.
|
||||
Information on styling individual modules can be found on their pages in the sidebar.
|
||||
|
||||
| Selector | Description |
|
||||
|---------------------|--------------------------------------------|
|
||||
| `.background` | Top-level window. |
|
||||
| `#bar` | Bar root box. |
|
||||
| `#bar #start` | Bar left or top modules container box. |
|
||||
| `#bar #center` | Bar center modules container box. |
|
||||
| `#bar #end` | Bar right or bottom modules container box. |
|
||||
| `.container` | All of the above. |
|
||||
| `.widget-container` | The `EventBox` wrapping any widget. |
|
||||
| `.widget` | Any widget. |
|
||||
| `.popup` | Any popup box. |
|
||||
| Selector | Description |
|
||||
|----------------|--------------------------------------------|
|
||||
| `.background` | Top-level window. |
|
||||
| `#bar` | Bar root box. |
|
||||
| `#bar #start` | Bar left or top modules container box. |
|
||||
| `#bar #center` | Bar center modules container box. |
|
||||
| `#bar #end` | Bar right or bottom modules container box. |
|
||||
| `.container` | All of the above. |
|
||||
| `.popup` | Any popup box. |
|
||||
|
||||
Every 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.
|
||||
|
||||
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
|
||||
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
|
||||
|
||||
You can also target all GTK widgets of a certain type directly using their name. For example, `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`.
|
||||
|
||||
> [!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:
|
||||
|
||||
```css
|
||||
@@ -46,4 +37,4 @@ GTK CSS does not support custom properties, but it does have its own custom `@de
|
||||
box, menubar {
|
||||
background-color: @color_bg;
|
||||
}
|
||||
```
|
||||
```
|
||||
@@ -8,13 +8,9 @@ Clicking on the widget opens a popup with the time and a calendar.
|
||||
|
||||
> Type: `clock`
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|----------|------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. |
|
||||
| `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>
|
||||
| Name | Type | Default | Description |
|
||||
|----------|----------|------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `format` | `string` | `%d/%m/%Y %H:%M` | Date/time format string. Detail on available tokens can be found here: <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
@@ -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. |
|
||||
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||
| `icons.prev` | `string` or [image](images) | `` | Icon to show on previous button. |
|
||||
| `icons.next` | `string` or [image](images) | `` | Icon to show on next button. |
|
||||
| `icons.volume` | `string` or [image](images) | `` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string` or [image](images) | `` | Icon to show next to artist name. |
|
||||
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
|
||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||
@@ -128,6 +128,8 @@ and will be replaced with values from the currently playing track:
|
||||
| `{track}` | Track number |
|
||||
| `{disc}` | Disc number |
|
||||
| `{genre}` | Genre |
|
||||
| `{duration}` | Duration in `mm:ss` |
|
||||
| `{elapsed}` | Time elapsed in `mm:ss` |
|
||||
|
||||
## 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-next` | Next button inside popup box |
|
||||
| `.popup-music .volume` | Volume container inside popup box |
|
||||
| `.popup-music .volume .slider` | Slider inside volume container |
|
||||
| `.popup-music .volume .icon` | Icon inside volume container |
|
||||
| `.popup-music .progress` | Progress (seek) bar container |
|
||||
| `.popup-music .progress .slider` | Slider inside progress container |
|
||||
| `.popup-music .progress .label` | Duration label inside progress container |
|
||||
| `.popup-music .volume .slider` | Volume slider popup box |
|
||||
| `.popup-music .volume .icon` | Volume icon label inside popup box |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -28,13 +28,13 @@ Pango markup is supported.
|
||||
"end": [
|
||||
{
|
||||
"format": [
|
||||
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C",
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
],
|
||||
"interval": {
|
||||
"cpu": 1,
|
||||
@@ -58,13 +58,13 @@ Pango markup is supported.
|
||||
[[end]]
|
||||
type = 'sys_info'
|
||||
format = [
|
||||
' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C',
|
||||
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
' {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
' {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
]
|
||||
|
||||
[end.interval]
|
||||
@@ -85,13 +85,13 @@ temps = 5
|
||||
```yaml
|
||||
end:
|
||||
- format:
|
||||
- ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C'
|
||||
- ' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C'
|
||||
- ' {memory_used} / {memory_total} GB ({memory_percent}%)'
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
|
||||
- ' {net_down:enp39s0} / {net_up:enp39s0} Mbps'
|
||||
- ' {load_average:1} | {load_average:5} | {load_average:15}'
|
||||
- ' {uptime}'
|
||||
- ' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)'
|
||||
- '李 {net_down:enp39s0} / {net_up:enp39s0} Mbps'
|
||||
- '猪 {load_average:1} | {load_average:5} | {load_average:15}'
|
||||
- ' {uptime}'
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
@@ -119,13 +119,13 @@ end:
|
||||
interval.networks = 3
|
||||
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C"
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
" {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -168,7 +168,7 @@ The following tokens can be used in the `format` configuration option:
|
||||
| `{load_average:15}` | 15-minute load average. |
|
||||
| `{uptime}` | System uptime formatted as `HH:mm`. |
|
||||
|
||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp-Tccd1`.
|
||||
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp_Tccd1`.
|
||||
|
||||
## Styling
|
||||
|
||||
|
||||
@@ -8,14 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
|
||||
> Type: `workspaces`
|
||||
|
||||
| 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. |
|
||||
| `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). |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
| Name | Type | Default | Description |
|
||||
|----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
@@ -30,7 +28,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
"2": "",
|
||||
"3": ""
|
||||
},
|
||||
"favorites": ["1", "2", "3"],
|
||||
"all_monitors": false
|
||||
}
|
||||
]
|
||||
@@ -46,7 +43,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||
[[end]]
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
favorites = ["1", "2", "3"]
|
||||
|
||||
[[end.name_map]]
|
||||
1 = ""
|
||||
@@ -67,10 +63,6 @@ end:
|
||||
1: ""
|
||||
2: ""
|
||||
3: ""
|
||||
favorites:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
all_monitors: false
|
||||
```
|
||||
|
||||
@@ -87,7 +79,6 @@ end:
|
||||
name_map.1 = ""
|
||||
name_map.2 = ""
|
||||
name_map.3 = ""
|
||||
favorites = [ "1" "2" "3" ]
|
||||
all_monitors = false
|
||||
}
|
||||
]
|
||||
@@ -103,10 +94,8 @@ end:
|
||||
| `.workspaces` | Workspaces widget box |
|
||||
| `.workspaces .item` | Workspace button |
|
||||
| `.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 .text-icon` | Workspace button icon (textual only) |
|
||||
| `.workspaces .item .image` | Workspace button icon (image only) |
|
||||
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
For more information on styling, please see the [styling guide](styling-guide).
|
||||
@@ -3,7 +3,7 @@ let {
|
||||
type = "workspaces"
|
||||
all_monitors = false
|
||||
name_map = {
|
||||
1 = ""
|
||||
1 = "ﭮ"
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Games = "icon:steam"
|
||||
@@ -46,10 +46,10 @@ let {
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C"
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)"
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
" {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)"
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps"
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}"
|
||||
" {uptime}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ let {
|
||||
|
||||
$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 --
|
||||
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
"李 {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
"猪 {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}"
|
||||
],
|
||||
"interval": {
|
||||
"cpu": 1,
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"all_monitors": false,
|
||||
"name_map": {
|
||||
"1": "",
|
||||
"1": "ﭮ",
|
||||
"2": "icon:firefox",
|
||||
"3": "",
|
||||
"Code": "",
|
||||
@@ -128,7 +128,7 @@
|
||||
"type": "launcher"
|
||||
},
|
||||
{
|
||||
"label": "random num: {{500:echo FIXME}}",
|
||||
"label": "random num: {{500:echo $RANDOM}}",
|
||||
"type": "label"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
anchor_to_edges = true
|
||||
icon_theme = "Paper"
|
||||
position = "bottom"
|
||||
icon_theme = 'Paper'
|
||||
position = 'bottom'
|
||||
|
||||
[[end]]
|
||||
music_dir = "/home/jake/Music"
|
||||
player_type = "mpd"
|
||||
type = "music"
|
||||
music_dir = '/home/jake/Music'
|
||||
player_type = 'mpd'
|
||||
type = 'music'
|
||||
|
||||
[end.truncate]
|
||||
max_length = 100
|
||||
mode = "end"
|
||||
mode = 'end'
|
||||
|
||||
[[end]]
|
||||
host = "chloe:6600"
|
||||
player_type = "mpd"
|
||||
truncate = "end"
|
||||
type = "music"
|
||||
host = 'chloe:6600'
|
||||
player_type = 'mpd'
|
||||
truncate = 'end'
|
||||
type = 'music'
|
||||
|
||||
[[end]]
|
||||
cmd = "/home/jake/bin/phone-battery"
|
||||
type = "script"
|
||||
cmd = '/home/jake/bin/phone-battery'
|
||||
type = 'script'
|
||||
|
||||
[end.show_if]
|
||||
cmd = "/home/jake/bin/phone-connected"
|
||||
cmd = '/home/jake/bin/phone-connected'
|
||||
interval = 500
|
||||
|
||||
[[end]]
|
||||
type = 'sys_info'
|
||||
format = [
|
||||
" {cpu_percent}% | {temp_c:k10temp_Tccd1}°C",
|
||||
" {memory_used} / {memory_total} GB ({memory_percent}%)",
|
||||
"| {swap_used} / {swap_total} GB ({swap_percent}%)",
|
||||
" {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)",
|
||||
" {net_down:enp39s0} / {net_up:enp39s0} Mbps",
|
||||
" {load_average:1} | {load_average:5} | {load_average:15}",
|
||||
" {uptime}",
|
||||
' {cpu_percent}% | {temp_c:k10temp_Tccd1}°C',
|
||||
' {memory_used} / {memory_total} GB ({memory_percent}%)',
|
||||
'| {swap_used} / {swap_total} GB ({swap_percent}%)',
|
||||
' {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)',
|
||||
'李 {net_down:enp39s0} / {net_up:enp39s0} Mbps',
|
||||
'猪 {load_average:1} | {load_average:5} | {load_average:15}',
|
||||
' {uptime}',
|
||||
]
|
||||
type = "sys_info"
|
||||
|
||||
[end.interval]
|
||||
cpu = 1
|
||||
@@ -46,77 +46,77 @@ temps = 5
|
||||
|
||||
[[end]]
|
||||
max_items = 3
|
||||
type = "clipboard"
|
||||
type = 'clipboard'
|
||||
|
||||
[end.truncate]
|
||||
length = 50
|
||||
mode = "end"
|
||||
mode = 'end'
|
||||
|
||||
[[end]]
|
||||
class = "power-menu"
|
||||
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
type = "custom"
|
||||
class = 'power-menu'
|
||||
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
type = 'custom'
|
||||
|
||||
[[end.bar]]
|
||||
label = ""
|
||||
name = "power-btn"
|
||||
on_click = "popup:toggle"
|
||||
type = "button"
|
||||
label = ''
|
||||
name = 'power-btn'
|
||||
on_click = 'popup:toggle'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup]]
|
||||
orientation = "vertical"
|
||||
type = "box"
|
||||
orientation = 'vertical'
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = "Power menu"
|
||||
name = "header"
|
||||
type = "label"
|
||||
label = 'Power menu'
|
||||
name = 'header'
|
||||
type = 'label'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
type = "box"
|
||||
type = 'box'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!shutdown now"
|
||||
type = "button"
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!shutdown now'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets.widgets]]
|
||||
class = "power-btn"
|
||||
label = "<span font-size='40pt'></span>"
|
||||
on_click = "!reboot"
|
||||
type = "button"
|
||||
class = 'power-btn'
|
||||
label = '''<span font-size='40pt'></span>'''
|
||||
on_click = '!reboot'
|
||||
type = 'button'
|
||||
|
||||
[[end.popup.widgets]]
|
||||
label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||
name = "uptime"
|
||||
type = "label"
|
||||
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||
name = 'uptime'
|
||||
type = 'label'
|
||||
|
||||
[[end]]
|
||||
type = "clock"
|
||||
type = 'clock'
|
||||
|
||||
[[start]]
|
||||
all_monitors = false
|
||||
type = "workspaces"
|
||||
type = 'workspaces'
|
||||
|
||||
[start.name_map]
|
||||
1 = ""
|
||||
2 = "icon:firefox"
|
||||
3 = ""
|
||||
Code = ""
|
||||
Games = "icon:steam"
|
||||
1 = 'ﭮ'
|
||||
2 = 'icon:firefox'
|
||||
3 = ''
|
||||
Code = ''
|
||||
Games = 'icon:steam'
|
||||
|
||||
[[start]]
|
||||
favorites = [
|
||||
"firefox",
|
||||
"discord",
|
||||
"steam",
|
||||
]
|
||||
show_icons = true
|
||||
show_names = false
|
||||
type = "launcher"
|
||||
type = 'launcher'
|
||||
favorites = [
|
||||
'firefox',
|
||||
'discord',
|
||||
'steam',
|
||||
]
|
||||
|
||||
[[start]]
|
||||
label = "random num: {{500:echo FIXME}}"
|
||||
type = "label"
|
||||
label = 'random num: {{500:echo $RANDOM}}'
|
||||
type = 'label'
|
||||
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
anchor_to_edges: true
|
||||
end:
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- format:
|
||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
||||
- {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||
- {load_average:1} | {load_average:5} | {load_average:15}
|
||||
- {uptime}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
- music_dir: /home/jake/Music
|
||||
player_type: mpd
|
||||
truncate:
|
||||
max_length: 100
|
||||
mode: end
|
||||
type: music
|
||||
- host: chloe:6600
|
||||
player_type: mpd
|
||||
truncate: end
|
||||
type: music
|
||||
- cmd: /home/jake/bin/phone-battery
|
||||
show_if:
|
||||
cmd: /home/jake/bin/phone-connected
|
||||
interval: 500
|
||||
type: script
|
||||
- format:
|
||||
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
|
||||
- {memory_used} / {memory_total} GB ({memory_percent}%)
|
||||
- '| {swap_used} / {swap_total} GB ({swap_percent}%)'
|
||||
- {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)
|
||||
- 李 {net_down:enp39s0} / {net_up:enp39s0} Mbps
|
||||
- 猪 {load_average:1} | {load_average:5} | {load_average:15}
|
||||
- {uptime}
|
||||
interval:
|
||||
cpu: 1
|
||||
disks: 300
|
||||
memory: 30
|
||||
networks: 3
|
||||
temps: 5
|
||||
type: sys_info
|
||||
- max_items: 3
|
||||
truncate:
|
||||
length: 50
|
||||
mode: end
|
||||
type: clipboard
|
||||
- bar:
|
||||
- label:
|
||||
name: power-btn
|
||||
on_click: popup:toggle
|
||||
type: button
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
class: power-menu
|
||||
popup:
|
||||
- orientation: vertical
|
||||
type: box
|
||||
widgets:
|
||||
- label: Power menu
|
||||
name: header
|
||||
type: label
|
||||
- type: box
|
||||
widgets:
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!shutdown now'
|
||||
type: button
|
||||
- class: power-btn
|
||||
label: <span font-size='40pt'></span>
|
||||
on_click: '!reboot'
|
||||
type: button
|
||||
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
name: uptime
|
||||
type: label
|
||||
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||
type: custom
|
||||
- type: clock
|
||||
icon_theme: Paper
|
||||
position: bottom
|
||||
start:
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1':
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo FIXME}}'
|
||||
type: label
|
||||
- all_monitors: false
|
||||
name_map:
|
||||
'1': ﭮ
|
||||
'2': icon:firefox
|
||||
'3':
|
||||
Code:
|
||||
Games: icon:steam
|
||||
type: workspaces
|
||||
- favorites:
|
||||
- firefox
|
||||
- discord
|
||||
- steam
|
||||
show_icons: true
|
||||
show_names: false
|
||||
type: launcher
|
||||
- label: 'random num: {{500:echo $RANDOM}}'
|
||||
type: label
|
||||
|
||||
|
||||
@@ -120,11 +120,7 @@ button:hover {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.popup-music .icon-box {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.popup-music .title .icon, .popup-music .title .label {
|
||||
.popup-music .title .icon *, .popup-music .title .label {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
@@ -132,17 +128,15 @@ button:hover {
|
||||
color: @color_border;
|
||||
}
|
||||
|
||||
.popup-music .volume .slider slider {
|
||||
.popup-music .volume scale slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.popup-music .volume .icon {
|
||||
margin-left: 4px;
|
||||
/* volume icon */
|
||||
.popup-music .volume > box:last-child label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.popup-music .progress .slider slider {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* -- script -- */
|
||||
|
||||
|
||||
68
flake.lock
generated
68
flake.lock
generated
@@ -1,25 +1,5 @@
|
||||
{
|
||||
"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": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -38,45 +18,13 @@
|
||||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1704008649,
|
||||
"narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1703637592,
|
||||
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
|
||||
"lastModified": 1686960236,
|
||||
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
|
||||
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -88,9 +36,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
@@ -102,11 +48,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703902408,
|
||||
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
|
||||
"lastModified": 1686968542,
|
||||
"narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
|
||||
"rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
57
flake.nix
57
flake.nix
@@ -6,18 +6,11 @@
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
crane,
|
||||
naersk,
|
||||
...
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
@@ -34,18 +27,10 @@
|
||||
rust-overlay.overlays.default
|
||||
];
|
||||
};
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
|
||||
in {
|
||||
overlays.default = final: prev: let
|
||||
rust = mkRustToolchain final;
|
||||
craneLib = (crane.mkLib final).overrideToolchain rust;
|
||||
naersk' = prev.callPackage naersk {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
|
||||
rustPlatform = prev.makeRustPlatform {
|
||||
cargo = rust;
|
||||
@@ -57,33 +42,11 @@
|
||||
(builtins.substring 4 2 longDate)
|
||||
(builtins.substring 6 2 longDate)
|
||||
]);
|
||||
builder = "naersk";
|
||||
in {
|
||||
ironbar = let
|
||||
ironbar = prev.callPackage ./nix/default.nix {
|
||||
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;
|
||||
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;
|
||||
};
|
||||
inherit rustPlatform;
|
||||
};
|
||||
};
|
||||
packages = genSystems (
|
||||
system: let
|
||||
@@ -119,14 +82,6 @@
|
||||
gtk-layer-shell
|
||||
pkg-config
|
||||
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";
|
||||
@@ -193,8 +148,8 @@
|
||||
ExecStart = "${pkg}/bin/ironbar";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemdIntegration "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemdIntegration "sway-session.target")
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
116
nix/default.nix
116
nix/default.nix
@@ -19,77 +19,49 @@
|
||||
lib,
|
||||
version ? "git",
|
||||
features ? [],
|
||||
builderName ? "nix",
|
||||
builder ? {},
|
||||
}: let
|
||||
basePkg = rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||
propagatedBuildInputs = [
|
||||
gtk3
|
||||
];
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# Thumbnailers
|
||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||
)
|
||||
'';
|
||||
passthru = {
|
||||
updateScript = gnome.updateScript {
|
||||
packageName = pname;
|
||||
attrPath = "gnome.${pname}";
|
||||
};
|
||||
};
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
inherit version;
|
||||
pname = "ironbar";
|
||||
src = builtins.path {
|
||||
name = "ironbar";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
buildNoDefaultFeatures =
|
||||
if features == []
|
||||
then false
|
||||
else true;
|
||||
buildFeatures = features;
|
||||
cargoDeps = rustPlatform.importCargoLock {
|
||||
lockFile = ../Cargo.lock;
|
||||
};
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
|
||||
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
|
||||
buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon openssl];
|
||||
propagatedBuildInputs = [
|
||||
gtk3
|
||||
];
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# Thumbnailers
|
||||
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
|
||||
--prefix XDG_DATA_DIRS : "${librsvg}/share"
|
||||
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
|
||||
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
|
||||
)
|
||||
'';
|
||||
passthru = {
|
||||
updateScript = gnome.updateScript {
|
||||
packageName = pname;
|
||||
attrPath = "gnome.${pname}";
|
||||
};
|
||||
};
|
||||
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=";
|
||||
})
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/JakeStanger/ironbar";
|
||||
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "ironbar";
|
||||
};
|
||||
}
|
||||
|
||||
17
shell.nix
17
shell.nix
@@ -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
|
||||
];
|
||||
}
|
||||
477
src/bar.rs
477
src/bar.rs
@@ -3,311 +3,125 @@ use crate::modules::{
|
||||
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
|
||||
};
|
||||
use crate::popup::Popup;
|
||||
use crate::{Config, Ironbar};
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use crate::Config;
|
||||
use color_eyre::Result;
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
||||
use gtk_layer_shell::LayerShell;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tracing::{debug, info};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Inner {
|
||||
New { config: Option<Config> },
|
||||
Loaded { popup: Rc<RefCell<Popup>> },
|
||||
/// Creates a new window for a bar,
|
||||
/// sets it up and adds its widgets.
|
||||
pub fn create_bar(
|
||||
app: &Application,
|
||||
monitor: &Monitor,
|
||||
monitor_name: &str,
|
||||
config: Config,
|
||||
) -> Result<()> {
|
||||
let win = ApplicationWindow::builder().application(app).build();
|
||||
|
||||
setup_layer_shell(
|
||||
&win,
|
||||
monitor,
|
||||
config.position,
|
||||
config.anchor_to_edges,
|
||||
config.margin,
|
||||
);
|
||||
|
||||
let orientation = config.position.get_orientation();
|
||||
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(orientation)
|
||||
.spacing(0)
|
||||
.hexpand(false)
|
||||
.name("bar");
|
||||
|
||||
let content = if orientation == Orientation::Horizontal {
|
||||
content.height_request(config.height)
|
||||
} else {
|
||||
content.width_request(config.height)
|
||||
}
|
||||
.build();
|
||||
|
||||
content.style_context().add_class("container");
|
||||
|
||||
let start = create_container("start", orientation);
|
||||
let center = create_container("center", orientation);
|
||||
let end = create_container("end", orientation);
|
||||
|
||||
content.add(&start);
|
||||
content.set_center_widget(Some(¢er));
|
||||
content.pack_end(&end, false, false, 0);
|
||||
|
||||
load_modules(&start, ¢er, &end, app, config, monitor, monitor_name)?;
|
||||
win.add(&content);
|
||||
|
||||
win.connect_destroy_event(|_, _| {
|
||||
info!("Shutting down");
|
||||
gtk::main_quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
debug!("Showing bar");
|
||||
|
||||
// show each box but do not use `show_all`.
|
||||
// this ensures `show_if` option works as intended.
|
||||
start.show();
|
||||
center.show();
|
||||
end.show();
|
||||
content.show();
|
||||
win.show();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bar {
|
||||
name: String,
|
||||
monitor_name: String,
|
||||
/// Sets up GTK layer shell for a provided application window.
|
||||
fn setup_layer_shell(
|
||||
win: &ApplicationWindow,
|
||||
monitor: &Monitor,
|
||||
position: BarPosition,
|
||||
anchor_to_edges: bool,
|
||||
margin: MarginConfig,
|
||||
) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
gtk_layer_shell::set_layer(win, gtk_layer_shell::Layer::Top);
|
||||
gtk_layer_shell::auto_exclusive_zone_enable(win);
|
||||
gtk_layer_shell::set_namespace(win, env!("CARGO_PKG_NAME"));
|
||||
|
||||
window: ApplicationWindow,
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Top, margin.top);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Bottom, margin.bottom);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Left, margin.left);
|
||||
gtk_layer_shell::set_margin(win, gtk_layer_shell::Edge::Right, margin.right);
|
||||
|
||||
content: gtk::Box,
|
||||
let bar_orientation = position.get_orientation();
|
||||
|
||||
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()
|
||||
.orientation(orientation)
|
||||
.spacing(0)
|
||||
.hexpand(false)
|
||||
.name("bar");
|
||||
|
||||
let content = if orientation == Orientation::Horizontal {
|
||||
content.height_request(config.height)
|
||||
} else {
|
||||
content.width_request(config.height)
|
||||
}
|
||||
.build();
|
||||
|
||||
content.style_context().add_class("container");
|
||||
|
||||
let start = create_container("start", orientation);
|
||||
let center = create_container("center", orientation);
|
||||
let end = create_container("end", orientation);
|
||||
|
||||
content.add(&start);
|
||||
content.set_center_widget(Some(¢er));
|
||||
content.pack_end(&end, false, false, 0);
|
||||
|
||||
window.add(&content);
|
||||
|
||||
window.connect_destroy_event(|_, _| {
|
||||
info!("Shutting down");
|
||||
gtk::main_quit();
|
||||
Propagation::Proceed
|
||||
});
|
||||
|
||||
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> {
|
||||
let Inner::New { ref mut config } = self.inner else {
|
||||
return Ok(self);
|
||||
};
|
||||
|
||||
let Some(config) = config.take() else {
|
||||
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.
|
||||
fn setup_layer_shell(
|
||||
&self,
|
||||
win: &impl IsA<Window>,
|
||||
exclusive_zone: bool,
|
||||
anchor_to_edges: bool,
|
||||
margin: MarginConfig,
|
||||
monitor: &Monitor,
|
||||
) {
|
||||
let position = self.position;
|
||||
|
||||
win.init_layer_shell();
|
||||
win.set_monitor(monitor);
|
||||
win.set_layer(gtk_layer_shell::Layer::Top);
|
||||
win.set_namespace(env!("CARGO_PKG_NAME"));
|
||||
|
||||
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();
|
||||
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::Edge::Top,
|
||||
position == BarPosition::Top
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
position == BarPosition::Bottom
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::Edge::Left,
|
||||
position == BarPosition::Left
|
||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||
);
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::Edge::Right,
|
||||
position == BarPosition::Right
|
||||
|| (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(),
|
||||
}
|
||||
}
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
position == BarPosition::Top
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
position == BarPosition::Bottom
|
||||
|| (bar_orientation == Orientation::Vertical && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Left,
|
||||
position == BarPosition::Left
|
||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
win,
|
||||
gtk_layer_shell::Edge::Right,
|
||||
position == BarPosition::Right
|
||||
|| (bar_orientation == Orientation::Horizontal && anchor_to_edges),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Box` container to place widgets inside.
|
||||
@@ -322,9 +136,54 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
|
||||
container
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BarLoadResult {
|
||||
popup: Rc<RefCell<Popup>>,
|
||||
/// Loads the configured modules onto a bar.
|
||||
fn load_modules(
|
||||
left: >k::Box,
|
||||
center: >k::Box,
|
||||
right: >k::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,
|
||||
@@ -333,20 +192,14 @@ fn add_modules(
|
||||
content: >k::Box,
|
||||
modules: Vec<ModuleConfig>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<()> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
macro_rules! add_module {
|
||||
($module:expr, $id:expr) => {{
|
||||
let common = $module.common.take().expect("common config to exist");
|
||||
let widget_parts = create_module(
|
||||
*$module,
|
||||
$id,
|
||||
common.name.clone(),
|
||||
&info,
|
||||
&Rc::clone(&popup),
|
||||
)?;
|
||||
let common = $module.common.take().expect("Common config did not exist");
|
||||
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
|
||||
set_widget_identifiers(&widget_parts, &common);
|
||||
|
||||
let container = wrap_widget(&widget_parts.widget, common, orientation);
|
||||
@@ -355,7 +208,7 @@ fn add_modules(
|
||||
}
|
||||
|
||||
for config in modules {
|
||||
let id = Ironbar::unique_id();
|
||||
let id = get_unique_usize();
|
||||
match config {
|
||||
#[cfg(feature = "clipboard")]
|
||||
ModuleConfig::Clipboard(mut module) => add_module!(module, id),
|
||||
@@ -381,13 +234,3 @@ fn add_modules(
|
||||
|
||||
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
44
src/bridge_channel.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::wayland::{self, ClipboardItem};
|
||||
use crate::{arc_mut, lock, spawn, try_send};
|
||||
use crate::{lock, try_send};
|
||||
use indexmap::map::Iter;
|
||||
use indexmap::IndexMap;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
@@ -27,9 +28,9 @@ impl ClipboardClient {
|
||||
fn new() -> Self {
|
||||
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();
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{lock, send};
|
||||
use color_eyre::Result;
|
||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
use hyprland::event_listener::EventListener;
|
||||
use hyprland::event_listener::EventListenerMutable as EventListener;
|
||||
use hyprland::prelude::*;
|
||||
use hyprland::shared::{HyprDataVec, WorkspaceType};
|
||||
use hyprland::shared::WorkspaceType;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub struct EventClient {
|
||||
@@ -34,25 +36,28 @@ impl EventClient {
|
||||
let mut event_listener = EventListener::new();
|
||||
|
||||
// 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
|
||||
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 lock = lock.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);
|
||||
debug!("Added workspace: {workspace_type:?}");
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let prev_workspace = lock!(active);
|
||||
let focused = prev_workspace
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.name == workspace_name);
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
let workspace = Self::get_workspace(&workspace_name, focused);
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||
@@ -65,7 +70,7 @@ impl EventClient {
|
||||
let lock = lock.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 mut prev_workspace = lock!(active);
|
||||
@@ -76,7 +81,10 @@ impl EventClient {
|
||||
);
|
||||
|
||||
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(
|
||||
|| {
|
||||
@@ -84,7 +92,8 @@ impl EventClient {
|
||||
},
|
||||
|workspace| {
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
@@ -97,9 +106,9 @@ impl EventClient {
|
||||
let lock = lock.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 workspace_type = event_data.workspace;
|
||||
let workspace_type = event_data.1;
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
@@ -109,11 +118,12 @@ impl EventClient {
|
||||
);
|
||||
|
||||
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)) =
|
||||
workspace.map(|w| (w.visibility.is_focused(), w))
|
||||
{
|
||||
if let (Some(workspace), false) = (workspace, focused) {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
} else {
|
||||
error!("Unable to locate workspace");
|
||||
@@ -125,20 +135,23 @@ impl EventClient {
|
||||
let tx = tx.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 workspace_type = event_data.workspace;
|
||||
let workspace_type = event_data.1;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let 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 {
|
||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||
|
||||
if !workspace.visibility.is_focused() {
|
||||
if !focused {
|
||||
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);
|
||||
debug!("Received workspace destroy: {workspace_type:?}");
|
||||
|
||||
@@ -168,28 +181,32 @@ impl EventClient {
|
||||
workspace: Workspace,
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
) {
|
||||
let old = prev_workspace
|
||||
.as_ref()
|
||||
.map(|w| w.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Focus {
|
||||
old: prev_workspace.take(),
|
||||
new: workspace.clone(),
|
||||
old,
|
||||
new: workspace.name.clone(),
|
||||
}
|
||||
);
|
||||
|
||||
prev_workspace.replace(workspace);
|
||||
}
|
||||
|
||||
/// Gets a workspace by name from the server, given the active workspace if known.
|
||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
|
||||
/// Gets a workspace by name from the server.
|
||||
///
|
||||
/// Use `focused` to manually mark the workspace as focused,
|
||||
/// as this is not automatically checked.
|
||||
fn get_workspace(name: &str, focused: bool) -> Option<Workspace> {
|
||||
Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.find_map(|w| {
|
||||
if w.name == name {
|
||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
||||
create_is_visible()(w)
|
||||
}));
|
||||
|
||||
Some(Workspace::from((vis, w)))
|
||||
Some(Workspace::from((focused, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -198,19 +215,16 @@ impl EventClient {
|
||||
|
||||
/// Gets the active workspace from the server.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for EventClient {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
let identifier = match id.parse::<i32>() {
|
||||
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum),
|
||||
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
};
|
||||
|
||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
||||
Dispatch::call(DispatchType::Workspace(
|
||||
WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -220,16 +234,13 @@ impl WorkspaceClient for EventClient {
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
|
||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||
let is_visible = create_is_visible();
|
||||
let active_name = HWorkspace::get_active()
|
||||
.map(|active| active.name)
|
||||
.unwrap_or_default();
|
||||
|
||||
let workspaces = Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.map(|w| {
|
||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||
|
||||
Workspace::from((vis, w))
|
||||
})
|
||||
.map(|w| Workspace::from((w.name == active_name, w)))
|
||||
.collect();
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
impl From<(bool, hyprland::data::Workspace)> for Workspace {
|
||||
fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self {
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.monitor,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
focused,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,38 +75,8 @@ pub struct Workspace {
|
||||
pub name: String,
|
||||
/// Name of the monitor (output) the workspace is located on
|
||||
pub monitor: String,
|
||||
/// How visible the workspace is
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
/// Whether the workspace is in focus
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -116,17 +86,13 @@ pub enum WorkspaceUpdate {
|
||||
Init(Vec<Workspace>),
|
||||
Add(Workspace),
|
||||
Remove(String),
|
||||
Update(Workspace),
|
||||
Move(Workspace),
|
||||
/// Declares focus moved from the old workspace to the new.
|
||||
Focus {
|
||||
old: Option<Workspace>,
|
||||
new: Workspace,
|
||||
old: String,
|
||||
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 {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{await_sync, send, spawn};
|
||||
use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{await_sync, send};
|
||||
use async_once::AsyncOnce;
|
||||
use color_eyre::Report;
|
||||
use futures_util::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, trace};
|
||||
@@ -31,11 +32,8 @@ impl SwayEventClient {
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
trace!("event: {:?}", event);
|
||||
if let Event::Workspace(event) = event? {
|
||||
let event = WorkspaceUpdate::from(*event);
|
||||
if !matches!(event, WorkspaceUpdate::Unknown) {
|
||||
workspace_tx.send(event)?;
|
||||
}
|
||||
if let Event::Workspace(ev) = event? {
|
||||
workspace_tx.send(WorkspaceUpdate::from(*ev))?;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,50 +105,22 @@ pub fn get_sub_client() -> &'static SwayEventClient {
|
||||
|
||||
impl From<Node> for Workspace {
|
||||
fn from(node: Node) -> Self {
|
||||
let visibility = Visibility::from(&node);
|
||||
|
||||
Self {
|
||||
id: node.id.to_string(),
|
||||
name: node.name.unwrap_or_default(),
|
||||
monitor: node.output.unwrap_or_default(),
|
||||
visibility,
|
||||
focused: node.focused,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<swayipc_async::Workspace> for Workspace {
|
||||
fn from(workspace: swayipc_async::Workspace) -> Self {
|
||||
let visibility = Visibility::from(&workspace);
|
||||
|
||||
Self {
|
||||
id: workspace.id.to_string(),
|
||||
name: workspace.name,
|
||||
monitor: workspace.output,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
focused: workspace.focused,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,13 +139,21 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
WorkspaceChange::Focus => Self::Focus {
|
||||
old: event.old.map(Workspace::from),
|
||||
new: Workspace::from(event.current.expect("Missing current workspace")),
|
||||
old: event
|
||||
.old
|
||||
.expect("Missing old workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
new: event
|
||||
.current
|
||||
.expect("Missing current workspace")
|
||||
.name
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
WorkspaceChange::Move => {
|
||||
Self::Move(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
_ => Self::Unknown,
|
||||
_ => Self::Update(event.current.expect("Missing current workspace").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,9 @@ pub mod mpd;
|
||||
#[cfg(feature = "music+mpris")]
|
||||
pub mod mpris;
|
||||
|
||||
pub const TICK_INTERVAL_MS: u64 = 200;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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),
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -35,25 +27,21 @@ pub struct Track {
|
||||
pub cover_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Status {
|
||||
pub state: PlayerState,
|
||||
pub volume_percent: Option<u8>,
|
||||
pub playlist_position: u32,
|
||||
pub playlist_length: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProgressTick {
|
||||
pub volume_percent: u8,
|
||||
pub duration: Option<Duration>,
|
||||
pub elapsed: Option<Duration>,
|
||||
pub playlist_position: u32,
|
||||
pub playlist_length: u32,
|
||||
}
|
||||
|
||||
pub trait MusicClient {
|
||||
@@ -63,7 +51,6 @@ pub trait MusicClient {
|
||||
fn prev(&self) -> Result<()>;
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||
fn seek(&self, duration: Duration) -> Result<()>;
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use super::{
|
||||
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
||||
};
|
||||
use crate::{await_sync, send, spawn};
|
||||
use super::{MusicClient, Status, Track};
|
||||
use crate::await_sync;
|
||||
use crate::clients::music::{PlayerState, PlayerUpdate};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||
use mpd_client::commands::SeekMode;
|
||||
use mpd_client::protocol::MpdProtocolError;
|
||||
use mpd_client::responses::{PlayState, Song};
|
||||
use mpd_client::tag::Tag;
|
||||
@@ -17,7 +15,8 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
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::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
@@ -30,8 +29,8 @@ lazy_static! {
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
music_dir: PathBuf,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -58,7 +57,7 @@ impl MpdClient {
|
||||
let (client, mut state_changes) =
|
||||
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();
|
||||
@@ -79,19 +78,7 @@ impl MpdClient {
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::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;
|
||||
}
|
||||
Ok::<(), SendError<(Option<Track>, Status)>>(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,9 +92,9 @@ impl MpdClient {
|
||||
|
||||
async fn send_update(
|
||||
client: &Client,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
tx: &Sender<PlayerUpdate>,
|
||||
music_dir: &Path,
|
||||
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
) -> Result<(), SendError<PlayerUpdate>> {
|
||||
let current_song = client.command(commands::CurrentSong).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 status = Status::from(status);
|
||||
|
||||
let update = PlayerUpdate::Update(Box::new(track), status);
|
||||
send!(tx, update);
|
||||
tx.send(PlayerUpdate::Update(Box::new(track), status))?;
|
||||
}
|
||||
|
||||
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 {
|
||||
!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");
|
||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||
Ok(())
|
||||
@@ -211,12 +182,7 @@ impl MusicClient for MpdClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
await_sync(async {
|
||||
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 {
|
||||
Self {
|
||||
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_length: status.playlist_length as u32,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
|
||||
use crate::clients::music::ProgressTick;
|
||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
||||
use super::{MusicClient, PlayerUpdate, Status, Track};
|
||||
use crate::clients::music::PlayerState;
|
||||
use crate::{lock, send};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::collections::HashSet;
|
||||
use std::string;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{cmp, string};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
lazy_static! {
|
||||
@@ -18,18 +19,18 @@ lazy_static! {
|
||||
|
||||
pub struct Client {
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
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 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, ¤t_player, &tx);
|
||||
sleep(Duration::from_millis(TICK_INTERVAL_MS));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
current_player,
|
||||
tx,
|
||||
@@ -108,7 +95,7 @@ impl Client {
|
||||
player_id: String,
|
||||
players: Arc<Mutex<HashSet<String>>>,
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
) {
|
||||
spawn_blocking(move || {
|
||||
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());
|
||||
|
||||
let metadata = player.get_metadata()?;
|
||||
@@ -161,7 +148,10 @@ impl Client {
|
||||
|
||||
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 {
|
||||
// MRPIS doesn't seem to provide playlist info reliably,
|
||||
@@ -169,6 +159,8 @@ impl Client {
|
||||
playlist_position: 1,
|
||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
||||
state: PlayerState::from(playback_status),
|
||||
elapsed: player.get_position().ok(),
|
||||
duration: metadata.length(),
|
||||
volume_percent,
|
||||
};
|
||||
|
||||
@@ -189,26 +181,6 @@ impl Client {
|
||||
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 {
|
||||
@@ -244,30 +216,14 @@ impl MusicClient for Client {
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
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 {
|
||||
error!("Could not find player");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
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> {
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
debug!("Creating new subscription");
|
||||
let rx = self.tx.subscribe();
|
||||
|
||||
@@ -280,7 +236,9 @@ impl MusicClient for Client {
|
||||
playlist_position: 0,
|
||||
playlist_length: 0,
|
||||
state: PlayerState::Stopped,
|
||||
volume_percent: None,
|
||||
elapsed: None,
|
||||
duration: None,
|
||||
volume_percent: 0,
|
||||
};
|
||||
send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
|
||||
}
|
||||
@@ -299,18 +257,9 @@ impl From<Metadata> for Track {
|
||||
const KEY_GENRE: &str = "xesam:genre";
|
||||
|
||||
Self {
|
||||
title: value
|
||||
.title()
|
||||
.map(std::string::ToString::to_string)
|
||||
.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),
|
||||
title: value.title().map(std::string::ToString::to_string),
|
||||
album: value.album_name().map(std::string::ToString::to_string),
|
||||
artist: value.artists().map(|artists| artists.join(", ")),
|
||||
date: value
|
||||
.get(KEY_DATE)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 color_eyre::Report;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use system_tray::message::menu::TrayMenu;
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::StatusNotifierWatcher;
|
||||
use stray::message::menu::TrayMenu;
|
||||
use stray::message::tray::StatusNotifierItem;
|
||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use stray::StatusNotifierWatcher;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
@@ -22,8 +24,8 @@ pub struct TrayEventReceiver {
|
||||
}
|
||||
|
||||
impl TrayEventReceiver {
|
||||
async fn new() -> system_tray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", Ironbar::unique_id());
|
||||
async fn new() -> stray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", get_unique_usize());
|
||||
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let (b_tx, b_rx) = broadcast::channel(16);
|
||||
@@ -31,7 +33,7 @@ impl TrayEventReceiver {
|
||||
let tray = StatusNotifierWatcher::new(rx).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();
|
||||
|
||||
@@ -3,29 +3,29 @@ use super::wlr_foreign_toplevel::manager::ToplevelManagerState;
|
||||
use super::wlr_foreign_toplevel::ToplevelEvent;
|
||||
use super::Environment;
|
||||
use crate::error::ERR_CHANNEL_RECV;
|
||||
use crate::{send, spawn_blocking};
|
||||
use crate::send;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Report;
|
||||
use smithay_client_toolkit::output::{OutputInfo, OutputState};
|
||||
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
|
||||
use smithay_client_toolkit::registry::RegistryState;
|
||||
use smithay_client_toolkit::seat::SeatState;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::globals::registry_queue_init;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::Connection;
|
||||
use wayland_client::{Connection, WaylandSource};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "clipboard")] {
|
||||
use super::ClipboardItem;
|
||||
use super::wlr_data_control::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ impl WaylandClient {
|
||||
let mut 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())
|
||||
.expect("Failed to insert Wayland event queue into event loop");
|
||||
|
||||
@@ -137,7 +138,7 @@ impl WaylandClient {
|
||||
seats: vec![],
|
||||
handles: HashMap::new(),
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: crate::arc_mut!(None),
|
||||
clipboard: Arc::new(Mutex::new(None)),
|
||||
toplevel_tx,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx,
|
||||
|
||||
@@ -6,7 +6,7 @@ mod wl_seat;
|
||||
mod wlr_foreign_toplevel;
|
||||
|
||||
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 lazy_static::lazy_static;
|
||||
use smithay_client_toolkit::output::OutputState;
|
||||
@@ -105,7 +105,7 @@ impl ProvidesRegistryState for Environment {
|
||||
}
|
||||
|
||||
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>> {
|
||||
|
||||
@@ -7,13 +7,14 @@ use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||
use self::source::DataControlSourceHandler;
|
||||
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 glib::Bytes;
|
||||
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::reexports::calloop::{PostAction, RegistrationToken};
|
||||
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
|
||||
use std::cmp::min;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
@@ -144,7 +145,7 @@ impl Environment {
|
||||
};
|
||||
|
||||
Ok(ClipboardItem {
|
||||
id: Ironbar::unique_id(),
|
||||
id: get_unique_usize(),
|
||||
value,
|
||||
mime_type: mime_type.value.clone(),
|
||||
})
|
||||
@@ -195,31 +196,29 @@ impl DataControlDeviceHandler for Environment {
|
||||
let tx = self.clipboard_tx.clone();
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
let token =
|
||||
self.loop_handle
|
||||
.insert_source(read_pipe, move |(), file, state| unsafe {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
let token = self
|
||||
.loop_handle
|
||||
.insert_source(read_pipe, move |_, file, state| {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
|
||||
match Self::read_file(&mime_type, file.get_mut()) {
|
||||
Ok(item) => {
|
||||
let item = Arc::new(item);
|
||||
lock!(clipboard).replace(item.clone());
|
||||
send!(tx, item);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
match Self::read_file(&mime_type, file) {
|
||||
Ok(item) => {
|
||||
let item = Arc::new(item);
|
||||
lock!(clipboard).replace(item.clone());
|
||||
send!(tx, item);
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
|
||||
PostAction::Remove
|
||||
});
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
});
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
@@ -240,7 +239,7 @@ impl DataControlOfferHandler for Environment {
|
||||
_offer: &mut DataControlDeviceOffer,
|
||||
_mime_type: String,
|
||||
) {
|
||||
trace!("Handler received offer");
|
||||
debug!("Handler received offer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,22 +289,23 @@ impl DataControlSourceHandler for Environment {
|
||||
trace!("Num bytes: {}", bytes.len());
|
||||
|
||||
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 =
|
||||
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
|
||||
epoll_fd
|
||||
.add(fd, epoll_event)
|
||||
.expect("to send valid epoll operation");
|
||||
let epoll_fd = epoll_create().expect("to get valid file descriptor");
|
||||
epoll_ctl(
|
||||
epoll_fd,
|
||||
EpollOp::EpollCtlAdd,
|
||||
fd.as_raw_fd(),
|
||||
&mut epoll_event,
|
||||
)
|
||||
.expect("to send valid epoll operation");
|
||||
|
||||
while !bytes.is_empty() {
|
||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||
|
||||
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
|
||||
|
||||
epoll_fd
|
||||
.wait(&mut events, 100)
|
||||
.expect("Failed to wait to epoll");
|
||||
epoll_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
|
||||
|
||||
match file.write(chunk) {
|
||||
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 {
|
||||
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 {
|
||||
trace!("Requesting pipe size increase to (at least): {size}");
|
||||
|
||||
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
|
||||
trace!("New pipe size: {res}");
|
||||
|
||||
if res < size as i32 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
size as i32
|
||||
|
||||
@@ -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::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::{BorrowedFd, FromRawFd};
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{trace, warn};
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
Event, ZwlrDataControlOfferV1,
|
||||
@@ -37,7 +37,7 @@ impl PartialEq for SelectionOffer {
|
||||
|
||||
impl SelectionOffer {
|
||||
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();
|
||||
|
||||
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());
|
||||
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
|
||||
/// could not be created.
|
||||
pub unsafe fn receive(
|
||||
offer: &ZwlrDataControlOfferV1,
|
||||
mime_type: String,
|
||||
) -> std::io::Result<ReadPipe> {
|
||||
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
|
||||
offer.receive(mime_type, BorrowedFd::borrow_raw(writefd));
|
||||
offer.receive(mime_type, writefd);
|
||||
|
||||
if let Err(err) = close(writefd) {
|
||||
warn!("Failed to close write pipe: {}", err);
|
||||
}
|
||||
|
||||
Ok(FromRawFd::from_raw_fd(readfd))
|
||||
Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::manager::ToplevelManagerState;
|
||||
use crate::{lock, Ironbar};
|
||||
use crate::lock;
|
||||
use crate::unique_id::get_unique_usize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::trace;
|
||||
@@ -67,7 +68,7 @@ pub struct ToplevelInfo {
|
||||
impl Default for ToplevelInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Ironbar::unique_id(),
|
||||
id: get_unique_usize(),
|
||||
app_id: String::new(),
|
||||
title: String::new(),
|
||||
fullscreen: false,
|
||||
|
||||
@@ -30,7 +30,7 @@ impl ToplevelManagerHandler for Environment {
|
||||
|
||||
impl ToplevelHandleHandler for Environment {
|
||||
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() {
|
||||
Some(info) => {
|
||||
@@ -50,7 +50,7 @@ impl ToplevelHandleHandler for Environment {
|
||||
_qh: &QueueHandle<Self>,
|
||||
handle: ToplevelHandle,
|
||||
) {
|
||||
trace!("Handler received handle update");
|
||||
debug!("Handler received handle update");
|
||||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
@@ -76,7 +75,7 @@ impl CommonConfig {
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
|
||||
@@ -94,7 +93,7 @@ impl CommonConfig {
|
||||
script.run_as_oneshot(None);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
macro_rules! install_oneshot {
|
||||
@@ -102,7 +101,7 @@ impl CommonConfig {
|
||||
$option.map(Script::new_polling).map(|script| {
|
||||
container.$method(move |_, _| {
|
||||
script.run_as_oneshot(None);
|
||||
Propagation::Proceed
|
||||
Inhibit(false)
|
||||
});
|
||||
})
|
||||
};
|
||||
@@ -115,6 +114,7 @@ impl CommonConfig {
|
||||
let container = container.clone();
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,7 @@ impl CommonConfig {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,17 +21,16 @@ use crate::modules::tray::TrayModule;
|
||||
use crate::modules::upower::UpowerModule;
|
||||
#[cfg(feature = "workspaces")]
|
||||
use crate::modules::workspaces::WorkspacesModule;
|
||||
use cfg_if::cfg_if;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use self::common::{CommonConfig, TransitionType};
|
||||
pub use self::truncate::TruncateMode;
|
||||
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ModuleConfig {
|
||||
#[cfg(feature = "clipboard")]
|
||||
#[cfg(feature = "clock")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
@@ -97,12 +96,6 @@ pub struct Config {
|
||||
pub margin: MarginConfig,
|
||||
#[serde(default = "default_popup_gap")]
|
||||
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.
|
||||
pub icon_theme: Option<String>,
|
||||
@@ -116,38 +109,6 @@ pub struct Config {
|
||||
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 {
|
||||
42
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
@@ -28,14 +29,6 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
||||
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
|
||||
if let Some(mut user_dir) = user_dir {
|
||||
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
|
||||
let files = find_desktop_files();
|
||||
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
if let Some(path) = find_desktop_file_by_filename(app_id, &files) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
find_desktop_file_by_filedata(app_id, &files)
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using a simple condition check
|
||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let with_names = files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f,
|
||||
f.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let app_id = app_id.to_lowercase();
|
||||
|
||||
with_names
|
||||
files
|
||||
.iter()
|
||||
// first pass - check for exact match
|
||||
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
|
||||
// second pass - check for substring
|
||||
.or_else(|| {
|
||||
with_names.iter().find(|(_, name)| {
|
||||
// this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
app_id
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part))
|
||||
})
|
||||
.find(|file| {
|
||||
let file_name: String = file
|
||||
.file_name()
|
||||
.expect("file name doesn't end with ...")
|
||||
.to_string_lossy()
|
||||
.to_lowercase();
|
||||
|
||||
file_name.contains(&app_id)
|
||||
|| app_id
|
||||
.split(&['-', ' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| file_name.contains(part)) // this will attempt to find flatpak apps that are like this
|
||||
// `com.company.app` or `com.app.something`
|
||||
})
|
||||
.map(|(file, _)| file.into())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
|
||||
@@ -106,92 +92,61 @@ fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<Path
|
||||
let app_id = &app_id.to_lowercase();
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
let files = files
|
||||
files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let Some(parsed_desktop_file) = parse_desktop_file(file) else {
|
||||
return None;
|
||||
};
|
||||
let Some(parsed_desktop_file) = parse_desktop_file(file) else { return None };
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = files
|
||||
.iter()
|
||||
// first pass - check name key for exact match
|
||||
.find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.map(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
||||
.unwrap_or_default()
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
// 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
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
});
|
||||
|
||||
file.map(|(path, _)| path).cloned()
|
||||
.map(|(path, _)| path)
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||
let Ok(file) = fs::read_to_string(path) else {
|
||||
let Ok(file) = File::open(path) else {
|
||||
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();
|
||||
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
return None;
|
||||
};
|
||||
let _ = lines.flatten().map(|line| {
|
||||
line.split_once('=')
|
||||
.iter()
|
||||
.filter_map(|(key, value)| {
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_default()
|
||||
.push(value.to_string());
|
||||
});
|
||||
if DESKTOP_FILES_LOOK_OUT_KEYS.contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(value.to_string());
|
||||
});
|
||||
});
|
||||
|
||||
Some(desktop_file)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
let Some(path) = find_desktop_file(app_id) else {
|
||||
return None;
|
||||
};
|
||||
let Some(path) = find_desktop_file(app_id) else { return None };
|
||||
|
||||
let mut desktop_files_cache = lock!(DESKTOP_FILES);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::script::Script;
|
||||
use crate::{glib_recv_mpsc, spawn, try_send};
|
||||
#[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 glib::Continue;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::spawn;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
@@ -17,9 +18,9 @@ pub enum DynamicBool {
|
||||
}
|
||||
|
||||
impl DynamicBool {
|
||||
pub fn subscribe<F>(self, mut f: F)
|
||||
pub fn subscribe<F>(self, f: F)
|
||||
where
|
||||
F: FnMut(bool) + 'static,
|
||||
F: FnMut(bool) -> Continue + 'static,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown(input) => {
|
||||
@@ -39,29 +40,29 @@ impl DynamicBool {
|
||||
_ => 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 {
|
||||
match value {
|
||||
DynamicBool::Script(script) => {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
try_send!(tx, success);
|
||||
send!(tx, success);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
DynamicBool::Variable(variable) => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let variable_manager = get_variable_manager();
|
||||
|
||||
let variable_name = variable[1..].into(); // remove hash
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||
send_async!(tx, has_value);
|
||||
send!(tx, has_value);
|
||||
}
|
||||
}
|
||||
DynamicBool::Unknown(_) => unreachable!(),
|
||||
@@ -70,10 +71,7 @@ impl DynamicBool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a string ironvar is 'truthy',
|
||||
/// i.e should be evaluated to true.
|
||||
///
|
||||
/// This loosely follows the common JavaScript cases.
|
||||
/// Check if a string ironvar is 'truthy'
|
||||
#[cfg(feature = "ipc")]
|
||||
fn is_truthy(string: &str) -> bool {
|
||||
!(string.is_empty() || string == "0" || string == "false")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::script::{OutputStream, Script};
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::Ironbar;
|
||||
use crate::{arc_mut, glib_recv_mpsc, lock, spawn, try_send};
|
||||
use tokio::sync::mpsc;
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{lock, send};
|
||||
use gtk::prelude::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
|
||||
/// A segment of a dynamic string,
|
||||
/// containing either a static string
|
||||
@@ -23,16 +25,17 @@ enum DynamicStringSegment {
|
||||
/// ```rs
|
||||
/// dynamic_string(&text, move |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
|
||||
F: FnMut(String) + 'static,
|
||||
F: FnMut(String) -> Continue + 'static,
|
||||
{
|
||||
let tokens = parse_input(input);
|
||||
|
||||
let label_parts = arc_mut!(vec![]);
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
for (i, segment) in tokens.into_iter().enumerate() {
|
||||
match segment {
|
||||
@@ -55,7 +58,7 @@ where
|
||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||
|
||||
let string = label_parts.join("");
|
||||
try_send!(tx, string);
|
||||
send!(tx, string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
@@ -70,7 +73,7 @@ where
|
||||
lock!(label_parts).push(String::new());
|
||||
|
||||
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);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
@@ -80,7 +83,7 @@ where
|
||||
let _: String = std::mem::replace(&mut label_parts[i], value);
|
||||
|
||||
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
|
||||
{
|
||||
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])
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_BRACKETS;
|
||||
let len = str.len() + SKIP_BRACKETS;
|
||||
let script = Script::from(str.as_str());
|
||||
|
||||
(DynamicStringSegment::Script(script), len)
|
||||
@@ -158,7 +161,7 @@ fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
.take_while(|&c| !c.is_whitespace())
|
||||
.collect::<String>();
|
||||
|
||||
let len = str.chars().count() + SKIP_HASH;
|
||||
let len = str.len() + SKIP_HASH;
|
||||
let value = str.into();
|
||||
|
||||
(DynamicStringSegment::Variable(value), len)
|
||||
@@ -171,16 +174,15 @@ fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||
.map(|w| w[0])
|
||||
.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 chars.len() == char_count + 1 {
|
||||
let remaining_char = *chars.get(char_count).expect("Failed to find last char");
|
||||
if chars.len() == str.len() + 1 {
|
||||
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||
str.push(remaining_char);
|
||||
char_count += 1;
|
||||
}
|
||||
|
||||
(DynamicStringSegment::Static(str), char_count)
|
||||
let len = str.len();
|
||||
|
||||
(DynamicStringSegment::Static(str), len)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
pub enum ExitCode {
|
||||
GtkDisplay = 1,
|
||||
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";
|
||||
|
||||
@@ -1,77 +1,8 @@
|
||||
use glib::IsA;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Orientation, Widget};
|
||||
use gtk::Widget;
|
||||
|
||||
/// Represents a widget's size
|
||||
/// and location relative to the bar's start edge.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
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) }
|
||||
}
|
||||
/// Adds a new CSS class to a widget.
|
||||
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
|
||||
widget.style_context().add_class(class);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use gtk::prelude::*;
|
||||
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) {
|
||||
let image = Image::new();
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
add_class(&image, "image");
|
||||
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()))
|
||||
{
|
||||
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) {
|
||||
let image = Image::new();
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
add_class(&image, "icon");
|
||||
add_class(&image, "image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
ImageProvider::parse(input, icon_theme, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::new(Some(input));
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
add_class(&label, "icon");
|
||||
add_class(&label, "text-icon");
|
||||
|
||||
container.add(&label);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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 color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
@@ -9,13 +7,13 @@ use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "http")]
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use crate::send;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
}
|
||||
);
|
||||
@@ -43,44 +41,23 @@ impl<'a> ImageProvider<'a> {
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
||||
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size)?;
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
fn get_location(
|
||||
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);
|
||||
|
||||
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
@@ -115,26 +92,21 @@ impl<'a> ImageProvider<'a> {
|
||||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
fallback!()
|
||||
None
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
||||
None if should_parse_desktop_file => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
||||
}) {
|
||||
None => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name)
|
||||
.map(|input| Self::get_location(&input, theme, size))
|
||||
{
|
||||
location
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
None
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,18 +117,18 @@ impl<'a> ImageProvider<'a> {
|
||||
#[cfg(feature = "http")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
send_async!(tx, bytes);
|
||||
send!(tx, bytes);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let size = self.size;
|
||||
glib_recv_mpsc!(rx, bytes => {
|
||||
rx.attach(None, move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
@@ -177,6 +149,8 @@ impl<'a> ImageProvider<'a> {
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Continue(false)
|
||||
});
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Command {
|
||||
/// Return "ok"
|
||||
Ping,
|
||||
@@ -12,15 +11,12 @@ pub enum Command {
|
||||
/// Open the GTK inspector
|
||||
Inspect,
|
||||
|
||||
/// Reload the config
|
||||
Reload,
|
||||
|
||||
/// Set an `ironvar` value.
|
||||
/// This creates it if it does not already exist, and updates it if it does.
|
||||
/// Any references to this variable are automatically and immediately updated.
|
||||
/// Keys and values can be any valid UTF-8 string.
|
||||
Set {
|
||||
/// Variable key. Can be any alphanumeric ASCII string.
|
||||
/// Variable key. Can be any valid UTF-8 string.
|
||||
key: Box<str>,
|
||||
/// Variable value. Can be any valid UTF-8 string.
|
||||
value: String,
|
||||
@@ -38,42 +34,4 @@ pub enum Command {
|
||||
/// The path to the sheet.
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod commands;
|
||||
pub mod responses;
|
||||
mod server;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
pub use commands::Command;
|
||||
@@ -30,8 +30,4 @@ impl Ipc {
|
||||
path: ipc_socket_file,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
OkValue { value: String },
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::Ipc;
|
||||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::ipc::{Command, Response};
|
||||
use crate::ironvar::get_variable_manager;
|
||||
use crate::style::load_css;
|
||||
use crate::{read_lock, send_async, try_send, write_lock};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use glib::Continue;
|
||||
use std::fs;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
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 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 {
|
||||
/// Starts the IPC server on its socket.
|
||||
///
|
||||
/// Once started, the server will begin accepting connections.
|
||||
pub fn start(&self, application: &Application, ironbar: Rc<Ironbar>) {
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(32);
|
||||
pub fn start(&self) {
|
||||
let bridge = BridgeChannel::<Command>::new();
|
||||
let cmd_tx = bridge.create_sender();
|
||||
let (res_tx, mut res_rx) = mpsc::channel(32);
|
||||
|
||||
let path = self.path.clone();
|
||||
@@ -30,7 +28,7 @@ impl Ipc {
|
||||
if path.exists() {
|
||||
warn!("Socket already exists. Did Ironbar exit abruptly?");
|
||||
warn!("Attempting IPC shutdown to allow binding to address");
|
||||
Self::shutdown(&path);
|
||||
self.shutdown();
|
||||
}
|
||||
|
||||
spawn(async move {
|
||||
@@ -63,10 +61,10 @@ impl Ipc {
|
||||
}
|
||||
});
|
||||
|
||||
let application = application.clone();
|
||||
glib_recv_mpsc!(cmd_rx, command => {
|
||||
let res = Self::handle_command(command, &application, &ironbar);
|
||||
bridge.recv(move |command| {
|
||||
let res = Self::handle_command(command);
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
Command::Inspect => {
|
||||
gtk::Window::set_interactive_debugging(true);
|
||||
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 } => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let variable_manager = get_variable_manager();
|
||||
let mut variable_manager = write_lock!(variable_manager);
|
||||
match variable_manager.set(key, value) {
|
||||
Ok(()) => Response::Ok,
|
||||
Ok(_) => Response::Ok,
|
||||
Err(err) => Response::error(&format!("{err}")),
|
||||
}
|
||||
}
|
||||
Command::Get { key } => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let variable_manager = get_variable_manager();
|
||||
let value = read_lock!(variable_manager).get(&key);
|
||||
match value {
|
||||
Some(value) => Response::OkValue { value },
|
||||
@@ -145,124 +132,13 @@ impl Ipc {
|
||||
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::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,
|
||||
/// removing the socket file in the process.
|
||||
///
|
||||
/// Note this is static as the `Ipc` struct is not `Send`.
|
||||
pub fn shutdown<P: AsRef<Path>>(path: P) {
|
||||
fs::remove_file(&path).ok();
|
||||
pub fn shutdown(&self) {
|
||||
fs::remove_file(&self.path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
#![doc = include_str!("../docs/Ironvars.md")]
|
||||
|
||||
use crate::send;
|
||||
use crate::{arc_rw, send};
|
||||
use color_eyre::{Report, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
lazy_static! {
|
||||
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
|
||||
}
|
||||
|
||||
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
|
||||
VARIABLE_MANAGER.clone()
|
||||
}
|
||||
|
||||
/// Global singleton manager for `IronVar` variables.
|
||||
pub struct VariableManager {
|
||||
variables: HashMap<Box<str>, IronVar>,
|
||||
}
|
||||
|
||||
impl Default for VariableManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl VariableManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -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`.
|
||||
/// Panics if the `Mutex` cannot be locked.
|
||||
///
|
||||
@@ -153,7 +100,7 @@ macro_rules! write_lock {
|
||||
#[macro_export]
|
||||
macro_rules! arc_mut {
|
||||
($val:expr) => {
|
||||
std::sync::Arc::new(std::sync::Mutex::new($val))
|
||||
std::sync::Arc::new(std::Sync::Mutex::new($val))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
431
src/main.rs
431
src/main.rs
@@ -1,41 +1,7 @@
|
||||
#![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 bridge_channel;
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli;
|
||||
mod clients;
|
||||
@@ -55,16 +21,45 @@ mod modules;
|
||||
mod popup;
|
||||
mod script;
|
||||
mod style;
|
||||
mod unique_id;
|
||||
|
||||
use crate::bar::create_bar;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::style::load_css;
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use dirs::config_dir;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use std::cell::Cell;
|
||||
use std::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::{block_in_place, 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 VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let _guard = logging::install_logging();
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "cli")] {
|
||||
run_with_args();
|
||||
run_with_args().await;
|
||||
} else {
|
||||
start_ironbar();
|
||||
}
|
||||
@@ -72,212 +67,128 @@ fn main() {
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
fn run_with_args() {
|
||||
async fn run_with_args() {
|
||||
let args = cli::Args::parse();
|
||||
|
||||
match args.command {
|
||||
Some(command) => {
|
||||
let rt = create_runtime();
|
||||
rt.block_on(async move {
|
||||
let ipc = ipc::Ipc::new();
|
||||
match ipc.send(command).await {
|
||||
Ok(res) => cli::handle_response(res),
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
});
|
||||
let ipc = ipc::Ipc::new();
|
||||
match ipc.send(command).await {
|
||||
Ok(res) => cli::handle_response(res),
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
}
|
||||
None => start_ironbar(),
|
||||
}
|
||||
}
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
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!("Starting application");
|
||||
|
||||
let app = Application::builder().application_id(GTK_APP_ID).build();
|
||||
|
||||
let running = AtomicBool::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| {
|
||||
if running.load(Ordering::Relaxed) {
|
||||
info!("Ironbar already running, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
running.set(true);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
let ipc = ipc::Ipc::new();
|
||||
ipc.start(app, instance.clone());
|
||||
}
|
||||
}
|
||||
|
||||
*instance.bars.borrow_mut() = load_interface(app);
|
||||
|
||||
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
||||
|| {
|
||||
config_dir().map_or_else(
|
||||
|| {
|
||||
let report = Report::msg("Failed to locate user config dir");
|
||||
error!("{:?}", report);
|
||||
exit(ExitCode::CreateBars as i32);
|
||||
},
|
||||
|dir| dir.join("ironbar").join("style.css"),
|
||||
)
|
||||
},
|
||||
PathBuf::from,
|
||||
);
|
||||
|
||||
if style_path.exists() {
|
||||
load_css(style_path);
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
let ipc_path = ipc.path().to_path_buf();
|
||||
spawn_blocking(move || {
|
||||
rx.recv().expect("to receive from channel");
|
||||
|
||||
info!("Shutting down");
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
ipc::Ipc::shutdown(ipc_path);
|
||||
|
||||
exit(0);
|
||||
});
|
||||
|
||||
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
|
||||
.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
|
||||
// Some are provided by swaybar_config but not currently supported
|
||||
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();
|
||||
}
|
||||
info!("Ironbar version {}", VERSION);
|
||||
info!("Starting application");
|
||||
|
||||
/// 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 app = Application::builder().application_id(GTK_APP_ID).build();
|
||||
let _ = wayland::get_client(); // force-init
|
||||
|
||||
let mut config = env::var("IRONBAR_CONFIG")
|
||||
.map_or_else(
|
||||
let running = Rc::new(Cell::new(false));
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
if running.get() {
|
||||
info!("Ironbar already running, returning");
|
||||
return;
|
||||
}
|
||||
|
||||
running.set(true);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
let ipc = ipc::Ipc::new();
|
||||
ipc.start();
|
||||
}
|
||||
}
|
||||
|
||||
let display = Display::default().map_or_else(
|
||||
|| {
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
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,
|
||||
)
|
||||
.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()
|
||||
});
|
||||
let mut config: Config = match config_res {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
exit(ExitCode::Config as i32)
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Loaded config file");
|
||||
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}'");
|
||||
#[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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match create_bars(app, &display, &config) {
|
||||
Ok(bars) => {
|
||||
debug!("Created {} bars", bars.len());
|
||||
bars
|
||||
}
|
||||
Err(err) => {
|
||||
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(
|
||||
|| {
|
||||
config_dir().map_or_else(
|
||||
|| {
|
||||
let report = Report::msg("Failed to locate user config dir");
|
||||
error!("{:?}", report);
|
||||
exit(ExitCode::CreateBars as i32);
|
||||
},
|
||||
|dir| dir.join("ironbar").join("style.css"),
|
||||
)
|
||||
},
|
||||
PathBuf::from,
|
||||
);
|
||||
|
||||
if style_path.exists() {
|
||||
load_css(style_path);
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
spawn_blocking(move || {
|
||||
rx.recv().expect("to receive from channel");
|
||||
|
||||
info!("Shutting down");
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
ipc.shutdown();
|
||||
|
||||
exit(0);
|
||||
});
|
||||
|
||||
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
});
|
||||
|
||||
// Ignore CLI args
|
||||
// Some are provided by swaybar_config but not currently supported
|
||||
app.run_with_args(&Vec::<&str>::new());
|
||||
}
|
||||
|
||||
/// 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 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 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 {
|
||||
let monitor = display
|
||||
.monitor(i)
|
||||
@@ -298,65 +205,35 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
|
||||
.get(i as usize)
|
||||
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
|
||||
|
||||
let Some(monitor_name) = &output.name else {
|
||||
continue;
|
||||
};
|
||||
let Some(monitor_name) = &output.name else { continue };
|
||||
|
||||
let mut bars = match config
|
||||
.monitors
|
||||
.as_ref()
|
||||
.and_then(|config| config.get(monitor_name))
|
||||
{
|
||||
Some(MonitorConfig::Single(config)) => {
|
||||
vec![create_bar(
|
||||
app,
|
||||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
config.clone(),
|
||||
)?]
|
||||
}
|
||||
Some(MonitorConfig::Multiple(configs)) => configs
|
||||
.iter()
|
||||
.map(|config| create_bar(app, &monitor, monitor_name.to_string(), config.clone()))
|
||||
.collect::<Result<_>>()?,
|
||||
None if show_default_bar => vec![create_bar(
|
||||
app,
|
||||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
config.clone(),
|
||||
)?],
|
||||
None => vec![],
|
||||
};
|
||||
config.monitors.as_ref().map_or_else(
|
||||
|| {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())
|
||||
},
|
||||
|config| {
|
||||
let config = config.get(monitor_name);
|
||||
match &config {
|
||||
Some(MonitorConfig::Single(config)) => {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())
|
||||
}
|
||||
Some(MonitorConfig::Multiple(configs)) => {
|
||||
for config in configs {
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
create_bar(app, &monitor, monitor_name, config.clone())?;
|
||||
}
|
||||
|
||||
all_bars.append(&mut bars);
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(all_bars)
|
||||
}
|
||||
|
||||
fn create_runtime() -> Runtime {
|
||||
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)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Blocks on a `Future` until it resolves.
|
||||
@@ -370,5 +247,5 @@ where
|
||||
///
|
||||
/// TODO: remove all instances of this once async trait funcs are stable
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ use crate::clients::clipboard::{self, ClipboardEvent};
|
||||
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
||||
};
|
||||
use crate::{glib_recv, spawn, try_send};
|
||||
use glib::Propagation;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::try_send;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use gtk::prelude::*;
|
||||
@@ -14,7 +12,8 @@ use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -72,8 +71,8 @@ impl Module<Button> for ClipboardModule {
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> color_eyre::Result<()> {
|
||||
let max_items = self.max_items;
|
||||
|
||||
@@ -125,27 +124,31 @@ impl Module<Button> for ClipboardModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
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);
|
||||
button.style_context().add_class("btn");
|
||||
|
||||
let tx = context.tx.clone();
|
||||
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();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
// we need to bind to the receiver as the channel does not open
|
||||
// until the popup is first opened.
|
||||
context.widget_rx.attach(None, |_| Continue(true));
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
@@ -163,7 +166,7 @@ impl Module<Button> for ClipboardModule {
|
||||
|
||||
{
|
||||
let hidden_option = hidden_option.clone();
|
||||
glib_recv!(rx, event => {
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
ControllerEvent::Add(id, item) => {
|
||||
debug!("Adding new value with ID {}", id);
|
||||
@@ -229,7 +232,7 @@ impl Module<Button> for ClipboardModule {
|
||||
try_send!(tx, UIEvent::Copy(id));
|
||||
}
|
||||
|
||||
Propagation::Stop
|
||||
Inhibit(true)
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -288,6 +291,8 @@ impl Module<Button> for ClipboardModule {
|
||||
hidden_option.set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use std::env;
|
||||
|
||||
use chrono::{DateTime, Local, Locale};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
use crate::{send_async, try_send};
|
||||
use chrono::{DateTime, Local};
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, Calendar, Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
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)]
|
||||
pub struct ClockModule {
|
||||
/// Date/time format string.
|
||||
@@ -25,48 +23,14 @@ pub struct ClockModule {
|
||||
#[serde(default = "default_format")]
|
||||
format: String,
|
||||
|
||||
#[serde(default = "default_popup_format")]
|
||||
format_popup: String,
|
||||
|
||||
#[serde(default = "default_locale")]
|
||||
locale: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
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 {
|
||||
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 {
|
||||
type SendMessage = DateTime<Local>;
|
||||
type ReceiveMessage = ();
|
||||
@@ -96,57 +60,62 @@ impl Module<Button> for ClockModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
let button = Button::new();
|
||||
let label = Label::new(None);
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
button.add(&label);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
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 locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
{
|
||||
context.widget_rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(&format));
|
||||
label.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, date => {
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
label.set_label(&date_string);
|
||||
});
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
let popup = self
|
||||
.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(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
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);
|
||||
|
||||
let calendar = Calendar::new();
|
||||
calendar.add_class("calendar");
|
||||
add_class(&calendar, "calendar");
|
||||
container.add(&calendar);
|
||||
|
||||
let format = self.format_popup;
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
glib_recv!(rx, date => {
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
clock.set_label(&date_string);
|
||||
});
|
||||
{
|
||||
rx.attach(None, move |date| {
|
||||
let date_string = format!("{}", date.format(format));
|
||||
clock.set_label(&date_string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
for widget in widgets {
|
||||
widget.widget.add_to(&container, &context, widget.common);
|
||||
widget.widget.add_to(&container, context, widget.common);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::{Button, Label};
|
||||
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)]
|
||||
pub struct ButtonWidget {
|
||||
name: Option<String>,
|
||||
@@ -21,7 +19,6 @@ impl CustomWidget for ButtonWidget {
|
||||
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let button = build!(self, Self::Widget);
|
||||
context.popup_buttons.borrow_mut().push(button.clone());
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let label = Label::new(None);
|
||||
@@ -30,10 +27,12 @@ impl CustomWidget for ButtonWidget {
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(exec) = self.on_click {
|
||||
let bar_orientation = context.bar_orientation;
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
@@ -42,7 +41,7 @@ impl CustomWidget for ButtonWidget {
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
args: None,
|
||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
||||
geometry: Popup::widget_geometry(button, bar_orientation),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::Image;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::image::ImageProvider;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Image;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ImageWidget {
|
||||
@@ -32,8 +30,10 @@ impl CustomWidget for ImageWidget {
|
||||
let icon_theme = context.icon_theme.clone();
|
||||
|
||||
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()));
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LabelWidget {
|
||||
name: Option<String>,
|
||||
@@ -26,6 +24,7 @@ impl CustomWidget for LabelWidget {
|
||||
let label = label.clone();
|
||||
dynamic_string(&self.label, move |string| {
|
||||
label.set_markup(&string);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,17 @@ use crate::config::CommonConfig;
|
||||
use crate::modules::custom::button::ButtonWidget;
|
||||
use crate::modules::custom::progress::ProgressWidget;
|
||||
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::{send_async, spawn};
|
||||
use crate::send_async;
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use gtk::{IconTheme, Orientation};
|
||||
use serde::Deserialize;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -56,12 +56,11 @@ pub enum Widget {
|
||||
Progress(ProgressWidget),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct CustomWidgetContext<'a> {
|
||||
tx: &'a mpsc::Sender<ExecEvent>,
|
||||
tx: &'a Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &'a IconTheme,
|
||||
popup_buttons: Rc<RefCell<Vec<Button>>>,
|
||||
}
|
||||
|
||||
trait CustomWidget {
|
||||
@@ -116,11 +115,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(self, parent: >k::Box, context: &CustomWidgetContext, common: CommonConfig) {
|
||||
fn add_to(self, parent: >k::Box, context: CustomWidgetContext, common: CommonConfig) {
|
||||
macro_rules! create {
|
||||
($widget:expr) => {
|
||||
wrap_widget(
|
||||
&$widget.into_widget(context.clone()),
|
||||
&$widget.into_widget(context),
|
||||
common,
|
||||
context.bar_orientation,
|
||||
)
|
||||
@@ -144,7 +143,7 @@ impl Widget {
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
args: Option<Vec<String>>,
|
||||
id: usize,
|
||||
geometry: WidgetGeometry,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
@@ -158,8 +157,8 @@ impl Module<gtk::Box> for CustomModule {
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
@@ -174,9 +173,9 @@ impl Module<gtk::Box> for CustomModule {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} 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" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
} else {
|
||||
@@ -192,30 +191,25 @@ impl Module<gtk::Box> for CustomModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let container = gtk::Box::builder().orientation(orientation).build();
|
||||
|
||||
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let custom_context = CustomWidgetContext {
|
||||
tx: &context.controller_tx,
|
||||
bar_orientation: orientation,
|
||||
icon_theme: info.icon_theme,
|
||||
popup_buttons: popup_buttons.clone(),
|
||||
};
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
widget
|
||||
.widget
|
||||
.add_to(&container, &custom_context, widget.common);
|
||||
.add_to(&container, custom_context, widget.common);
|
||||
});
|
||||
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
|
||||
.into_popup_parts_owned(popup_buttons.take());
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
Ok(ModuleParts {
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
@@ -223,8 +217,8 @@ impl Module<gtk::Box> for CustomModule {
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
@@ -237,13 +231,12 @@ impl Module<gtk::Box> for CustomModule {
|
||||
tx: &tx,
|
||||
bar_orientation: info.bar_position.get_orientation(),
|
||||
icon_theme: info.icon_theme,
|
||||
popup_buttons: Rc::new(RefCell::new(vec![])),
|
||||
};
|
||||
|
||||
for widget in popup {
|
||||
widget
|
||||
.widget
|
||||
.add_to(&container, &custom_context, widget.common);
|
||||
.add_to(&container, custom_context, widget.common);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::ProgressBar;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
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};
|
||||
use crate::{build, send};
|
||||
use gtk::prelude::*;
|
||||
use gtk::ProgressBar;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ProgressWidget {
|
||||
@@ -47,13 +45,13 @@ impl CustomWidget for ProgressWidget {
|
||||
let script = Script::from(value);
|
||||
let progress = progress.clone();
|
||||
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
||||
Ok(value) => try_send!(tx, value),
|
||||
Ok(value) => send!(tx, value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
@@ -61,7 +59,10 @@ impl CustomWidget for ProgressWidget {
|
||||
.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 {
|
||||
@@ -70,6 +71,7 @@ impl CustomWidget for ProgressWidget {
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
progress.set_text(Some(&string));
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use glib::Propagation;
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
|
||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::popup::Popup;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, send, try_send};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Scale;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
use tokio::spawn;
|
||||
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)]
|
||||
pub struct SliderWidget {
|
||||
name: Option<String>,
|
||||
@@ -78,10 +75,10 @@ impl CustomWidget for SliderWidget {
|
||||
};
|
||||
|
||||
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
|
||||
let val = val.clamp(min, max);
|
||||
|
||||
@@ -91,14 +88,14 @@ impl CustomWidget for SliderWidget {
|
||||
ExecEvent {
|
||||
cmd: on_change.clone(),
|
||||
args: Some(vec![val.to_string()]),
|
||||
id: usize::MAX // ignored
|
||||
geometry: Popup::widget_geometry(scale, context.bar_orientation),
|
||||
}
|
||||
);
|
||||
|
||||
prev_value.set(val);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,13 +103,13 @@ impl CustomWidget for SliderWidget {
|
||||
let script = Script::from(value);
|
||||
let scale = scale.clone();
|
||||
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
spawn(async move {
|
||||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse() {
|
||||
Ok(value) => try_send!(tx, value),
|
||||
Ok(value) => send!(tx, value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
@@ -120,7 +117,10 @@ impl CustomWidget for SliderWidget {
|
||||
.await;
|
||||
});
|
||||
|
||||
glib_recv_mpsc!(rx, value => scale.set_value(value));
|
||||
rx.attach(None, move |value| {
|
||||
scale.set_value(value);
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
scale
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, lock, send_async, spawn, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{lock, send_async, try_send};
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::debug;
|
||||
|
||||
@@ -30,24 +32,12 @@ pub struct FocusedModule {
|
||||
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 {
|
||||
32
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for FocusedModule {
|
||||
type SendMessage = Option<(String, String)>;
|
||||
type SendMessage = (String, String);
|
||||
type ReceiveMessage = ();
|
||||
|
||||
fn name() -> &'static str {
|
||||
@@ -76,36 +66,21 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
if let Some(focused) = focused {
|
||||
try_send!(
|
||||
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 {
|
||||
match event {
|
||||
ToplevelEvent::Update(handle) => {
|
||||
let info = handle.info().unwrap_or_default();
|
||||
if let ToplevelEvent::Update(handle) = event {
|
||||
let info = handle.info().unwrap_or_default();
|
||||
|
||||
if info.focused {
|
||||
debug!("Changing focus");
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Some((
|
||||
info.title.clone(),
|
||||
info.app_id.clone()
|
||||
)))
|
||||
);
|
||||
} else {
|
||||
send_async!(tx, ModuleUpdateEvent::Update(None));
|
||||
}
|
||||
if info.focused {
|
||||
debug!("Changing focus");
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update((info.title.clone(), info.app_id.clone()))
|
||||
);
|
||||
}
|
||||
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,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
|
||||
|
||||
let icon = gtk::Image::new();
|
||||
if self.show_icon {
|
||||
icon.add_class("icon");
|
||||
add_class(&icon, "icon");
|
||||
container.add(&icon);
|
||||
}
|
||||
|
||||
let label = Label::new(None);
|
||||
label.add_class("label");
|
||||
add_class(&label, "label");
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
@@ -139,29 +114,25 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
|
||||
{
|
||||
let icon_theme = icon_theme.clone();
|
||||
glib_recv!(context.subscribe(), data => {
|
||||
if let Some((name, id)) = data {
|
||||
if self.show_icon {
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
Some(Ok(())) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
context.widget_rx.attach(None, move |(name, id)| {
|
||||
if self.show_icon {
|
||||
match ImageProvider::parse(&id, &icon_theme, self.icon_size)
|
||||
.map(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
Some(Ok(_)) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
|
||||
if self.show_title {
|
||||
label.show();
|
||||
label.set_label(&name);
|
||||
}
|
||||
} else {
|
||||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
|
||||
if self.show_title {
|
||||
label.set_label(&name);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::try_send;
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
@@ -16,15 +17,6 @@ pub struct LabelModule {
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
impl LabelModule {
|
||||
pub(crate) fn new(label: String) -> Self {
|
||||
Self {
|
||||
label,
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<Label> for LabelModule {
|
||||
type SendMessage = String;
|
||||
type ReceiveMessage = ();
|
||||
@@ -41,6 +33,7 @@ impl Module<Label> for LabelModule {
|
||||
) -> Result<()> {
|
||||
dynamic_string(&self.label, move |string| {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -50,16 +43,18 @@ impl Module<Label> for LabelModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
) -> Result<ModuleWidget<Label>> {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
|
||||
{
|
||||
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,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use super::open_state::OpenState;
|
||||
use crate::clients::wayland::ToplevelHandle;
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::popup::Popup;
|
||||
use crate::{read_lock, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use glib::Propagation;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme};
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
@@ -178,7 +176,7 @@ impl ItemButton {
|
||||
item: &Item,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
bar_position: BarPosition,
|
||||
orientation: Orientation,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
) -> Self {
|
||||
@@ -193,7 +191,7 @@ impl ItemButton {
|
||||
if appearance.show_icons {
|
||||
let gtk_image = gtk::Image::new();
|
||||
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 {
|
||||
button.set_image(Some(>k_image));
|
||||
button.set_always_show_image(true);
|
||||
@@ -251,40 +249,13 @@ impl ItemButton {
|
||||
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::OpenPopupAt(
|
||||
button.geometry(bar_position.get_orientation())
|
||||
)
|
||||
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
|
||||
);
|
||||
} else {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
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
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@ use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::modules::launcher::item::AppearanceOptions;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{arc_mut, glib_recv, lock, send_async, spawn, try_send, write_lock};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{lock, send_async, try_send, write_lock};
|
||||
use color_eyre::{Help, Report};
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -90,8 +90,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> crate::Result<()> {
|
||||
let items = self
|
||||
.favorites
|
||||
@@ -108,7 +108,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
.collect::<IndexMap<_, _>>()
|
||||
});
|
||||
|
||||
let items = arc_mut!(items);
|
||||
let items = Arc::new(Mutex::new(items));
|
||||
|
||||
let items2 = Arc::clone(&items);
|
||||
let tx2 = tx.clone();
|
||||
@@ -163,7 +163,6 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
match item {
|
||||
None => {
|
||||
let item: Item = handle.try_into()?;
|
||||
|
||||
items.insert(info.app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
@@ -314,7 +313,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||
) -> crate::Result<ModuleWidget<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
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 bar_position = info.bar_position;
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, event => {
|
||||
context.widget_rx.attach(None, move |event| {
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
debug!("Adding item with id {}", item.app_id);
|
||||
@@ -350,8 +347,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
&item,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
bar_position,
|
||||
&tx,
|
||||
orientation,
|
||||
&context.tx,
|
||||
&controller_tx,
|
||||
);
|
||||
|
||||
@@ -359,10 +356,9 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
buttons.insert(item.app_id, button);
|
||||
}
|
||||
}
|
||||
LauncherUpdate::AddWindow(app_id, win) => {
|
||||
LauncherUpdate::AddWindow(app_id, _) => {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_open(true);
|
||||
button.set_focused(win.open_state.is_focused());
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows += 1;
|
||||
@@ -383,12 +379,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||
debug!("Removing window {win_id} with id {app_id}");
|
||||
|
||||
LauncherUpdate::RemoveWindow(app_id, _) => {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(false);
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows -= 1;
|
||||
}
|
||||
@@ -411,15 +403,13 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
LauncherUpdate::Hover(_) => {}
|
||||
};
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
let rx = context.subscribe();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, rx, info)
|
||||
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
|
||||
|
||||
Ok(ModuleParts {
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
@@ -427,8 +417,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
controller_tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
const MAX_WIDTH: i32 = 250;
|
||||
@@ -444,7 +434,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
glib_recv!(rx, event => {
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
let app_id = item.app_id.clone();
|
||||
@@ -531,6 +521,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
pub mod clipboard;
|
||||
/// Displays the current date and time.
|
||||
@@ -41,6 +24,19 @@ pub mod upower;
|
||||
#[cfg(feature = "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)]
|
||||
pub enum ModuleLocation {
|
||||
Left,
|
||||
@@ -56,98 +52,30 @@ pub struct ModuleInfo<'a> {
|
||||
pub icon_theme: &'a IconTheme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ModuleUpdateEvent<T: Clone> {
|
||||
/// Sends an update to the module UI.
|
||||
#[derive(Debug)]
|
||||
pub enum ModuleUpdateEvent<T> {
|
||||
/// Sends an update to the module UI
|
||||
Update(T),
|
||||
/// Toggles the open state of the popup.
|
||||
/// Takes the button ID.
|
||||
TogglePopup(usize),
|
||||
TogglePopup(WidgetGeometry),
|
||||
/// Force sets the popup open.
|
||||
/// Takes the button ID.
|
||||
OpenPopup(usize),
|
||||
OpenPopupAt(WidgetGeometry),
|
||||
/// Takes the button X position and width.
|
||||
OpenPopup(WidgetGeometry),
|
||||
/// Force sets the popup closed.
|
||||
ClosePopup,
|
||||
}
|
||||
|
||||
pub struct WidgetContext<TSend, TReceive>
|
||||
where
|
||||
TSend: Clone,
|
||||
{
|
||||
pub struct WidgetContext<TSend, TReceive> {
|
||||
pub id: usize,
|
||||
pub tx: mpsc::Sender<ModuleUpdateEvent<TSend>>,
|
||||
pub update_tx: broadcast::Sender<TSend>,
|
||||
pub controller_tx: mpsc::Sender<TReceive>,
|
||||
|
||||
_update_rx: broadcast::Receiver<TSend>,
|
||||
pub widget_rx: glib::Receiver<TSend>,
|
||||
pub popup_rx: glib::Receiver<TSend>,
|
||||
}
|
||||
|
||||
impl<TSend, TReceive> WidgetContext<TSend, TReceive>
|
||||
where
|
||||
TSend: Clone,
|
||||
{
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<TSend> {
|
||||
self.update_tx.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModuleParts<W: IsA<Widget>> {
|
||||
pub struct ModuleWidget<W: IsA<Widget>> {
|
||||
pub widget: W,
|
||||
pub popup: Option<ModulePopupParts>,
|
||||
}
|
||||
|
||||
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 popup: Option<gtk::Box>,
|
||||
}
|
||||
|
||||
pub trait Module<W>
|
||||
@@ -164,22 +92,18 @@ where
|
||||
info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()>
|
||||
where
|
||||
<Self as Module<W>>::SendMessage: Clone;
|
||||
) -> Result<()>;
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<W>>
|
||||
where
|
||||
<Self as Module<W>>::SendMessage: Clone;
|
||||
) -> Result<ModuleWidget<W>>;
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
@@ -194,59 +118,53 @@ where
|
||||
pub fn create_module<TModule, TWidget, TSend, TRec>(
|
||||
module: TModule,
|
||||
id: usize,
|
||||
name: Option<String>,
|
||||
info: &ModuleInfo,
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
) -> Result<ModuleParts<TWidget>>
|
||||
popup: &Arc<RwLock<Popup>>,
|
||||
) -> Result<ModuleWidget<TWidget>>
|
||||
where
|
||||
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
|
||||
TWidget: IsA<Widget>,
|
||||
TSend: Debug + Clone + Send + 'static,
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
let (ui_tx, ui_rx) = mpsc::channel::<ModuleUpdateEvent<TSend>>(64);
|
||||
let (controller_tx, controller_rx) = mpsc::channel::<TRec>(64);
|
||||
let (w_tx, w_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
let (p_tx, p_rx) = glib::MainContext::channel::<TSend>(glib::PRIORITY_DEFAULT);
|
||||
|
||||
let (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 {
|
||||
id,
|
||||
tx: ui_tx,
|
||||
update_tx: tx.clone(),
|
||||
controller_tx,
|
||||
_update_rx: rx,
|
||||
widget_rx: w_rx,
|
||||
popup_rx: p_rx,
|
||||
tx: channel.create_sender(),
|
||||
controller_tx: ui_tx,
|
||||
};
|
||||
|
||||
let module_name = TModule::name();
|
||||
let instance_name = name.unwrap_or_else(|| module_name.to_string());
|
||||
let name = TModule::name();
|
||||
|
||||
let module_parts = module.into_widget(context, info)?;
|
||||
module_parts.widget.add_class("widget");
|
||||
module_parts.widget.add_class(module_name);
|
||||
module_parts.widget.style_context().add_class(name);
|
||||
|
||||
let mut has_popup = false;
|
||||
if let Some(popup_content) = module_parts.popup.clone() {
|
||||
popup_content
|
||||
.container
|
||||
.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)
|
||||
}
|
||||
|
||||
/// Registers the popup content with the popup.
|
||||
fn register_popup_content(
|
||||
popup: &Rc<RefCell<Popup>>,
|
||||
id: usize,
|
||||
name: String,
|
||||
popup_content: ModulePopupParts,
|
||||
) {
|
||||
popup.borrow_mut().register_content(id, name, popup_content);
|
||||
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
|
||||
write_lock!(popup).register_content(id, popup_content);
|
||||
}
|
||||
|
||||
/// Sets up the bridge channel receiver
|
||||
@@ -255,83 +173,80 @@ fn register_popup_content(
|
||||
/// Handles opening/closing popups
|
||||
/// and communicating update messages between controllers and widgets/popups.
|
||||
fn setup_receiver<TSend>(
|
||||
tx: broadcast::Sender<TSend>,
|
||||
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
|
||||
popup: Rc<RefCell<Popup>>,
|
||||
channel: BridgeChannel<ModuleUpdateEvent<TSend>>,
|
||||
w_tx: glib::Sender<TSend>,
|
||||
p_tx: glib::Sender<TSend>,
|
||||
popup: Arc<RwLock<Popup>>,
|
||||
name: &'static str,
|
||||
id: usize,
|
||||
has_popup: bool,
|
||||
) where
|
||||
TSend: Debug + Clone + Send + 'static,
|
||||
TSend: Clone + Send + 'static,
|
||||
{
|
||||
// some rare cases can cause the popup to incorrectly calculate its size on first open.
|
||||
// we can fix that by just force re-rendering it on its first open.
|
||||
let mut has_popup_opened = false;
|
||||
|
||||
glib_recv_mpsc!(rx, ev => {
|
||||
channel.recv(move |ev| {
|
||||
match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
send!(tx, update);
|
||||
if has_popup {
|
||||
send!(p_tx, update.clone());
|
||||
}
|
||||
|
||||
send!(w_tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(button_id) => {
|
||||
ModuleUpdateEvent::TogglePopup(geometry) => {
|
||||
debug!("Toggling popup for {} [#{}]", name, id);
|
||||
let mut popup = popup.borrow_mut();
|
||||
let popup = read_lock!(popup);
|
||||
if popup.is_visible() {
|
||||
popup.hide();
|
||||
} 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 {
|
||||
popup.show(id, button_id);
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(button_id) => {
|
||||
ModuleUpdateEvent::OpenPopup(geometry) => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
let mut popup = popup.borrow_mut();
|
||||
let popup = read_lock!(popup);
|
||||
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 {
|
||||
popup.show(id, button_id);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
popup.show_content(id);
|
||||
popup.show(geometry);
|
||||
has_popup_opened = true;
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
|
||||
let mut popup = popup.borrow_mut();
|
||||
let popup = read_lock!(popup);
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
|
||||
widget_parts: &ModuleParts<TWidget>,
|
||||
widget_parts: &ModuleWidget<TWidget>,
|
||||
common: &CommonConfig,
|
||||
) {
|
||||
if let Some(ref name) = common.name {
|
||||
widget_parts.widget.set_widget_name(name);
|
||||
|
||||
if let Some(ref popup) = widget_parts.popup {
|
||||
popup.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 {
|
||||
for part in class.split(' ') {
|
||||
popup
|
||||
.container
|
||||
.style_context()
|
||||
.add_class(&format!("popup-{part}"));
|
||||
popup.style_context().add_class(&format!("popup-{part}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,8 +286,6 @@ pub fn wrap_widget<W: IsA<Widget>>(
|
||||
revealer.set_reveal_child(true);
|
||||
|
||||
let container = EventBox::new();
|
||||
container.add_class("widget-container");
|
||||
|
||||
container.add_events(EventMask::SCROLL_MASK);
|
||||
container.add(&revealer);
|
||||
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
mod config;
|
||||
|
||||
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 glib::{Propagation, PropertySet};
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||
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 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;
|
||||
use self::config::PlayerType;
|
||||
|
||||
mod config;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PlayerCommand {
|
||||
Previous,
|
||||
@@ -34,7 +28,6 @@ pub enum PlayerCommand {
|
||||
Pause,
|
||||
Next,
|
||||
Volume(u8),
|
||||
Seek(Duration),
|
||||
}
|
||||
|
||||
/// Formats a duration given in seconds
|
||||
@@ -54,12 +47,6 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ControllerEvent {
|
||||
Update(Option<SongUpdate>),
|
||||
UpdateProgress(ProgressTick),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SongUpdate {
|
||||
song: Track,
|
||||
@@ -80,7 +67,7 @@ async fn get_client(
|
||||
}
|
||||
|
||||
impl Module<Button> for MusicModule {
|
||||
type SendMessage = ControllerEvent;
|
||||
type SendMessage = Option<SongUpdate>;
|
||||
type ReceiveMessage = PlayerCommand;
|
||||
|
||||
fn name() -> &'static str {
|
||||
@@ -90,8 +77,8 @@ impl Module<Button> for MusicModule {
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let format = self.format.clone();
|
||||
|
||||
@@ -116,7 +103,7 @@ impl Module<Button> for MusicModule {
|
||||
PlayerUpdate::Update(track, status) => match *track {
|
||||
Some(track) => {
|
||||
let display_string =
|
||||
replace_tokens(format.as_str(), &tokens, &track);
|
||||
replace_tokens(format.as_str(), &tokens, &track, &status);
|
||||
|
||||
let update = SongUpdate {
|
||||
song: track,
|
||||
@@ -124,24 +111,10 @@ impl Module<Button> for MusicModule {
|
||||
display_string,
|
||||
};
|
||||
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
|
||||
update
|
||||
)))
|
||||
);
|
||||
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
|
||||
}
|
||||
None => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
|
||||
),
|
||||
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
|
||||
},
|
||||
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
|
||||
progress_tick
|
||||
))
|
||||
),
|
||||
PlayerUpdate::Disconnect => break,
|
||||
}
|
||||
}
|
||||
@@ -164,7 +137,6 @@ impl Module<Button> for MusicModule {
|
||||
PlayerCommand::Pause => client.pause(),
|
||||
PlayerCommand::Next => client.next(),
|
||||
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
|
||||
PlayerCommand::Seek(duration) => client.seek(duration),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
@@ -181,10 +153,10 @@ impl Module<Button> for MusicModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
let button = Button::new();
|
||||
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
button_contents.add_class("contents");
|
||||
add_class(&button_contents, "contents");
|
||||
|
||||
button.add(&button_contents);
|
||||
|
||||
@@ -202,25 +174,24 @@ impl Module<Button> for MusicModule {
|
||||
button_contents.add(&icon_play);
|
||||
button_contents.add(&label);
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
{
|
||||
let tx = context.tx.clone();
|
||||
|
||||
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 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() {
|
||||
label.set_label(&event.display_string);
|
||||
|
||||
@@ -249,33 +220,34 @@ impl Module<Button> for MusicModule {
|
||||
button.hide();
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
};
|
||||
|
||||
let rx = context.subscribe();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx, rx, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
|
||||
let album_image = gtk::Image::builder()
|
||||
.width_request(128)
|
||||
.height_request(128)
|
||||
.build();
|
||||
album_image.add_class("album-art");
|
||||
add_class(&album_image, "album-art");
|
||||
|
||||
let icons = self.icons;
|
||||
|
||||
@@ -284,28 +256,28 @@ impl Module<Button> for MusicModule {
|
||||
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
||||
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
||||
|
||||
title_label.container.add_class("title");
|
||||
album_label.container.add_class("album");
|
||||
artist_label.container.add_class("artist");
|
||||
add_class(&title_label.container, "title");
|
||||
add_class(&album_label.container, "album");
|
||||
add_class(&artist_label.container, "artist");
|
||||
|
||||
info_box.add(&title_label.container);
|
||||
info_box.add(&album_label.container);
|
||||
info_box.add(&artist_label.container);
|
||||
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
btn_next.add_class("btn-next");
|
||||
add_class(&btn_next, "btn-next");
|
||||
|
||||
controls_box.add(&btn_prev);
|
||||
controls_box.add(&btn_play);
|
||||
@@ -315,22 +287,21 @@ impl Module<Button> for MusicModule {
|
||||
info_box.add(&controls_box);
|
||||
|
||||
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);
|
||||
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);
|
||||
volume_icon.add_class("icon");
|
||||
add_class(&volume_icon, "icon");
|
||||
|
||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||
|
||||
main_container.add(&album_image);
|
||||
main_container.add(&info_box);
|
||||
main_container.add(&volume_box);
|
||||
container.add(&main_container);
|
||||
container.add(&album_image);
|
||||
container.add(&info_box);
|
||||
container.add(&volume_box);
|
||||
|
||||
let tx_prev = tx.clone();
|
||||
btn_prev.connect_clicked(move |_| {
|
||||
@@ -352,49 +323,12 @@ impl Module<Button> for MusicModule {
|
||||
try_send!(tx_next, PlayerCommand::Next);
|
||||
});
|
||||
|
||||
let tx_vol = tx.clone();
|
||||
let tx_vol = tx;
|
||||
volume_slider.connect_change_value(move |_, _, val| {
|
||||
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();
|
||||
|
||||
{
|
||||
@@ -402,91 +336,70 @@ impl Module<Button> for MusicModule {
|
||||
let image_size = self.cover_image_size;
|
||||
|
||||
let mut prev_cover = None;
|
||||
glib_recv!(rx, event => {
|
||||
match event {
|
||||
ControllerEvent::Update(Some(update)) => {
|
||||
// only update art when album changes
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover = new_cover.clone();
|
||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
|
||||
}) {
|
||||
album_image.show();
|
||||
image.load_into_image(album_image.clone())
|
||||
} else {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
update_popup_metadata_label(update.song.title, &title_label);
|
||||
update_popup_metadata_label(update.song.album, &album_label);
|
||||
update_popup_metadata_label(update.song.artist, &artist_label);
|
||||
|
||||
match update.status.state {
|
||||
PlayerState::Stopped => {
|
||||
btn_pause.hide();
|
||||
btn_play.show();
|
||||
btn_play.set_sensitive(false);
|
||||
}
|
||||
PlayerState::Playing => {
|
||||
btn_play.set_sensitive(false);
|
||||
btn_play.hide();
|
||||
|
||||
btn_pause.set_sensitive(true);
|
||||
btn_pause.show();
|
||||
}
|
||||
PlayerState::Paused => {
|
||||
btn_pause.set_sensitive(false);
|
||||
btn_pause.hide();
|
||||
|
||||
btn_play.set_sensitive(true);
|
||||
btn_play.show();
|
||||
}
|
||||
}
|
||||
|
||||
let enable_prev = update.status.playlist_position > 0;
|
||||
|
||||
let enable_next =
|
||||
update.status.playlist_position < update.status.playlist_length;
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
if let Some(volume) = update.status.volume_percent {
|
||||
volume_slider.set_value(f64::from(volume));
|
||||
volume_box.show();
|
||||
rx.attach(None, move |update| {
|
||||
if let Some(update) = update {
|
||||
// only update art when album changes
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover = new_cover.clone();
|
||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
||||
}) {
|
||||
image.load_into_image(album_image.clone())
|
||||
} else {
|
||||
volume_box.hide();
|
||||
album_image.set_from_pixbuf(None);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
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());
|
||||
progress.set_range(0.0, duration.as_secs_f64());
|
||||
progress_box.show_all();
|
||||
} else {
|
||||
progress_box.hide();
|
||||
title_label
|
||||
.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 {
|
||||
PlayerState::Stopped => {
|
||||
btn_pause.hide();
|
||||
btn_play.show();
|
||||
btn_play.set_sensitive(false);
|
||||
}
|
||||
PlayerState::Playing => {
|
||||
btn_play.set_sensitive(false);
|
||||
btn_play.hide();
|
||||
|
||||
btn_pause.set_sensitive(true);
|
||||
btn_pause.show();
|
||||
}
|
||||
PlayerState::Paused => {
|
||||
btn_pause.set_sensitive(false);
|
||||
btn_pause.hide();
|
||||
|
||||
btn_play.set_sensitive(true);
|
||||
btn_play.show();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let enable_prev = update.status.playlist_position > 0;
|
||||
|
||||
let enable_next =
|
||||
update.status.playlist_position < update.status.playlist_length;
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
volume_slider.set_value(update.status.volume_percent as f64);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// 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();
|
||||
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
|
||||
@@ -519,7 +425,7 @@ fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> St
|
||||
|
||||
/// Converts a string format token 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 {
|
||||
"title" => song.title.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()),
|
||||
"genre" => song.genre.clone(),
|
||||
"track" => song.track.map(|x| x.to_string()),
|
||||
"duration" => status.duration.map(format_time),
|
||||
"elapsed" => status.elapsed.map(format_time),
|
||||
_ => Some(token.to_string()),
|
||||
}
|
||||
.unwrap_or_default()
|
||||
@@ -546,8 +454,8 @@ impl IconLabel {
|
||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||
let label = Label::new(label);
|
||||
|
||||
icon.add_class("icon-box");
|
||||
label.add_class("label");
|
||||
add_class(&icon, "icon-box");
|
||||
add_class(&label, "label");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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::{glib_recv, spawn, try_send};
|
||||
use crate::try_send;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::error;
|
||||
|
||||
@@ -82,16 +83,19 @@ impl Module<Label> for ScriptModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
) -> Result<ModuleWidget<Label>> {
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
|
||||
{
|
||||
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,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, send_async, spawn};
|
||||
use crate::gtk_helpers::add_class;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::send_async;
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
@@ -10,6 +10,7 @@ use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::time::sleep;
|
||||
@@ -185,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let re = Regex::new(r"\{([^}]+)}")?;
|
||||
|
||||
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 {
|
||||
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());
|
||||
|
||||
container.add(&label);
|
||||
@@ -204,7 +205,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
|
||||
{
|
||||
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()) {
|
||||
let format_compiled = re.replace_all(format, |caps: &Captures| {
|
||||
info.get(&caps[1])
|
||||
@@ -214,10 +215,12 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
|
||||
label.set_markup(format_compiled.as_ref());
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use crate::clients::system_tray::get_tray_event_client;
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{await_sync, glib_recv, spawn, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{await_sync, try_send};
|
||||
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::prelude::*;
|
||||
use gtk::{
|
||||
@@ -13,13 +10,11 @@ use gtk::{
|
||||
SeparatorMenuItem,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::ptr;
|
||||
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use std::collections::HashMap;
|
||||
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
||||
use stray::message::tray::StatusNotifierItem;
|
||||
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
@@ -29,43 +24,21 @@ pub struct TrayModule {
|
||||
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
|
||||
/// for the status notifier item's icon.
|
||||
fn get_image_from_icon_name(item: &StatusNotifierItem, icon_theme: &IconTheme) -> Option<Image> {
|
||||
if let Some(path) = item.icon_theme_path.as_ref() {
|
||||
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
|
||||
icon_theme.append_search_path(path);
|
||||
}
|
||||
}
|
||||
fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> {
|
||||
let theme = item
|
||||
.icon_theme_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| {
|
||||
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()))
|
||||
})
|
||||
}
|
||||
@@ -198,17 +171,16 @@ impl Module<MenuBar> for TrayModule {
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<MenuBar>> {
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<MenuBar>> {
|
||||
let container = MenuBar::new();
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let mut widgets = HashMap::new();
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
|
||||
// listen for UI updates
|
||||
glib_recv!(context.subscribe(), update => {
|
||||
context.widget_rx.attach(None, move |update| {
|
||||
match update {
|
||||
NotifierItemMessage::Update {
|
||||
item,
|
||||
@@ -220,7 +192,7 @@ impl Module<MenuBar> for TrayModule {
|
||||
let menu_item = MenuItem::new();
|
||||
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))
|
||||
.map_or_else(
|
||||
|| {
|
||||
@@ -261,10 +233,12 @@ impl Module<MenuBar> for TrayModule {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
};
|
||||
|
||||
Ok(ModuleParts {
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
@@ -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 futures_lite::stream::StreamExt;
|
||||
use gtk::{prelude::*, Button};
|
||||
use gtk::{Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use upower_dbus::BatteryState;
|
||||
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 HOUR: i64 = 60 * 60;
|
||||
const MINUTE: i64 = 60;
|
||||
@@ -61,8 +59,8 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
// await_sync due to strange "higher-ranked lifetime error"
|
||||
@@ -152,58 +150,61 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
) -> Result<ModuleWidget<Button>> {
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let icon = gtk::Image::new();
|
||||
icon.add_class("icon");
|
||||
add_class(&icon, "icon");
|
||||
|
||||
let label = Label::builder()
|
||||
.label(&self.format)
|
||||
.use_markup(true)
|
||||
.build();
|
||||
label.add_class("label");
|
||||
add_class(&label, "label");
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
container.add_class("contents");
|
||||
add_class(&container, "contents");
|
||||
|
||||
let button = Button::new();
|
||||
button.add_class("button");
|
||||
add_class(&button, "button");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
button.add(&container);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
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());
|
||||
let format = self.format.clone();
|
||||
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, properties => {
|
||||
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||
|
||||
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
|
||||
context
|
||||
.widget_rx
|
||||
.attach(None, move |properties: UpowerProperties| {
|
||||
let format = format.replace("{percentage}", &properties.percentage.to_string());
|
||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||
ImageProvider::parse(&icon_name, &icon_theme, self.icon_size)
|
||||
.map(|provider| provider.load_into_image(icon.clone()));
|
||||
label.set_markup(format.as_ref());
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
label.set_markup(format.as_ref());
|
||||
});
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
|
||||
|
||||
let rx = context.subscribe();
|
||||
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(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
@@ -214,10 +215,10 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
.build();
|
||||
|
||||
let label = Label::new(None);
|
||||
label.add_class("upower-details");
|
||||
add_class(&label, "upower-details");
|
||||
container.add(&label);
|
||||
|
||||
glib_recv!(rx, properties => {
|
||||
rx.attach(None, move |properties| {
|
||||
let state = u32_to_battery_state(properties.state);
|
||||
let format = match state {
|
||||
Ok(BatteryState::Charging | BatteryState::PendingCharge) => {
|
||||
@@ -244,6 +245,7 @@ impl Module<gtk::Button> for UpowerModule {
|
||||
};
|
||||
|
||||
label.set_markup(&format);
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
container.show_all();
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate};
|
||||
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, send_async, spawn, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::{send_async, try_send};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme};
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, trace, warn};
|
||||
use tracing::trace;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
#[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)]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
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.
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
@@ -75,7 +55,7 @@ const fn default_icon_size() -> i32 {
|
||||
/// Creates a button from a workspace
|
||||
fn create_button(
|
||||
name: &str,
|
||||
visibility: Visibility,
|
||||
focused: bool,
|
||||
name_map: &HashMap<String, String>,
|
||||
icon_theme: &IconTheme,
|
||||
icon_size: i32,
|
||||
@@ -89,18 +69,10 @@ fn create_button(
|
||||
let style_context = button.style_context();
|
||||
style_context.add_class("item");
|
||||
|
||||
if visibility.is_visible() {
|
||||
style_context.add_class("visible");
|
||||
}
|
||||
|
||||
if visibility.is_focused() {
|
||||
if focused {
|
||||
style_context.add_class("focused");
|
||||
}
|
||||
|
||||
if !visibility.is_visible() {
|
||||
style_context.add_class("inactive")
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let name = name.to_string();
|
||||
@@ -133,13 +105,6 @@ fn reorder_workspaces(container: >k::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 {
|
||||
type SendMessage = WorkspaceUpdate;
|
||||
type ReceiveMessage = String;
|
||||
@@ -162,10 +127,9 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
client.subscribe_workspace_change()
|
||||
};
|
||||
|
||||
trace!("Set up workspace subscription");
|
||||
trace!("Set up Sway workspace subscription");
|
||||
|
||||
while let Ok(payload) = srx.recv().await {
|
||||
debug!("Received update: {payload:?}");
|
||||
send_async!(tx, ModuleUpdateEvent::Update(payload));
|
||||
}
|
||||
});
|
||||
@@ -190,12 +154,10 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
|
||||
|
||||
let name_map = self.name_map.clone().unwrap_or_default();
|
||||
let favs = self.favorites.clone();
|
||||
let mut fav_names: Vec<String> = vec![];
|
||||
let name_map = self.name_map.unwrap_or_default();
|
||||
|
||||
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
|
||||
let mut has_initialized = false;
|
||||
|
||||
glib_recv!(context.subscribe(), event => {
|
||||
context.widget_rx.attach(None, move |event| {
|
||||
match event {
|
||||
WorkspaceUpdate::Init(workspaces) => {
|
||||
if !has_initialized {
|
||||
trace!("Creating workspace buttons");
|
||||
for workspace in workspaces {
|
||||
if self.all_monitors || workspace.monitor == output_name {
|
||||
let item = create_button(
|
||||
&workspace.name,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
container.add(&item);
|
||||
|
||||
let mut added = HashSet::new();
|
||||
|
||||
let mut add_workspace = |name: &str, visibility: Visibility| {
|
||||
let item = create_button(
|
||||
name,
|
||||
visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
container.add(&item);
|
||||
button_map.insert(name.to_string(), item);
|
||||
};
|
||||
|
||||
// add workspaces from client
|
||||
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);
|
||||
}
|
||||
button_map.insert(workspace.name, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,33 +201,22 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Focus { old, new } => {
|
||||
if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) {
|
||||
if Some(new.monitor) == old.map(|w| w.monitor) {
|
||||
btn.style_context().remove_class("visible");
|
||||
}
|
||||
|
||||
btn.style_context().remove_class("focused");
|
||||
let old = button_map.get(&old);
|
||||
if let Some(old) = old {
|
||||
old.style_context().remove_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");
|
||||
let new = button_map.get(&new);
|
||||
if let Some(new) = new {
|
||||
new.style_context().add_class("focused");
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Add(workspace) => {
|
||||
if fav_names.contains(&workspace.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) {
|
||||
if self.all_monitors || workspace.monitor == output_name {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.visibility,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
@@ -314,12 +236,12 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Move(workspace) => {
|
||||
if !self.hidden.contains(&workspace.name) && !self.all_monitors {
|
||||
if !self.all_monitors {
|
||||
if workspace.monitor == output_name {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.visibility,
|
||||
workspace.focused,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
@@ -345,19 +267,17 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
WorkspaceUpdate::Remove(workspace) => {
|
||||
let button = button_map.get(&workspace);
|
||||
if let Some(item) = button {
|
||||
if fav_names.contains(&workspace) {
|
||||
item.style_context().add_class("inactive");
|
||||
} else {
|
||||
container.remove(item);
|
||||
}
|
||||
container.remove(item);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Unknown => warn!("Received unknown type workspace event")
|
||||
WorkspaceUpdate::Update(_) => {}
|
||||
};
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
|
||||
162
src/popup.rs
162
src/popup.rs
@@ -1,30 +1,18 @@
|
||||
use glib::Propagation;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::config::BarPosition;
|
||||
use crate::modules::ModuleInfo;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{ApplicationWindow, Orientation};
|
||||
use gtk_layer_shell::LayerShell;
|
||||
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)]
|
||||
pub struct Popup {
|
||||
pub window: ApplicationWindow,
|
||||
pub cache: HashMap<usize, PopupCacheValue>,
|
||||
pub cache: HashMap<usize, gtk::Box>,
|
||||
monitor: Monitor,
|
||||
pos: BarPosition,
|
||||
current_widget: Option<usize>,
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
@@ -39,38 +27,51 @@ impl Popup {
|
||||
.application(module_info.app)
|
||||
.build();
|
||||
|
||||
win.init_layer_shell();
|
||||
win.set_monitor(module_info.monitor);
|
||||
win.set_layer(gtk_layer_shell::Layer::Overlay);
|
||||
win.set_namespace(env!("CARGO_PKG_NAME"));
|
||||
gtk_layer_shell::init_for_window(&win);
|
||||
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
|
||||
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
|
||||
|
||||
win.set_layer_shell_margin(
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
if pos == BarPosition::Top { gap } else { 0 },
|
||||
);
|
||||
win.set_layer_shell_margin(
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
if pos == BarPosition::Bottom { gap } else { 0 },
|
||||
);
|
||||
win.set_layer_shell_margin(
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Left,
|
||||
if pos == BarPosition::Left { gap } else { 0 },
|
||||
);
|
||||
win.set_layer_shell_margin(
|
||||
gtk_layer_shell::set_margin(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Right,
|
||||
if pos == BarPosition::Right { gap } else { 0 },
|
||||
);
|
||||
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::set_anchor(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Top,
|
||||
pos == BarPosition::Top || orientation == Orientation::Vertical,
|
||||
);
|
||||
win.set_anchor(gtk_layer_shell::Edge::Bottom, pos == BarPosition::Bottom);
|
||||
win.set_anchor(
|
||||
gtk_layer_shell::set_anchor(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Bottom,
|
||||
pos == BarPosition::Bottom,
|
||||
);
|
||||
gtk_layer_shell::set_anchor(
|
||||
&win,
|
||||
gtk_layer_shell::Edge::Left,
|
||||
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| {
|
||||
const THRESHOLD: f64 = 3.0;
|
||||
@@ -99,7 +100,7 @@ impl Popup {
|
||||
win.hide();
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -107,54 +108,20 @@ impl Popup {
|
||||
cache: HashMap::new(),
|
||||
monitor: module_info.monitor.clone(),
|
||||
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);
|
||||
|
||||
for button in &content.buttons {
|
||||
let id = Ironbar::unique_id();
|
||||
button.set_tag("popup-id", id);
|
||||
}
|
||||
|
||||
self.cache.insert(key, PopupCacheValue { name, content });
|
||||
self.cache.insert(key, content);
|
||||
}
|
||||
|
||||
pub fn show(&mut self, widget_id: usize, button_id: usize) {
|
||||
pub fn show_content(&self, key: usize) {
|
||||
self.clear_window();
|
||||
|
||||
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
|
||||
self.current_widget = Some(widget_id);
|
||||
|
||||
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);
|
||||
if let Some(content) = self.cache.get(&key) {
|
||||
content.style_context().add_class("popup");
|
||||
self.window.add(content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
pub fn hide(&mut self) {
|
||||
self.current_widget = None;
|
||||
pub fn hide(&self) {
|
||||
self.window.hide();
|
||||
}
|
||||
|
||||
@@ -176,10 +148,6 @@ impl Popup {
|
||||
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
|
||||
/// (depending on orientation).
|
||||
fn set_pos(&self, geometry: WidgetGeometry) {
|
||||
@@ -217,6 +185,50 @@ impl Popup {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ impl Script {
|
||||
Ok(output) => callback(output.0, output.1),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
ScriptMode::Watch => match self.spawn() {
|
||||
ScriptMode::Watch => match self.spawn().await {
|
||||
Ok(mut rx) => {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
callback(msg, true);
|
||||
@@ -264,7 +264,7 @@ impl Script {
|
||||
/// Spawns a long-running process.
|
||||
/// Returns a `mpsc::Receiver` that sends a message
|
||||
/// every time a new line is written to `stdout` or `stderr`.
|
||||
pub fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
|
||||
pub async fn spawn(&self) -> Result<mpsc::Receiver<OutputStream>> {
|
||||
let mut handle = Command::new("/bin/sh")
|
||||
.args(["-c", &self.cmd])
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
50
src/style.rs
50
src/style.rs
@@ -1,13 +1,14 @@
|
||||
use crate::{glib_recv_mpsc, spawn, try_send};
|
||||
use crate::send;
|
||||
use color_eyre::{Help, Report};
|
||||
use glib::Continue;
|
||||
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||
use gtk::prelude::CssProviderExt;
|
||||
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 std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::spawn;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@@ -35,20 +36,14 @@ pub fn load_css(style_path: PathBuf) {
|
||||
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 {
|
||||
let style_path2 = style_path.clone();
|
||||
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:?}");
|
||||
if event
|
||||
.paths
|
||||
.first()
|
||||
.map(|p| p == &style_path2)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
try_send!(tx, style_path2.clone());
|
||||
if let Some(path) = event.paths.first() {
|
||||
send!(tx, path.clone());
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
let dir_path = style_path.parent().expect("to exist");
|
||||
|
||||
watcher
|
||||
.watch(dir_path, RecursiveMode::NonRecursive)
|
||||
.watch(&style_path, RecursiveMode::NonRecursive)
|
||||
.expect("Failed to start CSS file watcher");
|
||||
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 => {
|
||||
info!("Reloading CSS");
|
||||
if let Err(err) = provider.load_from_file(&gio::File::for_path(path)) {
|
||||
error!("{:?}", Report::new(err)
|
||||
.wrap_err("Failed to load CSS")
|
||||
.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.")
|
||||
);
|
||||
}
|
||||
});
|
||||
{
|
||||
rx.attach(None, move |path| {
|
||||
info!("Reloading CSS");
|
||||
if let Err(err) = provider
|
||||
.load_from_file(&gio::File::for_path(path)) {
|
||||
error!("{:?}", Report::new(err)
|
||||
.wrap_err("Failed to load CSS")
|
||||
.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.")
|
||||
);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
9
src/unique_id.rs
Normal file
9
src/unique_id.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
/// Gets a `usize` ID value that is unique to the entire Ironbar instance.
|
||||
/// This is just an `AtomicUsize` that increments every time this function is called.
|
||||
pub fn get_unique_usize() -> usize {
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
Reference in New Issue
Block a user