1 Commits

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

1
.envrc
View File

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

View File

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

803
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,6 @@ edition = "2021"
license = "MIT" license = "MIT"
description = "Customisable GTK Layer Shell wlroots/sway bar" description = "Customisable GTK Layer Shell wlroots/sway bar"
repository = "https://github.com/jakestanger/ironbar" repository = "https://github.com/jakestanger/ironbar"
categories = ["gui"]
keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features] [features]
default = [ default = [
@@ -52,7 +50,7 @@ music = ["regex"]
sys_info = ["sysinfo", "regex"] sys_info = ["sysinfo", "regex"]
tray = ["system-tray"] tray = ["stray"]
upower = ["upower_dbus", "zbus", "futures-lite"] upower = ["upower_dbus", "zbus", "futures-lite"]
@@ -66,7 +64,7 @@ workspaces = ["futures-util"]
gtk = "0.17.0" gtk = "0.17.0"
gtk-layer-shell = "0.6.0" gtk-layer-shell = "0.6.0"
glib = "0.17.10" glib = "0.17.10"
tokio = { version = "1.31.0", features = [ tokio = { version = "1.28.2", features = [
"macros", "macros",
"rt-multi-thread", "rt-multi-thread",
"time", "time",
@@ -79,20 +77,20 @@ tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-appender = "0.2.2" tracing-appender = "0.2.2"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2" color-eyre = "0.6.2"
serde = { version = "1.0.183", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
indexmap = "2.0.0" indexmap = "2.0.0"
dirs = "5.0.1" dirs = "5.0.1"
walkdir = "2.3.2" walkdir = "2.3.2"
notify = { version = "6.0.1", default-features = false } notify = { version = "6.0.1", default-features = false }
wayland-client = "0.30.2" wayland-client = "0.30.2"
wayland-protocols = { version = "0.30.1", features = ["unstable", "client"] } wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] }
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] } wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [ smithay-client-toolkit = { version = "0.17.0", default-features = false, features = [
"calloop", "calloop",
] } ] }
universal-config = { version = "0.4.2", default_features = false } universal-config = { version = "0.4.0", default_features = false }
ctrlc = "3.4.0" ctrlc = "3.4.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
@@ -100,10 +98,10 @@ async_once = "0.2.6"
cfg-if = "1.0.0" cfg-if = "1.0.0"
# cli # cli
clap = { version = "4.3.21", optional = true, features = ["derive"] } clap = { version = "4.2.7", optional = true, features = ["derive"] }
# ipc # ipc
serde_json = { version = "1.0.104", optional = true } serde_json = { version = "1.0.96", optional = true }
# http # http
reqwest = { version = "0.11.18", optional = true } reqwest = { version = "0.11.18", optional = true }
@@ -112,29 +110,32 @@ reqwest = { version = "0.11.18", optional = true }
nix = { version = "0.26.2", optional = true, features = ["event"] } nix = { version = "0.26.2", optional = true, features = ["event"] }
# clock # clock
chrono = { version = "0.4.26", optional = true, features = ["unstable-locales"] } chrono = { version = "0.4.26", optional = true }
# music # music
mpd_client = { version = "1.2.0", optional = true } mpd_client = { version = "1.0.0", optional = true }
mpris = { version = "2.0.1", optional = true } mpris = { version = "2.0.1", optional = true }
# sys_info # sys_info
sysinfo = { version = "0.29.8", optional = true } sysinfo = { version = "0.29.2", optional = true }
# tray # tray
system-tray = { version = "0.1.4", optional = true } stray = { version = "0.1.3", optional = true }
# upower # upower
upower_dbus = { version = "0.3.2", optional = true } upower_dbus = { version = "0.3.2", optional = true }
futures-lite = { version = "1.12.0", optional = true } futures-lite = { version = "1.12.0", optional = true }
zbus = { version = "3.14.1", optional = true } zbus = { version = "3.13.1", optional = true }
# workspaces # workspaces
swayipc-async = { version = "2.0.1", optional = true } swayipc-async = { version = "2.0.1", optional = true }
hyprland = { version = "0.3.8", features = ["silent"], optional = true } hyprland = { version = "=0.3.1", optional = true }
futures-util = { version = "0.3.21", optional = true } futures-util = { version = "0.3.21", optional = true }
# shared # shared
regex = { version = "1.8.4", default-features = false, features = [ regex = { version = "1.8.4", default-features = false, features = [
"std", "std",
], optional = true } # music, sys_info ], optional = true } # music, sys_info
[patch.crates-io]
stray = { git = "https://github.com/jakestanger/stray", branch = "fix/connection-errors" }

View File

@@ -267,22 +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: The following table lists each of the top-level bar config options:
| Name | Type | Default | Description | | Name | Type | Default | Description |
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------| |--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
| `name` | `string` | `bar-<n>` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. | | `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | | `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | | `height` | `integer` | `42` | The bar's height in pixels. |
| `height` | `integer` | `42` | The bar's height in pixels. | | `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. | | `margin.top` | `integer` | `0` | The margin on the top of the bar |
| `margin.top` | `integer` | `0` | The margin on the top of the bar | | `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar |
| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | | `margin.left` | `integer` | `0` | The margin on the left of the bar |
| `margin.left` | `integer` | `0` | The margin on the left of the bar | | `margin.right` | `integer` | `0` | The margin on the right of the bar |
| `margin.right` | `integer` | `0` | The margin on the right of the bar | | `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | | `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. | | `start` | `Module[]` | `[]` | Array of left or top modules. |
| `start` | `Module[]` | `[]` | Array of left or top modules. | | `center` | `Module[]` | `[]` | Array of center modules. |
| `center` | `Module[]` | `[]` | Array of center modules. | | `end` | `Module[]` | `[]` | Array of right or bottom modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
### 3.2 Module-level options ### 3.2 Module-level options

View File

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

View File

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

View File

@@ -128,6 +128,8 @@ and will be replaced with values from the currently playing track:
| `{track}` | Track number | | `{track}` | Track number |
| `{disc}` | Disc number | | `{disc}` | Disc number |
| `{genre}` | Genre | | `{genre}` | Genre |
| `{duration}` | Duration in `mm:ss` |
| `{elapsed}` | Time elapsed in `mm:ss` |
## Styling ## Styling
@@ -164,10 +166,7 @@ and will be replaced with values from the currently playing track:
| `.popup-music .controls .btn-pause` | Pause button inside popup box | | `.popup-music .controls .btn-pause` | Pause button inside popup box |
| `.popup-music .controls .btn-next` | Next button inside popup box | | `.popup-music .controls .btn-next` | Next button inside popup box |
| `.popup-music .volume` | Volume container inside popup box | | `.popup-music .volume` | Volume container inside popup box |
| `.popup-music .volume .slider` | Slider inside volume container | | `.popup-music .volume .slider` | Volume slider popup box |
| `.popup-music .volume .icon` | Icon inside volume container | | `.popup-music .volume .icon` | Volume icon label inside popup box |
| `.popup-music .progress` | Progress (seek) bar container |
| `.popup-music .progress .slider` | Slider inside progress container |
| `.popup-music .progress .label` | Duration label inside progress container |
For more information on styling, please see the [styling guide](styling-guide). For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -8,14 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces` > Type: `workspaces`
| Name | Type | Default | Description | | Name | Type | Default | Description |
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | | `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. | | `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `hidden` | `string[]` | `[]` | A list of workspace names to never show | | `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | | `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
| `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> <details>
<summary>JSON</summary> <summary>JSON</summary>
@@ -30,7 +28,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
"2": "", "2": "",
"3": "" "3": ""
}, },
"favorites": ["1", "2", "3"],
"all_monitors": false "all_monitors": false
} }
] ]
@@ -46,7 +43,6 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
[[end]] [[end]]
type = "workspaces" type = "workspaces"
all_monitors = false all_monitors = false
favorites = ["1", "2", "3"]
[[end.name_map]] [[end.name_map]]
1 = "" 1 = ""
@@ -67,10 +63,6 @@ end:
1: "" 1: ""
2: "" 2: ""
3: "" 3: ""
favorites:
- "1"
- "2"
- "3"
all_monitors: false all_monitors: false
``` ```
@@ -87,7 +79,6 @@ end:
name_map.1 = "" name_map.1 = ""
name_map.2 = "" name_map.2 = ""
name_map.3 = "" name_map.3 = ""
favorites = [ "1" "2" "3" ]
all_monitors = false all_monitors = false
} }
] ]
@@ -107,4 +98,4 @@ end:
| `.workspaces .item .text-icon` | Workspace button icon (textual only) | | `.workspaces .item .text-icon` | Workspace button icon (textual only) |
| `.workspaces .item .image` | Workspace button icon (image only) | | `.workspaces .item .image` | Workspace button icon (image only) |
For more information on styling, please see the [styling guide](styling-guide). For more information on styling, please see the [styling guide](styling-guide).

View File

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

149
flake.lock generated
View File

@@ -1,66 +1,9 @@
{ {
"nodes": { "nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1688772518,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
}, },
"locked": {
"lastModified": 1687709756,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": { "locked": {
"lastModified": 1681202837, "lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
@@ -75,45 +18,13 @@
"type": "github" "type": "github"
} }
}, },
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1690373729,
"narHash": "sha256-e136hTT7LqQ2QjOTZQMW+jnsevWwBpMj78u6FRUsH9I=",
"owner": "nix-community",
"repo": "naersk",
"rev": "d9a33d69a9c421d64c8d925428864e93be895dcc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1690833316, "lastModified": 1686960236,
"narHash": "sha256-+YU+/pTJmVKNW12R07/SJiTn7PQk90xwCI4D2PfLRPs=", "narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9418167277f665de6f4a29f414d438cf39c55b9e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1690789960,
"narHash": "sha256-3K+2HuyGTiJUSZNJxXXvc0qj4xFx1FHC/ItYtEa7/Xs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fb942492b7accdee4e6d17f5447091c65897dde4", "rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -125,50 +36,23 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane", "nixpkgs": "nixpkgs",
"naersk": "naersk", "rust-overlay": "rust-overlay"
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay_2"
} }
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": [ "flake-utils": "flake-utils",
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688351637,
"narHash": "sha256-CLTufJ29VxNOIZ8UTg0lepsn3X03AmopmaLTTeHDCL4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "f9b92316727af9e6c7fee4a761242f7f46880329",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1690683485, "lastModified": 1686968542,
"narHash": "sha256-Sp/QpbMg86v12xhCsa6q0yTH8LYaJIcxzbf9LO1zFzM=", "narHash": "sha256-Gjlj7UeHqMFRAYyefeoLnSjLo8V+0XheIamojNEyTbE=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "05d480a7aef1aae1bfb67a39134dcf48c5322528", "rev": "01d84cd842e48e89be67e4c2d9dc46aa7709adc5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -191,21 +75,6 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -6,18 +6,11 @@
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
rust-overlay, rust-overlay,
crane,
naersk,
... ...
}: let }: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
@@ -34,18 +27,10 @@
rust-overlay.overlays.default rust-overlay.overlays.default
]; ];
}; };
mkRustToolchain = pkgs: mkRustToolchain = pkgs: pkgs.rust-bin.stable.latest.default;
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
in { in {
overlays.default = final: prev: let overlays.default = final: prev: let
rust = mkRustToolchain final; rust = mkRustToolchain final;
craneLib = (crane.mkLib final).overrideToolchain rust;
naersk' = prev.callPackage naersk {
cargo = rust;
rustc = rust;
};
rustPlatform = prev.makeRustPlatform { rustPlatform = prev.makeRustPlatform {
cargo = rust; cargo = rust;
@@ -57,33 +42,11 @@
(builtins.substring 4 2 longDate) (builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate) (builtins.substring 6 2 longDate)
]); ]);
builder = "naersk";
in { in {
ironbar = let ironbar = prev.callPackage ./nix/default.nix {
version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); version = props.package.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in inherit rustPlatform;
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;
};
}; };
packages = genSystems ( packages = genSystems (
system: let system: let
@@ -119,14 +82,6 @@
gtk-layer-shell gtk-layer-shell
pkg-config pkg-config
openssl openssl
gdk-pixbuf
glib
glib-networking
shared-mime-info
gnome.adwaita-icon-theme
hicolor-icon-theme
gsettings-desktop-schemas
libxkbcommon
]; ];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";

View File

@@ -19,77 +19,49 @@
lib, lib,
version ? "git", version ? "git",
features ? [], features ? [],
builderName ? "nix", }:
builder ? {}, rustPlatform.buildRustPackage rec {
}: let inherit version;
basePkg = rec { pname = "ironbar";
inherit version; src = builtins.path {
pname = "ironbar"; name = "ironbar";
src = builtins.path { path = lib.cleanSource ../.;
name = "ironbar"; };
path = lib.cleanSource ../.; buildNoDefaultFeatures =
}; if features == []
nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection]; then false
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]; else true;
propagatedBuildInputs = [ buildFeatures = features;
gtk3 cargoDeps = rustPlatform.importCargoLock {
]; lockFile = ../Cargo.lock;
preFixup = '' };
gappsWrapperArgs+=( cargoLock.lockFile = ../Cargo.lock;
# Thumbnailers cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
--prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share" nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
--prefix XDG_DATA_DIRS : "${librsvg}/share" 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];
--prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share" propagatedBuildInputs = [
--prefix XDG_DATA_DIRS : "${shared-mime-info}/share" gtk3
) ];
''; preFixup = ''
passthru = { gappsWrapperArgs+=(
updateScript = gnome.updateScript { # Thumbnailers
packageName = pname; --prefix XDG_DATA_DIRS : "${gdk-pixbuf}/share"
attrPath = "gnome.${pname}"; --prefix XDG_DATA_DIRS : "${librsvg}/share"
}; --prefix XDG_DATA_DIRS : "${webp-pixbuf-loader}/share"
}; --prefix XDG_DATA_DIRS : "${shared-mime-info}/share"
meta = with lib; { )
homepage = "https://github.com/JakeStanger/ironbar"; '';
description = "Customisable gtk-layer-shell wlroots/sway bar written in rust."; passthru = {
license = licenses.mit; updateScript = gnome.updateScript {
platforms = platforms.linux; packageName = pname;
mainProgram = "ironbar"; attrPath = "gnome.${pname}";
}; };
}; };
flags = let meta = with lib; {
noDefault = homepage = "https://github.com/JakeStanger/ironbar";
if features == [] description = "Customisable gtk-layer-shell wlroots/sway bar written in rust.";
then "" license = licenses.mit;
else "--no-default-features"; platforms = platforms.linux;
featuresStr = mainProgram = "ironbar";
if features == [] };
then "" }
else ''-F "${builtins.concatStringsSep "," features}"'';
in [noDefault featuresStr];
in
if builderName == "naersk"
then
builder.buildPackage (basePkg
// {
cargoOptions = old: old ++ flags;
})
else if builderName == "crane"
then
builder.buildPackage (basePkg
// {
cargoExtraArgs = builtins.concatStringsSep " " flags;
doCheck = false;
})
else
rustPlatform.buildRustPackage (basePkg
// {
buildNoDefaultFeatures =
if features == []
then false
else true;
buildFeatures = features;
cargoDeps = rustPlatform.importCargoLock {lockFile = ../Cargo.lock;};
cargoLock.lockFile = ../Cargo.lock;
cargoLock.outputHashes."stray-0.1.3" = "sha256-7mvsWZFmPWti9AiX67h6ZlWiVVRZRWIxq3pVaviOUtc=";
})

View File

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

View File

@@ -4,13 +4,11 @@ use crate::modules::{
}; };
use crate::popup::Popup; use crate::popup::Popup;
use crate::unique_id::get_unique_usize; use crate::unique_id::get_unique_usize;
use crate::{arc_rw, Config, GlobalState}; use crate::Config;
use color_eyre::Result; use color_eyre::Result;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation}; use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use tracing::{debug, info}; use tracing::{debug, info};
@@ -21,16 +19,8 @@ pub fn create_bar(
monitor: &Monitor, monitor: &Monitor,
monitor_name: &str, monitor_name: &str,
config: Config, config: Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> { ) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build(); let win = ApplicationWindow::builder().application(app).build();
let bar_name = config
.name
.clone()
.unwrap_or_else(|| format!("bar-{}", get_unique_usize()));
win.set_widget_name(&bar_name);
info!("Creating bar {}", bar_name);
setup_layer_shell( setup_layer_shell(
&win, &win,
@@ -65,12 +55,7 @@ pub fn create_bar(
content.set_center_widget(Some(&center)); content.set_center_widget(Some(&center));
content.pack_end(&end, false, false, 0); content.pack_end(&end, false, false, 0);
let load_result = load_modules(&start, &center, &end, app, config, monitor, monitor_name)?; load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
global_state
.borrow_mut()
.popups_mut()
.insert(bar_name.into(), load_result.popup);
win.add(&content); win.add(&content);
win.connect_destroy_event(|_, _| { win.connect_destroy_event(|_, _| {
@@ -151,11 +136,6 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
container container
} }
#[derive(Debug)]
struct BarLoadResult {
popup: Arc<RwLock<Popup>>,
}
/// Loads the configured modules onto a bar. /// Loads the configured modules onto a bar.
fn load_modules( fn load_modules(
left: &gtk::Box, left: &gtk::Box,
@@ -165,7 +145,7 @@ fn load_modules(
config: Config, config: Config,
monitor: &Monitor, monitor: &Monitor,
output_name: &str, output_name: &str,
) -> Result<BarLoadResult> { ) -> Result<()> {
let icon_theme = IconTheme::new(); let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme { if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme)); icon_theme.set_custom_theme(Some(theme));
@@ -186,7 +166,7 @@ fn load_modules(
// popup ignores module location so can bodge this for now // popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap); let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = arc_rw!(popup); let popup = Arc::new(RwLock::new(popup));
if let Some(modules) = config.start { if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left); let info = info!(ModuleLocation::Left);
@@ -203,9 +183,7 @@ fn load_modules(
add_modules(right, modules, &info, &popup)?; add_modules(right, modules, &info, &popup)?;
} }
let result = BarLoadResult { popup }; Ok(())
Ok(result)
} }
/// Adds modules into a provided GTK box, /// Adds modules into a provided GTK box,
@@ -220,14 +198,8 @@ fn add_modules(
macro_rules! add_module { macro_rules! add_module {
($module:expr, $id:expr) => {{ ($module:expr, $id:expr) => {{
let common = $module.common.take().expect("common config to exist"); let common = $module.common.take().expect("Common config did not exist");
let widget_parts = create_module( let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
*$module,
$id,
common.name.clone(),
&info,
&Arc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common); set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation); let container = wrap_widget(&widget_parts.widget, common, orientation);

View File

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

View File

@@ -1,12 +1,13 @@
use super::{Workspace, WorkspaceClient, WorkspaceUpdate}; use super::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{arc_mut, lock, send}; use crate::{lock, send};
use color_eyre::Result; use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces}; use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial}; use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListener; use hyprland::event_listener::EventListenerMutable as EventListener;
use hyprland::prelude::*; use hyprland::prelude::*;
use hyprland::shared::WorkspaceType; use hyprland::shared::WorkspaceType;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
@@ -35,18 +36,18 @@ impl EventClient {
let mut event_listener = EventListener::new(); let mut event_listener = EventListener::new();
// we need a lock to ensure events don't run at the same time // we need a lock to ensure events don't run at the same time
let lock = arc_mut!(()); let lock = Arc::new(Mutex::new(()));
// cache the active workspace since Hyprland doesn't give us the prev active // cache the active workspace since Hyprland doesn't give us the prev active
let active = Self::get_active_workspace().expect("Failed to get active workspace"); let active = Self::get_active_workspace().expect("Failed to get active workspace");
let active = arc_mut!(Some(active)); let active = Arc::new(Mutex::new(Some(active)));
{ {
let tx = tx.clone(); let tx = tx.clone();
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_workspace_added_handler(move |workspace_type| { event_listener.add_workspace_added_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
debug!("Added workspace: {workspace_type:?}"); debug!("Added workspace: {workspace_type:?}");
@@ -69,7 +70,7 @@ impl EventClient {
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_workspace_change_handler(move |workspace_type| { event_listener.add_workspace_change_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
@@ -105,9 +106,9 @@ impl EventClient {
let lock = lock.clone(); let lock = lock.clone();
let active = active.clone(); let active = active.clone();
event_listener.add_active_monitor_change_handler(move |event_data| { event_listener.add_active_monitor_change_handler(move |event_data, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let workspace_type = event_data.workspace; let workspace_type = event_data.1;
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
@@ -134,9 +135,9 @@ impl EventClient {
let tx = tx.clone(); let tx = tx.clone();
let lock = lock.clone(); let lock = lock.clone();
event_listener.add_workspace_moved_handler(move |event_data| { event_listener.add_workspace_moved_handler(move |event_data, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
let workspace_type = event_data.workspace; let workspace_type = event_data.1;
debug!("Received workspace move: {workspace_type:?}"); debug!("Received workspace move: {workspace_type:?}");
let mut prev_workspace = lock!(active); let mut prev_workspace = lock!(active);
@@ -158,7 +159,7 @@ impl EventClient {
} }
{ {
event_listener.add_workspace_destroy_handler(move |workspace_type| { event_listener.add_workspace_destroy_handler(move |workspace_type, _state| {
let _lock = lock!(lock); let _lock = lock!(lock);
debug!("Received workspace destroy: {workspace_type:?}"); debug!("Received workspace destroy: {workspace_type:?}");
@@ -221,12 +222,9 @@ impl EventClient {
impl WorkspaceClient for EventClient { impl WorkspaceClient for EventClient {
fn focus(&self, id: String) -> Result<()> { fn focus(&self, id: String) -> Result<()> {
let identifier = match id.parse::<i32>() { Dispatch::call(DispatchType::Workspace(
Ok(inum) => WorkspaceIdentifierWithSpecial::Id(inum), WorkspaceIdentifierWithSpecial::Name(&id),
Err(_) => WorkspaceIdentifierWithSpecial::Name(&id), ))?;
};
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(()) Ok(())
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ cfg_if! {
use super::ClipboardItem; use super::ClipboardItem;
use super::wlr_data_control::manager::DataControlDeviceManagerState; use super::wlr_data_control::manager::DataControlDeviceManagerState;
use crate::lock; use crate::lock;
use std::sync::Arc; use std::sync::{Arc, Mutex};
} }
} }
@@ -138,7 +138,7 @@ impl WaylandClient {
seats: vec![], seats: vec![],
handles: HashMap::new(), handles: HashMap::new(),
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard: crate::arc_mut!(None), clipboard: Arc::new(Mutex::new(None)),
toplevel_tx, toplevel_tx,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx, clipboard_tx,

View File

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

View File

@@ -239,7 +239,7 @@ impl DataControlOfferHandler for Environment {
_offer: &mut DataControlDeviceOffer, _offer: &mut DataControlDeviceOffer,
_mime_type: String, _mime_type: String,
) { ) {
trace!("Handler received offer"); debug!("Handler received offer");
} }
} }
@@ -315,6 +315,22 @@ impl DataControlSourceHandler for Environment {
} }
} }
} }
// for chunk in bytes.chunks(pipe_size as usize) {
// trace!("Writing chunk");
// file.write(chunk).expect("Failed to write chunk to buffer");
// file.flush().expect("Failed to flush to file");
// }
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
// match file.write_all(bytes) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
} else { } else {
error!("Failed to find source"); error!("Failed to find source");
} }
@@ -359,14 +375,11 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
let new_size = if size > curr_size { let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}"); trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?; let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
trace!("New pipe size: {res}"); trace!("New pipe size: {res}");
if res < size as i32 { if res < size as i32 {
return Err(io::Error::last_os_error()); return Err(io::Error::last_os_error());
} }
res res
} else { } else {
size as i32 size as i32

View File

@@ -7,7 +7,7 @@ use smithay_client_toolkit::data_device_manager::ReadPipe;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::os::fd::FromRawFd; use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::{trace, warn}; use tracing::{debug, warn};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1, Event, ZwlrDataControlOfferV1,
@@ -149,7 +149,7 @@ where
let data = data.data_control_offer_data(); let data = data.data_control_offer_data();
if let Event::Offer { mime_type } = event { if let Event::Offer { mime_type } = event {
trace!("Adding new offer with type '{mime_type}'"); debug!("Adding new offer with type '{mime_type}'");
data.push_mime_type(mime_type.clone()); data.push_mime_type(mime_type.clone());
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type); state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
} }

View File

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

View File

@@ -21,7 +21,6 @@ use crate::modules::tray::TrayModule;
use crate::modules::upower::UpowerModule; use crate::modules::upower::UpowerModule;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule; use crate::modules::workspaces::WorkspacesModule;
use cfg_if::cfg_if;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -31,7 +30,7 @@ pub use self::truncate::{EllipsizeMode, TruncateMode};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig { pub enum ModuleConfig {
#[cfg(feature = "clipboard")] #[cfg(feature = "clock")]
Clipboard(Box<ClipboardModule>), Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
Clock(Box<ClockModule>), Clock(Box<ClockModule>),
@@ -97,7 +96,6 @@ pub struct Config {
pub margin: MarginConfig, pub margin: MarginConfig,
#[serde(default = "default_popup_gap")] #[serde(default = "default_popup_gap")]
pub popup_gap: i32, pub popup_gap: i32,
pub name: Option<String>,
/// GTK icon theme to use. /// GTK icon theme to use.
pub icon_theme: Option<String>, pub icon_theme: Option<String>,
@@ -111,36 +109,6 @@ pub struct Config {
pub monitors: Option<HashMap<String, MonitorConfig>>, pub monitors: Option<HashMap<String, MonitorConfig>>,
} }
impl Default for Config {
fn default() -> Self {
cfg_if! {
if #[cfg(feature = "clock")] {
let end = Some(vec![ModuleConfig::Clock(Box::default())]);
}
else {
let end = None;
}
}
Self {
position: BarPosition::default(),
height: default_bar_height(),
margin: MarginConfig::default(),
name: None,
popup_gap: default_popup_gap(),
icon_theme: None,
ironvar_defaults: None,
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
)]),
center: Some(vec![ModuleConfig::Focused(Box::default())]),
end,
anchor_to_edges: default_true(),
monitors: None,
}
}
}
const fn default_bar_height() -> i32 { const fn default_bar_height() -> i32 {
42 42
} }

View File

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

View File

@@ -1,8 +1,9 @@
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
use crate::ironvar::get_variable_manager; use crate::ironvar::get_variable_manager;
use crate::script::{OutputStream, Script}; use crate::script::{OutputStream, Script};
use crate::{arc_mut, lock, send}; use crate::{lock, send};
use gtk::prelude::*; use gtk::prelude::*;
use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
/// A segment of a dynamic string, /// A segment of a dynamic string,
@@ -33,7 +34,7 @@ where
{ {
let tokens = parse_input(input); let tokens = parse_input(input);
let label_parts = arc_mut!(vec![]); let label_parts = Arc::new(Mutex::new(Vec::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in tokens.into_iter().enumerate() { for (i, segment) in tokens.into_iter().enumerate() {
@@ -144,7 +145,7 @@ fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
.map(|w| w[0]) .map(|w| w[0])
.collect::<String>(); .collect::<String>();
let len = str.chars().count() + SKIP_BRACKETS; let len = str.len() + SKIP_BRACKETS;
let script = Script::from(str.as_str()); let script = Script::from(str.as_str());
(DynamicStringSegment::Script(script), len) (DynamicStringSegment::Script(script), len)
@@ -160,7 +161,7 @@ fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
.take_while(|&c| !c.is_whitespace()) .take_while(|&c| !c.is_whitespace())
.collect::<String>(); .collect::<String>();
let len = str.chars().count() + SKIP_HASH; let len = str.len() + SKIP_HASH;
let value = str.into(); let value = str.into();
(DynamicStringSegment::Variable(value), len) (DynamicStringSegment::Variable(value), len)
@@ -173,16 +174,15 @@ fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
.map(|w| w[0]) .map(|w| w[0])
.collect::<String>(); .collect::<String>();
let mut char_count = str.chars().count();
// if segment is at end of string, last char gets missed above due to uneven window. // if segment is at end of string, last char gets missed above due to uneven window.
if chars.len() == char_count + 1 { if chars.len() == str.len() + 1 {
let remaining_char = *chars.get(char_count).expect("Failed to find last char"); let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
str.push(remaining_char); str.push(remaining_char);
char_count += 1;
} }
(DynamicStringSegment::Static(str), char_count) let len = str.len();
(DynamicStringSegment::Static(str), len)
} }
#[cfg(test)] #[cfg(test)]

View File

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

View File

@@ -1,43 +0,0 @@
use crate::popup::Popup;
use crate::write_lock;
use std::collections::HashMap;
use std::sync::{Arc, RwLock, RwLockWriteGuard};
/// Global application state shared across all bars.
///
/// Data that needs to be accessed from anywhere
/// that is not otherwise accessible should be placed on here.
#[derive(Debug)]
pub struct GlobalState {
popups: HashMap<Box<str>, Arc<RwLock<Popup>>>,
}
impl GlobalState {
pub(crate) fn new() -> Self {
Self {
popups: HashMap::new(),
}
}
pub fn popups(&self) -> &HashMap<Box<str>, Arc<RwLock<Popup>>> {
&self.popups
}
pub fn popups_mut(&mut self) -> &mut HashMap<Box<str>, Arc<RwLock<Popup>>> {
&mut self.popups
}
pub fn with_popup_mut<F, T>(&self, monitor_name: &str, f: F) -> Option<T>
where
F: FnOnce(RwLockWriteGuard<Popup>) -> T,
{
let popup = self.popups().get(monitor_name);
if let Some(popup) = popup {
let popup = write_lock!(popup);
Some(f(popup))
} else {
None
}
}
}

View File

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

View File

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

View File

@@ -41,44 +41,23 @@ impl<'a> ImageProvider<'a> {
/// ///
/// Note this checks that icons exist in theme, or files exist on disk /// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed. /// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> { pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?; let location = Self::get_location(input, theme, size)?;
Some(Self { location, size }) Some(Self { location, size })
} }
/// Returns true if the input starts with a prefix /// Returns true if the input starts with a prefix
/// that is supported by the parser /// that is supported by the parser
/// (ie the parser would not fallback to checking the input). /// (ie the parser would not fallback to checking the input).
#[cfg(any(feature = "music", feature = "workspaces"))]
pub fn is_definitely_image_input(input: &str) -> bool { pub fn is_definitely_image_input(input: &str) -> bool {
input.starts_with("icon:") input.starts_with("icon:")
|| input.starts_with("file://") || input.starts_with("file://")
|| input.starts_with("http://") || input.starts_with("http://")
|| input.starts_with("https://") || input.starts_with("https://")
|| input.starts_with('/')
} }
fn get_location( fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Option<ImageLocation<'a>> {
input: &str,
theme: &'a IconTheme,
size: i32,
use_fallback: bool,
recurse_depth: usize,
) -> Option<ImageLocation<'a>> {
macro_rules! fallback {
() => {
if use_fallback {
Some(Self::get_fallback_icon(theme))
} else {
None
}
};
}
const MAX_RECURSE_DEPTH: usize = 2;
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
let (input_type, input_name) = input let (input_type, input_name) = input
.split_once(':') .split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n)); .map_or((None, input), |(t, n)| (Some(t), n));
@@ -113,26 +92,21 @@ impl<'a> ImageProvider<'a> {
Report::msg(format!("Unsupported image type: {input_type}")) Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available") .note("You may need to recompile with support if available")
); );
fallback!() None
} }
None if PathBuf::from(input_name).is_file() => { None if PathBuf::from(input_name).is_file() => {
Some(ImageLocation::Local(PathBuf::from(input_name))) Some(ImageLocation::Local(PathBuf::from(input_name)))
} }
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(), None => {
None if should_parse_desktop_file => { if let Some(location) = get_desktop_icon_name(input_name)
if let Some(location) = get_desktop_icon_name(input_name).map(|input| { .map(|input| Self::get_location(&input, theme, size))
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1) {
}) {
location location
} else { } else {
warn!("Failed to find image: {input}"); warn!("Failed to find image: {input}");
fallback!() None
} }
} }
None => {
warn!("Failed to find image: {input}");
fallback!()
}
} }
} }
@@ -274,11 +248,4 @@ impl<'a> ImageProvider<'a> {
))) )))
} }
} }
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
ImageLocation::Icon {
name: "dialog-question-symbolic".to_string(),
theme,
}
}
} }

View File

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

View File

@@ -3,25 +3,21 @@ pub mod commands;
pub mod responses; pub mod responses;
mod server; mod server;
use std::cell::RefCell; use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use tracing::warn; use tracing::warn;
use crate::GlobalState;
pub use commands::Command; pub use commands::Command;
pub use responses::Response; pub use responses::Response;
#[derive(Debug)] #[derive(Debug)]
pub struct Ipc { pub struct Ipc {
path: PathBuf, path: PathBuf,
global_state: Rc<RefCell<GlobalState>>,
} }
impl Ipc { impl Ipc {
/// Creates a new IPC instance. /// Creates a new IPC instance.
/// This can be used as both a server and client. /// This can be used as both a server and client.
pub fn new(global_state: Rc<RefCell<GlobalState>>) -> Self { pub fn new() -> Self {
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR") let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from) .map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
.join("ironbar-ipc.sock"); .join("ironbar-ipc.sock");
@@ -32,11 +28,6 @@ impl Ipc {
Self { Self {
path: ipc_socket_file, path: ipc_socket_file,
global_state,
} }
} }
pub fn path(&self) -> &Path {
self.path.as_path()
}
} }

View File

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

View File

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

View File

@@ -100,7 +100,7 @@ macro_rules! write_lock {
#[macro_export] #[macro_export]
macro_rules! arc_mut { macro_rules! arc_mut {
($val:expr) => { ($val:expr) => {
std::sync::Arc::new(std::sync::Mutex::new($val)) std::sync::Arc::new(std::Sync::Mutex::new($val))
}; };
} }

View File

@@ -1,35 +1,5 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use std::cell::{Cell, RefCell};
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
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 tokio::runtime::Handle;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader;
use clients::wayland;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::error::ExitCode;
use crate::global_state::GlobalState;
use crate::style::load_css;
mod bar; mod bar;
mod bridge_channel; mod bridge_channel;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@@ -39,7 +9,6 @@ mod config;
mod desktop_file; mod desktop_file;
mod dynamic_value; mod dynamic_value;
mod error; mod error;
mod global_state;
mod gtk_helpers; mod gtk_helpers;
mod image; mod image;
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
@@ -54,6 +23,33 @@ mod script;
mod style; mod style;
mod unique_id; mod unique_id;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use std::cell::Cell;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use tokio::runtime::Handle;
use tokio::task::{block_in_place, spawn_blocking};
use crate::error::ExitCode;
use clients::wayland;
use tracing::{debug, error, info};
use universal_config::ConfigLoader;
const GTK_APP_ID: &str = "dev.jstanger.ironbar"; const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -61,34 +57,32 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
async fn main() { async fn main() {
let _guard = logging::install_logging(); let _guard = logging::install_logging();
let global_state = Rc::new(RefCell::new(GlobalState::new()));
cfg_if! { cfg_if! {
if #[cfg(feature = "cli")] { if #[cfg(feature = "cli")] {
run_with_args(global_state).await; run_with_args().await;
} else { } else {
start_ironbar(global_state); start_ironbar();
} }
} }
} }
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
async fn run_with_args(global_state: Rc<RefCell<GlobalState>>) { async fn run_with_args() {
let args = cli::Args::parse(); let args = cli::Args::parse();
match args.command { match args.command {
Some(command) => { Some(command) => {
let ipc = ipc::Ipc::new(global_state); let ipc = ipc::Ipc::new();
match ipc.send(command).await { match ipc.send(command).await {
Ok(res) => cli::handle_response(res), Ok(res) => cli::handle_response(res),
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}; };
} }
None => start_ironbar(global_state), None => start_ironbar(),
} }
} }
fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) { fn start_ironbar() {
info!("Ironbar version {}", VERSION); info!("Ironbar version {}", VERSION);
info!("Starting application"); info!("Starting application");
@@ -107,12 +101,51 @@ fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
cfg_if! { cfg_if! {
if #[cfg(feature = "ipc")] { if #[cfg(feature = "ipc")] {
let ipc = ipc::Ipc::new(global_state.clone()); let ipc = ipc::Ipc::new();
ipc.start(app); ipc.start();
} }
} }
load_interface(app, &global_state); let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
);
let config_res = env::var("IRONBAR_CONFIG").map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
);
let mut config: Config = match config_res {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
exit(ExitCode::Config as i32)
}
};
debug!("Loaded config file");
#[cfg(feature = "ipc")]
if let Some(ironvars) = config.ironvar_defaults.take() {
let variable_manager = ironvar::get_variable_manager();
for (k, v) in ironvars {
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
tracing::warn!("Ignoring invalid ironvar: '{k}'");
}
}
}
if let Err(err) = create_bars(app, &display, &config) {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
debug!("Created bars");
let style_path = env::var("IRONBAR_CSS").ok().map_or_else( let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| { || {
@@ -134,15 +167,13 @@ fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
#[cfg(feature = "ipc")]
let ipc_path = ipc.path().to_path_buf();
spawn_blocking(move || { spawn_blocking(move || {
rx.recv().expect("to receive from channel"); rx.recv().expect("to receive from channel");
info!("Shutting down"); info!("Shutting down");
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
ipc::Ipc::shutdown(ipc_path); ipc.shutdown();
exit(0); exit(0);
}); });
@@ -156,58 +187,8 @@ fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
app.run_with_args(&Vec::<&str>::new()); app.run_with_args(&Vec::<&str>::new());
} }
/// Loads the Ironbar config and interface.
pub fn load_interface(app: &Application, global_state: &Rc<RefCell<GlobalState>>) {
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(ExitCode::GtkDisplay as i32)
},
|display| display,
);
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
debug!("Loaded config file");
#[cfg(feature = "ipc")]
if let Some(ironvars) = config.ironvar_defaults.take() {
let variable_manager = ironvar::get_variable_manager();
for (k, v) in ironvars {
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
warn!("Ignoring invalid ironvar: '{k}'");
}
}
}
if let Err(err) = create_bars(app, &display, &config, global_state) {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
debug!("Created bars");
}
/// Creates each of the bars across each of the (configured) outputs. /// Creates each of the bars across each of the (configured) outputs.
fn create_bars( fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
app: &Application,
display: &Display,
config: &Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> {
let wl = wayland::get_client(); let wl = wayland::get_client();
let outputs = lock!(wl).get_outputs(); let outputs = lock!(wl).get_outputs();
@@ -229,19 +210,19 @@ fn create_bars(
config.monitors.as_ref().map_or_else( config.monitors.as_ref().map_or_else(
|| { || {
info!("Creating bar on '{}'", monitor_name); info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone(), global_state) create_bar(app, &monitor, monitor_name, config.clone())
}, },
|config| { |config| {
let config = config.get(monitor_name); let config = config.get(monitor_name);
match &config { match &config {
Some(MonitorConfig::Single(config)) => { Some(MonitorConfig::Single(config)) => {
info!("Creating bar on '{}'", monitor_name); info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone(), global_state) create_bar(app, &monitor, monitor_name, config.clone())
} }
Some(MonitorConfig::Multiple(configs)) => { Some(MonitorConfig::Multiple(configs)) => {
for config in configs { for config in configs {
info!("Creating bar on '{}'", monitor_name); info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone(), global_state)?; create_bar(app, &monitor, monitor_name, config.clone())?;
} }
Ok(()) Ok(())

View File

@@ -2,9 +2,8 @@ use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue}; use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, use crate::popup::Popup;
};
use crate::try_send; use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf; use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream}; use gtk::gio::{Cancellable, MemoryInputStream};
@@ -125,26 +124,25 @@ impl Module<Button> for ClipboardModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<Button>> { ) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size); let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
button.style_context().add_class("btn"); button.style_context().add_class("btn");
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!( let pos = Popup::widget_geometry(button, position.get_orientation());
context.tx, try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
}); });
// we need to bind to the receiver as the channel does not open // we need to bind to the receiver as the channel does not open
// until the popup is first opened. // until the popup is first opened.
context.widget_rx.attach(None, |_| Continue(true)); context.widget_rx.attach(None, |_| Continue(true));
let popup = self Ok(ModuleWidget {
.into_popup(context.controller_tx, context.popup_rx, info) widget: button,
.into_popup_parts(vec![&button]); popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
Ok(ModuleParts::new(button, popup))
} }
fn into_popup( fn into_popup(

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,10 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_value::dynamic_string;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Label; use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget { pub struct LabelWidget {
name: Option<String>, name: Option<String>,

View File

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

View File

@@ -1,16 +1,14 @@
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, send};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::ProgressBar; use gtk::ProgressBar;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn; use tokio::spawn;
use tracing::error; use tracing::error;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget { pub struct ProgressWidget {
name: Option<String>, name: Option<String>,

View File

@@ -1,18 +1,16 @@
use std::cell::Cell; use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use std::ops::Neg; 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::prelude::*;
use gtk::Scale; use gtk::Scale;
use serde::Deserialize; use serde::Deserialize;
use std::cell::Cell;
use std::ops::Neg;
use tokio::spawn; use tokio::spawn;
use tracing::error; use tracing::error;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget { pub struct SliderWidget {
name: Option<String>, name: Option<String>,
@@ -80,7 +78,7 @@ impl CustomWidget for SliderWidget {
Inhibit(false) Inhibit(false)
}); });
scale.connect_change_value(move |_, _, val| { scale.connect_change_value(move |scale, _, val| {
// GTK will send values outside min/max range // GTK will send values outside min/max range
let val = val.clamp(min, max); let val = val.clamp(min, max);
@@ -90,7 +88,7 @@ impl CustomWidget for SliderWidget {
ExecEvent { ExecEvent {
cmd: on_change.clone(), cmd: on_change.clone(),
args: Some(vec![val.to_string()]), args: Some(vec![val.to_string()]),
id: usize::MAX // ignored geometry: Popup::widget_geometry(scale, context.bar_orientation),
} }
); );

View File

@@ -1,8 +1,8 @@
use crate::clients::wayland::{self, ToplevelEvent}; use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::add_class;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, send_async, try_send}; use crate::{lock, send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use glib::Continue; use glib::Continue;
@@ -32,18 +32,6 @@ pub struct FocusedModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
impl Default for FocusedModule {
fn default() -> Self {
Self {
show_icon: crate::config::default_true(),
show_title: crate::config::default_true(),
icon_size: default_icon_size(),
truncate: None,
common: Some(CommonConfig::default()),
}
}
}
const fn default_icon_size() -> i32 { const fn default_icon_size() -> i32 {
32 32
} }
@@ -104,19 +92,19 @@ impl Module<gtk::Box> for FocusedModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5); let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::new(); let icon = gtk::Image::new();
if self.show_icon { if self.show_icon {
icon.add_class("icon"); add_class(&icon, "icon");
container.add(&icon); container.add(&icon);
} }
let label = Label::new(None); let label = Label::new(None);
label.add_class("label"); add_class(&label, "label");
if let Some(truncate) = self.truncate { if let Some(truncate) = self.truncate {
truncate.truncate_label(&label); truncate.truncate_label(&label);
@@ -128,7 +116,7 @@ impl Module<gtk::Box> for FocusedModule {
let icon_theme = icon_theme.clone(); let icon_theme = icon_theme.clone();
context.widget_rx.attach(None, move |(name, id)| { context.widget_rx.attach(None, move |(name, id)| {
if self.show_icon { if self.show_icon {
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size) match ImageProvider::parse(&id, &icon_theme, self.icon_size)
.map(|image| image.load_into_image(icon.clone())) .map(|image| image.load_into_image(icon.clone()))
{ {
Some(Ok(_)) => icon.show(), Some(Ok(_)) => icon.show(),
@@ -144,7 +132,7 @@ impl Module<gtk::Box> for FocusedModule {
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,6 +1,6 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string; use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::try_send; use crate::try_send;
use color_eyre::Result; use color_eyre::Result;
use glib::Continue; use glib::Continue;
@@ -17,15 +17,6 @@ pub struct LabelModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
impl LabelModule {
pub(crate) fn new(label: String) -> Self {
Self {
label,
common: Some(CommonConfig::default()),
}
}
}
impl Module<Label> for LabelModule { impl Module<Label> for LabelModule {
type SendMessage = String; type SendMessage = String;
type ReceiveMessage = (); type ReceiveMessage = ();
@@ -52,19 +43,18 @@ impl Module<Label> for LabelModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Result<ModuleParts<Label>> { ) -> Result<ModuleWidget<Label>> {
let label = Label::new(None); let label = Label::new(None);
label.set_use_markup(true);
{ {
let label = label.clone(); let label = label.clone();
context.widget_rx.attach(None, move |string| { context.widget_rx.attach(None, move |string| {
label.set_markup(&string); label.set_label(&string);
Continue(true) Continue(true)
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: label, widget: label,
popup: None, popup: None,
}) })

View File

@@ -1,14 +1,13 @@
use super::open_state::OpenState; use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle; use crate::clients::wayland::ToplevelHandle;
use crate::config::BarPosition;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent; use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send}; use crate::{read_lock, try_send};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme}; use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::RwLock; use std::sync::RwLock;
@@ -177,7 +176,7 @@ impl ItemButton {
item: &Item, item: &Item,
appearance: AppearanceOptions, appearance: AppearanceOptions,
icon_theme: &IconTheme, icon_theme: &IconTheme,
bar_position: BarPosition, orientation: Orientation,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>, tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>, controller_tx: &Sender<ItemEvent>,
) -> Self { ) -> Self {
@@ -192,7 +191,7 @@ impl ItemButton {
if appearance.show_icons { if appearance.show_icons {
let gtk_image = gtk::Image::new(); let gtk_image = gtk::Image::new();
let image = let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size); ImageProvider::parse(&item.app_id.clone(), icon_theme, appearance.icon_size);
if let Some(image) = image { if let Some(image) = image {
button.set_image(Some(&gtk_image)); button.set_image(Some(&gtk_image));
button.set_always_show_image(true); button.set_always_show_image(true);
@@ -250,9 +249,7 @@ impl ItemButton {
try_send!( try_send!(
tx, tx,
ModuleUpdateEvent::OpenPopupAt( ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
button.geometry(bar_position.get_orientation())
)
); );
} else { } else {
try_send!(tx, ModuleUpdateEvent::ClosePopup); try_send!(tx, ModuleUpdateEvent::ClosePopup);
@@ -262,31 +259,6 @@ impl ItemButton {
}); });
} }
{
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 < alloc.height() as f64,
BarPosition::Bottom => y > THRESHOLD,
BarPosition::Left => x + THRESHOLD < alloc.width() as f64,
BarPosition::Right => x > THRESHOLD,
};
if close {
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Inhibit(false)
});
}
button.show_all(); button.show_all();
Self { Self {

View File

@@ -7,10 +7,8 @@ use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file; use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions; use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext, use crate::{lock, send_async, try_send, write_lock};
};
use crate::{arc_mut, lock, send_async, try_send, write_lock};
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
@@ -18,7 +16,7 @@ use gtk::{Button, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::Deserialize; use serde::Deserialize;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::Arc; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -110,7 +108,7 @@ impl Module<gtk::Box> for LauncherModule {
.collect::<IndexMap<_, _>>() .collect::<IndexMap<_, _>>()
}); });
let items = arc_mut!(items); let items = Arc::new(Mutex::new(items));
let items2 = Arc::clone(&items); let items2 = Arc::clone(&items);
let tx2 = tx.clone(); let tx2 = tx.clone();
@@ -165,7 +163,6 @@ impl Module<gtk::Box> for LauncherModule {
match item { match item {
None => { None => {
let item: Item = handle.try_into()?; let item: Item = handle.try_into()?;
items.insert(info.app_id.clone(), item.clone()); items.insert(info.app_id.clone(), item.clone());
ItemOrWindow::Item(item) ItemOrWindow::Item(item)
@@ -316,7 +313,7 @@ impl Module<gtk::Box> for LauncherModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> crate::Result<ModuleParts<gtk::Box>> { ) -> crate::Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0); let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
@@ -334,7 +331,7 @@ impl Module<gtk::Box> for LauncherModule {
}; };
let show_names = self.show_names; let show_names = self.show_names;
let bar_position = info.bar_position; let orientation = info.bar_position.get_orientation();
let mut buttons = IndexMap::<String, ItemButton>::new(); let mut buttons = IndexMap::<String, ItemButton>::new();
@@ -350,7 +347,7 @@ impl Module<gtk::Box> for LauncherModule {
&item, &item,
appearance_options, appearance_options,
&icon_theme, &icon_theme,
bar_position, orientation,
&context.tx, &context.tx,
&controller_tx, &controller_tx,
); );
@@ -359,10 +356,9 @@ impl Module<gtk::Box> for LauncherModule {
buttons.insert(item.app_id, button); buttons.insert(item.app_id, button);
} }
} }
LauncherUpdate::AddWindow(app_id, win) => { LauncherUpdate::AddWindow(app_id, _) => {
if let Some(button) = buttons.get(&app_id) { if let Some(button) = buttons.get(&app_id) {
button.set_open(true); button.set_open(true);
button.set_focused(win.open_state.is_focused());
let mut menu_state = write_lock!(button.menu_state); let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows += 1; menu_state.num_windows += 1;
@@ -383,12 +379,8 @@ impl Module<gtk::Box> for LauncherModule {
} }
} }
} }
LauncherUpdate::RemoveWindow(app_id, win_id) => { LauncherUpdate::RemoveWindow(app_id, _) => {
debug!("Removing window {win_id} with id {app_id}");
if let Some(button) = buttons.get(&app_id) { if let Some(button) = buttons.get(&app_id) {
button.set_focused(false);
let mut menu_state = write_lock!(button.menu_state); let mut menu_state = write_lock!(button.menu_state);
menu_state.num_windows -= 1; menu_state.num_windows -= 1;
} }
@@ -416,11 +408,8 @@ impl Module<gtk::Box> for LauncherModule {
}); });
} }
let popup = self let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
.into_popup(context.controller_tx, context.popup_rx, info) Ok(ModuleWidget {
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
widget: container, widget: container,
popup, popup,
}) })

View File

@@ -1,19 +1,3 @@
use std::sync::{Arc, RwLock};
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::mpsc;
use tracing::debug;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{send, write_lock};
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
/// Displays the current date and time. /// Displays the current date and time.
@@ -40,6 +24,19 @@ pub mod upower;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub mod workspaces; pub mod workspaces;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::popup::{Popup, WidgetGeometry};
use crate::{read_lock, send, write_lock};
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Clone)] #[derive(Clone)]
pub enum ModuleLocation { pub enum ModuleLocation {
Left, Left,
@@ -57,15 +54,13 @@ pub struct ModuleInfo<'a> {
#[derive(Debug)] #[derive(Debug)]
pub enum ModuleUpdateEvent<T> { pub enum ModuleUpdateEvent<T> {
/// Sends an update to the module UI. /// Sends an update to the module UI
Update(T), Update(T),
/// Toggles the open state of the popup. /// Toggles the open state of the popup.
/// Takes the button ID. TogglePopup(WidgetGeometry),
TogglePopup(usize),
/// Force sets the popup open. /// Force sets the popup open.
/// Takes the button ID. /// Takes the button X position and width.
OpenPopup(usize), OpenPopup(WidgetGeometry),
OpenPopupAt(WidgetGeometry),
/// Force sets the popup closed. /// Force sets the popup closed.
ClosePopup, ClosePopup,
} }
@@ -78,62 +73,9 @@ pub struct WidgetContext<TSend, TReceive> {
pub popup_rx: glib::Receiver<TSend>, pub popup_rx: glib::Receiver<TSend>,
} }
pub struct ModuleParts<W: IsA<Widget>> { pub struct ModuleWidget<W: IsA<Widget>> {
pub widget: W, pub widget: W,
pub popup: Option<ModulePopupParts>, pub popup: Option<gtk::Box>,
}
impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
}
#[derive(Debug, Clone)]
pub struct ModulePopupParts {
/// The popup container, with all its contents
pub container: gtk::Box,
/// An array of buttons which can be used for opening the popup.
/// For most modules, this will only be a single button.
/// For some advanced modules, such as `Launcher`, this is all item buttons.
pub buttons: Vec<Button>,
}
pub trait ModulePopup {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
}
impl ModulePopup for Option<gtk::Box> {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
}
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
self.map(|container| ModulePopupParts { container, buttons })
}
}
pub trait PopupButton {
fn try_popup_id(&self) -> Option<usize>;
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button, if there is one.
/// Will return `None` if this is not a popup button.
fn try_popup_id(&self) -> Option<usize> {
self.get_tag("popup-id").copied()
}
/// Gets the popup ID associated with this button.
/// This should only be called on buttons which are known to be associated with popups.
///
/// # Panics
/// Will panic if an ID has not been set.
fn popup_id(&self) -> usize {
self.try_popup_id().expect("id to exist")
}
} }
pub trait Module<W> pub trait Module<W>
@@ -156,7 +98,7 @@ where
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<W>>; ) -> Result<ModuleWidget<W>>;
fn into_popup( fn into_popup(
self, self,
@@ -176,10 +118,9 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>( pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule, module: TModule,
id: usize, id: usize,
name: Option<String>,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>, popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleParts<TWidget>> ) -> Result<ModuleWidget<TWidget>>
where where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>, TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>, TWidget: IsA<Widget>,
@@ -201,45 +142,29 @@ where
controller_tx: ui_tx, controller_tx: ui_tx,
}; };
let module_name = TModule::name(); let name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
let module_parts = module.into_widget(context, info)?; let module_parts = module.into_widget(context, info)?;
module_parts.widget.style_context().add_class(module_name); module_parts.widget.style_context().add_class(name);
let has_popup = if let Some(popup_content) = module_parts.popup.clone() { let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() {
popup_content popup_content
.container
.style_context() .style_context()
.add_class(&format!("popup-{module_name}")); .add_class(&format!("popup-{name}"));
register_popup_content(popup, id, instance_name, popup_content); register_popup_content(popup, id, popup_content);
true has_popup = true;
} else { }
false
};
setup_receiver( setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
channel,
w_tx,
p_tx,
popup.clone(),
module_name,
id,
has_popup,
);
Ok(module_parts) Ok(module_parts)
} }
/// Registers the popup content with the popup. /// Registers the popup content with the popup.
fn register_popup_content( fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
popup: &Arc<RwLock<Popup>>, write_lock!(popup).register_content(id, popup_content);
id: usize,
name: String,
popup_content: ModulePopupParts,
) {
write_lock!(popup).register_content(id, name, popup_content);
} }
/// Sets up the bridge channel receiver /// Sets up the bridge channel receiver
@@ -271,51 +196,40 @@ fn setup_receiver<TSend>(
send!(w_tx, update); send!(w_tx, update);
} }
ModuleUpdateEvent::TogglePopup(button_id) => { ModuleUpdateEvent::TogglePopup(geometry) => {
debug!("Toggling popup for {} [#{}]", name, id); debug!("Toggling popup for {} [#{}]", name, id);
let mut popup = write_lock!(popup); let popup = read_lock!(popup);
if popup.is_visible() { if popup.is_visible() {
popup.hide(); popup.hide();
} else { } else {
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened { if !has_popup_opened {
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
has_popup_opened = true; has_popup_opened = true;
} }
} }
} }
ModuleUpdateEvent::OpenPopup(button_id) => { ModuleUpdateEvent::OpenPopup(geometry) => {
debug!("Opening popup for {} [#{}]", name, id); debug!("Opening popup for {} [#{}]", name, id);
let mut popup = write_lock!(popup); let popup = read_lock!(popup);
popup.hide(); popup.hide();
popup.show(id, button_id); popup.show_content(id);
popup.show(geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened { if !has_popup_opened {
popup.show(id, button_id); popup.show_content(id);
has_popup_opened = true; popup.show(geometry);
}
}
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = write_lock!(popup);
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_at(id, geometry);
has_popup_opened = true; has_popup_opened = true;
} }
} }
ModuleUpdateEvent::ClosePopup => { ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id); debug!("Closing popup for {} [#{}]", name, id);
let mut popup = write_lock!(popup); let popup = read_lock!(popup);
popup.hide(); popup.hide();
} }
} }
@@ -325,14 +239,14 @@ fn setup_receiver<TSend>(
} }
pub fn set_widget_identifiers<TWidget: IsA<Widget>>( pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleParts<TWidget>, widget_parts: &ModuleWidget<TWidget>,
common: &CommonConfig, common: &CommonConfig,
) { ) {
if let Some(ref name) = common.name { if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name); widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup { if let Some(ref popup) = widget_parts.popup {
popup.container.set_widget_name(&format!("popup-{name}")); popup.set_widget_name(&format!("popup-{name}"));
} }
} }
@@ -344,10 +258,7 @@ pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
if let Some(ref popup) = widget_parts.popup { if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') { for part in class.split(' ') {
popup popup.style_context().add_class(&format!("popup-{part}"));
.container
.style_context()
.add_class(&format!("popup-{part}"));
} }
} }
} }

View File

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

View File

@@ -1,5 +1,5 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode}; use crate::script::{OutputStream, Script, ScriptMode};
use crate::try_send; use crate::try_send;
use color_eyre::{Help, Report, Result}; use color_eyre::{Help, Report, Result};
@@ -83,7 +83,7 @@ impl Module<Label> for ScriptModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<Label>> { ) -> Result<ModuleWidget<Label>> {
let label = Label::builder().use_markup(true).build(); let label = Label::builder().use_markup(true).build();
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
@@ -95,7 +95,7 @@ impl Module<Label> for ScriptModule {
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: label, widget: label,
popup: None, popup: None,
}) })

View File

@@ -1,6 +1,6 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt; use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::send_async; use crate::send_async;
use color_eyre::Result; use color_eyre::Result;
use gtk::prelude::*; use gtk::prelude::*;
@@ -186,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?; let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10); let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
@@ -196,7 +196,7 @@ impl Module<gtk::Box> for SysInfoModule {
for format in &self.format { for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build(); let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item"); add_class(&label, "item");
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
container.add(&label); container.add(&label);
@@ -220,7 +220,7 @@ impl Module<gtk::Box> for SysInfoModule {
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,6 +1,6 @@
use crate::clients::system_tray::get_tray_event_client; use crate::clients::system_tray::get_tray_event_client;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, try_send}; use crate::{await_sync, try_send};
use color_eyre::Result; use color_eyre::Result;
use gtk::gdk_pixbuf::{Colorspace, InterpType}; use gtk::gdk_pixbuf::{Colorspace, InterpType};
@@ -11,9 +11,9 @@ use gtk::{
}; };
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType}; use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
use system_tray::message::tray::StatusNotifierItem; use stray::message::tray::StatusNotifierItem;
use system_tray::message::{NotifierItemCommand, NotifierItemMessage}; use stray::message::{NotifierItemCommand, NotifierItemMessage};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
@@ -172,7 +172,7 @@ impl Module<MenuBar> for TrayModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Result<ModuleParts<MenuBar>> { ) -> Result<ModuleWidget<MenuBar>> {
let container = MenuBar::new(); let container = MenuBar::new();
{ {
@@ -238,7 +238,7 @@ impl Module<MenuBar> for TrayModule {
}); });
}; };
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,3 +1,10 @@
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{await_sync, error, send_async, try_send};
use color_eyre::Result; use color_eyre::Result;
use futures_lite::stream::StreamExt; use futures_lite::stream::StreamExt;
use gtk::{prelude::*, Button}; use gtk::{prelude::*, Button};
@@ -8,16 +15,6 @@ use tokio::sync::mpsc::{Receiver, Sender};
use upower_dbus::BatteryState; use upower_dbus::BatteryState;
use zbus; use zbus;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{await_sync, error, send_async, try_send};
const DAY: i64 = 24 * 60 * 60; const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60; const HOUR: i64 = 60 * 60;
const MINUTE: i64 = 60; const MINUTE: i64 = 60;
@@ -153,31 +150,32 @@ impl Module<gtk::Button> for UpowerModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<Button>> { ) -> Result<ModuleWidget<Button>> {
let icon_theme = info.icon_theme.clone(); let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new(); let icon = gtk::Image::new();
icon.add_class("icon"); add_class(&icon, "icon");
let label = Label::builder() let label = Label::builder()
.label(&self.format) .label(&self.format)
.use_markup(true) .use_markup(true)
.build(); .build();
label.add_class("label"); add_class(&label, "label");
let container = gtk::Box::new(Orientation::Horizontal, 5); let container = gtk::Box::new(Orientation::Horizontal, 5);
container.add_class("contents"); add_class(&container, "contents");
let button = Button::new(); let button = Button::new();
button.add_class("button"); add_class(&button, "button");
container.add(&icon); container.add(&icon);
container.add(&label); container.add(&label);
button.add(&container); button.add(&container);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
try_send!( try_send!(
context.tx, context.tx,
ModuleUpdateEvent::TogglePopup(button.popup_id()) ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
); );
}); });
@@ -189,17 +187,18 @@ impl Module<gtk::Button> for UpowerModule {
.attach(None, move |properties: UpowerProperties| { .attach(None, move |properties: UpowerProperties| {
let format = format.replace("{percentage}", &properties.percentage.to_string()); let format = format.replace("{percentage}", &properties.percentage.to_string());
let icon_name = String::from("icon:") + &properties.icon_name; let icon_name = String::from("icon:") + &properties.icon_name;
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size) ImageProvider::parse(&icon_name, &icon_theme, self.icon_size)
.map(|provider| provider.load_into_image(icon.clone())); .map(|provider| provider.load_into_image(icon.clone()));
label.set_markup(format.as_ref()); label.set_markup(format.as_ref());
Continue(true) Continue(true)
}); });
let popup = self let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup)) Ok(ModuleWidget {
widget: button,
popup,
})
} }
fn into_popup( fn into_popup(
@@ -216,7 +215,7 @@ impl Module<gtk::Button> for UpowerModule {
.build(); .build();
let label = Label::new(None); let label = Label::new(None);
label.add_class("upower-details"); add_class(&label, "upower-details");
container.add(&label); container.add(&label);
rx.attach(None, move |properties| { rx.attach(None, move |properties| {

View File

@@ -1,14 +1,14 @@
use crate::clients::compositor::{Compositor, Workspace, WorkspaceUpdate}; use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::image::new_icon_button; use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send}; use crate::{send_async, try_send};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme}; use gtk::{Button, IconTheme};
use serde::Deserialize; use serde::Deserialize;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::trace; use tracing::trace;
@@ -29,32 +29,11 @@ impl Default for SortOrder {
} }
} }
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum Favorites {
ByMonitor(HashMap<String, Vec<String>>),
Global(Vec<String>),
}
impl Default for Favorites {
fn default() -> Self {
Self::Global(vec![])
}
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule { pub struct WorkspacesModule {
/// Map of actual workspace names to custom names. /// Map of actual workspace names to custom names.
name_map: Option<HashMap<String, String>>, name_map: Option<HashMap<String, String>>,
/// Array of always shown workspaces, and what monitor to show on
#[serde(default)]
favorites: Favorites,
/// List of workspace names to never show
#[serde(default)]
hidden: Vec<String>,
/// Whether to display buttons for all monitors. /// Whether to display buttons for all monitors.
#[serde(default = "crate::config::default_false")] #[serde(default = "crate::config::default_false")]
all_monitors: bool, all_monitors: bool,
@@ -77,7 +56,6 @@ const fn default_icon_size() -> i32 {
fn create_button( fn create_button(
name: &str, name: &str,
focused: bool, focused: bool,
inactive: bool,
name_map: &HashMap<String, String>, name_map: &HashMap<String, String>,
icon_theme: &IconTheme, icon_theme: &IconTheme,
icon_size: i32, icon_size: i32,
@@ -93,8 +71,6 @@ fn create_button(
if focused { if focused {
style_context.add_class("focused"); style_context.add_class("focused");
} else if inactive {
style_context.add_class("inactive");
} }
{ {
@@ -129,13 +105,6 @@ fn reorder_workspaces(container: &gtk::Box) {
} }
} }
impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.focused || !self.hidden.contains(&work.name))
&& (self.all_monitors || output == &work.monitor)
}
}
impl Module<gtk::Box> for WorkspacesModule { impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate; type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String; type ReceiveMessage = String;
@@ -185,12 +154,10 @@ impl Module<gtk::Box> for WorkspacesModule {
self, self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>, context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.get_orientation(), 0); let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let name_map = self.name_map.clone().unwrap_or_default(); let name_map = self.name_map.unwrap_or_default();
let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![];
let mut button_map: HashMap<String, Button> = HashMap::new(); let mut button_map: HashMap<String, Button> = HashMap::new();
@@ -209,49 +176,19 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Init(workspaces) => { WorkspaceUpdate::Init(workspaces) => {
if !has_initialized { if !has_initialized {
trace!("Creating workspace buttons"); 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(); button_map.insert(workspace.name, item);
let mut add_workspace = |name: &str, focused: bool| {
let item = create_button(
name,
focused,
false,
&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.focused);
added.insert(workspace.name.to_string());
}
}
let mut add_favourites = |names: &Vec<String>| {
for name in names {
if !added.contains(name) {
add_workspace(name, false);
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);
}
} }
} }
@@ -275,17 +212,11 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
WorkspaceUpdate::Add(workspace) => { WorkspaceUpdate::Add(workspace) => {
if fav_names.contains(&workspace.name) { if self.all_monitors || workspace.monitor == output_name {
let btn = button_map.get(&workspace.name);
if let Some(btn) = btn {
btn.style_context().remove_class("inactive");
}
} else if self.show_workspace_check(&output_name, &workspace) {
let name = workspace.name; let name = workspace.name;
let item = create_button( let item = create_button(
&name, &name,
workspace.focused, workspace.focused,
false,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size, icon_size,
@@ -305,13 +236,12 @@ impl Module<gtk::Box> for WorkspacesModule {
} }
} }
WorkspaceUpdate::Move(workspace) => { WorkspaceUpdate::Move(workspace) => {
if !self.hidden.contains(&workspace.name) && !self.all_monitors { if !self.all_monitors {
if workspace.monitor == output_name { if workspace.monitor == output_name {
let name = workspace.name; let name = workspace.name;
let item = create_button( let item = create_button(
&name, &name,
workspace.focused, workspace.focused,
false,
&name_map, &name_map,
&icon_theme, &icon_theme,
icon_size, icon_size,
@@ -337,11 +267,7 @@ impl Module<gtk::Box> for WorkspacesModule {
WorkspaceUpdate::Remove(workspace) => { WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace); let button = button_map.get(&workspace);
if let Some(item) = button { if let Some(item) = button {
if fav_names.contains(&workspace) { container.remove(item);
item.style_context().add_class("inactive");
} else {
container.remove(item);
}
} }
} }
WorkspaceUpdate::Update(_) => {} WorkspaceUpdate::Update(_) => {}
@@ -351,7 +277,7 @@ impl Module<gtk::Box> for WorkspacesModule {
}); });
} }
Ok(ModuleParts { Ok(ModuleWidget {
widget: container, widget: container,
popup: None, popup: None,
}) })

View File

@@ -1,22 +1,18 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::config::BarPosition;
use crate::modules::ModuleInfo;
use gtk::gdk::Monitor; use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation}; use gtk::{ApplicationWindow, Orientation};
use tracing::debug; use tracing::debug;
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::unique_id::get_unique_usize;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Popup { pub struct Popup {
pub window: ApplicationWindow, pub window: ApplicationWindow,
pub cache: HashMap<usize, (String, ModulePopupParts)>, pub cache: HashMap<usize, gtk::Box>,
monitor: Monitor, monitor: Monitor,
pos: BarPosition, pos: BarPosition,
current_widget: Option<usize>,
} }
impl Popup { impl Popup {
@@ -32,7 +28,6 @@ impl Popup {
.build(); .build();
gtk_layer_shell::init_for_window(&win); gtk_layer_shell::init_for_window(&win);
gtk_layer_shell::set_monitor(&win, module_info.monitor);
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay); gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME")); gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
@@ -113,54 +108,20 @@ impl Popup {
cache: HashMap::new(), cache: HashMap::new(),
monitor: module_info.monitor.clone(), monitor: module_info.monitor.clone(),
pos, pos,
current_widget: None,
} }
} }
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) { pub fn register_content(&mut self, key: usize, content: gtk::Box) {
debug!("Registered popup content for #{}", key); debug!("Registered popup content for #{}", key);
self.cache.insert(key, content);
for button in &content.buttons {
let id = get_unique_usize();
button.set_tag("popup-id", id);
}
self.cache.insert(key, (name, content));
} }
pub fn show(&mut self, widget_id: usize, button_id: usize) { pub fn show_content(&self, key: usize) {
self.clear_window(); self.clear_window();
if let Some((_name, content)) = self.cache.get(&widget_id) { if let Some(content) = self.cache.get(&key) {
self.current_widget = Some(widget_id); content.style_context().add_class("popup");
self.window.add(content);
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some((_name, 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);
} }
} }
@@ -171,9 +132,14 @@ impl Popup {
} }
} }
/// Shows the popup
pub fn show(&self, geometry: WidgetGeometry) {
self.window.show();
self.set_pos(geometry);
}
/// Hides the popover /// Hides the popover
pub fn hide(&mut self) { pub fn hide(&self) {
self.current_widget = None;
self.window.hide(); self.window.hide();
} }
@@ -182,10 +148,6 @@ impl Popup {
self.window.is_visible() self.window.is_visible()
} }
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
}
/// Sets the popup's X/Y position relative to the left or border of the screen /// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation). /// (depending on orientation).
fn set_pos(&self, geometry: WidgetGeometry) { fn set_pos(&self, geometry: WidgetGeometry) {
@@ -225,4 +187,48 @@ impl Popup {
gtk_layer_shell::set_margin(&self.window, 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,
} }