mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 10:41:03 +02:00
Merge pull request #199 from JakeStanger/feat/cli-ipc
IPC, CLI and Dynamic Variables
This commit is contained in:
commit
4a9410abac
37 changed files with 1367 additions and 311 deletions
122
Cargo.lock
generated
122
Cargo.lock
generated
|
@ -52,6 +52,55 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is-terminal",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.71"
|
version = "1.0.71"
|
||||||
|
@ -395,6 +444,48 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.4.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote 1.0.28",
|
||||||
|
"syn 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color-eyre"
|
name = "color-eyre"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -422,6 +513,12 @@ dependencies = [
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -515,6 +612,16 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctrlc"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e"
|
||||||
|
dependencies = [
|
||||||
|
"nix 0.26.2",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.14.4"
|
version = "0.14.4"
|
||||||
|
@ -1475,7 +1582,9 @@ dependencies = [
|
||||||
"async_once",
|
"async_once",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"ctrlc",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -1492,6 +1601,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"smithay-client-toolkit",
|
"smithay-client-toolkit",
|
||||||
"stray",
|
"stray",
|
||||||
"strip-ansi-escapes",
|
"strip-ansi-escapes",
|
||||||
|
@ -1511,6 +1621,18 @@ dependencies = [
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi 0.3.1",
|
||||||
|
"io-lifetimes",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
17
Cargo.toml
17
Cargo.toml
|
@ -4,9 +4,12 @@ version = "0.13.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||||
|
repository = "https://github.com/jakestanger/ironbar"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
|
"cli",
|
||||||
|
"ipc",
|
||||||
"http",
|
"http",
|
||||||
"config+all",
|
"config+all",
|
||||||
"clipboard",
|
"clipboard",
|
||||||
|
@ -17,8 +20,11 @@ default = [
|
||||||
"upower",
|
"upower",
|
||||||
"workspaces+all"
|
"workspaces+all"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
cli = ["dep:clap", "ipc"]
|
||||||
|
ipc = ["dep:serde_json"]
|
||||||
|
|
||||||
http = ["dep:reqwest"]
|
http = ["dep:reqwest"]
|
||||||
upower = ["upower_dbus", "zbus", "futures-lite"]
|
|
||||||
|
|
||||||
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
|
"config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"]
|
||||||
"config+json" = ["universal-config/json"]
|
"config+json" = ["universal-config/json"]
|
||||||
|
@ -40,6 +46,8 @@ sys_info = ["sysinfo", "regex"]
|
||||||
|
|
||||||
tray = ["stray"]
|
tray = ["stray"]
|
||||||
|
|
||||||
|
upower = ["upower_dbus", "zbus", "futures-lite"]
|
||||||
|
|
||||||
workspaces = ["futures-util"]
|
workspaces = ["futures-util"]
|
||||||
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
"workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"]
|
||||||
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
"workspaces+sway" = ["workspaces", "swayipc-async"]
|
||||||
|
@ -67,11 +75,18 @@ 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 = ["calloop"] }
|
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] }
|
||||||
universal-config = { version = "0.4.0", default_features = false }
|
universal-config = { version = "0.4.0", default_features = false }
|
||||||
|
ctrlc = "3.4.0"
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
|
# cli
|
||||||
|
clap = { version = "4.2.7", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
|
# ipc
|
||||||
|
serde_json = { version = "1.0.96", optional = true }
|
||||||
|
|
||||||
# http
|
# http
|
||||||
reqwest = { version = "0.11.18", optional = true }
|
reqwest = { version = "0.11.18", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,8 @@ cargo build --release --no-default-features \
|
||||||
|---------------------|-----------------------------------------------------------------------------------|
|
|---------------------|-----------------------------------------------------------------------------------|
|
||||||
| **Core** | |
|
| **Core** | |
|
||||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||||
|
| ipc | Enables the IPC server. |
|
||||||
|
| cli | Enables the CLI. Will also enable `ipc`. |
|
||||||
| config+all | Enables support for all configuration languages. |
|
| config+all | Enables support for all configuration languages. |
|
||||||
| config+json | Enables configuration support for JSON. |
|
| config+json | Enables configuration support for JSON. |
|
||||||
| config+yaml | Enables configuration support for YAML. |
|
| config+yaml | Enables configuration support for YAML. |
|
||||||
|
|
|
@ -267,20 +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 |
|
||||||
|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------|
|
|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------|
|
||||||
| `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. |
|
||||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||||
| `center` | `Module[]` | `[]` | Array of center modules. |
|
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||||
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
| `center` | `Module[]` | `[]` | Array of center modules. |
|
||||||
|
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |
|
||||||
|
|
||||||
### 3.2 Module-level options
|
### 3.2 Module-level options
|
||||||
|
|
||||||
|
@ -306,9 +307,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|-------------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
| `show_if` | [Dynamic Boolean](dynamic-values#dynamic-boolean) | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. |
|
||||||
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
| `transition_type` | `slide_start` or `slide_end` or `crossfade` or `none` | `slide_start` | The transition animation to use when showing/hiding the widget. |
|
||||||
| `transition_duration` | `Integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
| `transition_duration` | `integer` | `250` | The length of the transition animation to use when showing/hiding the widget. |
|
||||||
|
|
||||||
#### Appearance
|
#### Appearance
|
||||||
|
|
||||||
|
|
136
docs/Controlling Ironbar.md
Normal file
136
docs/Controlling Ironbar.md
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
Ironbar includes a simple IPC server which can be used to control it programmatically at runtime.
|
||||||
|
|
||||||
|
It also includes a command line interface, which can be used for interacting with the IPC server.
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
|
||||||
|
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
|
||||||
|
You can also view help per-command, for example using `ironbar set --help`.
|
||||||
|
|
||||||
|
Responses are handled by writing their type to stdout, followed by any value starting on the next line.
|
||||||
|
Error responses are written to stderr in the same format.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ironbar set subject world
|
||||||
|
ok
|
||||||
|
|
||||||
|
$ ironbar get subject
|
||||||
|
ok
|
||||||
|
world
|
||||||
|
```
|
||||||
|
|
||||||
|
# IPC
|
||||||
|
|
||||||
|
The server listens on a Unix socket.
|
||||||
|
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
|
||||||
|
|
||||||
|
Commands and responses are sent as JSON objects, denoted by their `type` key.
|
||||||
|
|
||||||
|
The message buffer is currently limited to `1024` bytes.
|
||||||
|
Particularly large messages will be truncated or cause an error.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `ping`
|
||||||
|
|
||||||
|
Sends a ping request to the IPC.
|
||||||
|
|
||||||
|
Responds with `ok`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `inspect`
|
||||||
|
|
||||||
|
Opens the GTK inspector window.
|
||||||
|
|
||||||
|
Responds with `ok`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "inspect"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `get`
|
||||||
|
|
||||||
|
Gets an [ironvar](ironvars) value.
|
||||||
|
|
||||||
|
Responds with `ok_value` if the value exists, otherwise `error`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "get",
|
||||||
|
"key": "foo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `set`
|
||||||
|
|
||||||
|
Sets an [ironvar](ironvars) value.
|
||||||
|
|
||||||
|
Responds with `ok`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "set",
|
||||||
|
"key": "foo",
|
||||||
|
"value": "bar"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `load_css`
|
||||||
|
|
||||||
|
### `get`
|
||||||
|
|
||||||
|
Loads an additional CSS stylesheet, with hot-reloading enabled.
|
||||||
|
|
||||||
|
Responds with `ok` if the stylesheet exists, otherwise `error`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "load_css",
|
||||||
|
"path": "/path/to/style.css"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
### `ok`
|
||||||
|
|
||||||
|
The operation completed successfully, with no response data.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ok_value`
|
||||||
|
|
||||||
|
The operation completed successfully, with response data.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ok_value",
|
||||||
|
"value": "lorem ipsum"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `error`
|
||||||
|
|
||||||
|
The operation failed.
|
||||||
|
|
||||||
|
Message is optional.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"message": "lorem ipsum"
|
||||||
|
}
|
||||||
|
```
|
39
docs/Dynamic values.md
Normal file
39
docs/Dynamic values.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
In some configuration locations, Ironbar supports dynamic values,
|
||||||
|
meaning you can inject content into the bar from an external source.
|
||||||
|
|
||||||
|
Currently two dynamic content sources are supported - scripts and ironvars.
|
||||||
|
|
||||||
|
## Dynamic String
|
||||||
|
|
||||||
|
Dynamic strings can contain any mixture of static string elements, scripts and variables.
|
||||||
|
|
||||||
|
Scripts should be placed inside `{{double braces}}`. Both polling and watching scripts are supported.
|
||||||
|
|
||||||
|
Variables use the standard `#name` syntax. Variables cannot be placed inside scripts.
|
||||||
|
|
||||||
|
To use a literal hash, use `##`. This is only necessary outside of scripts.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
label = "{{cat greeting.txt}}, #subject"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Boolean
|
||||||
|
|
||||||
|
Dynamic booleans can use a single source of either a script or variable to control a true/false value.
|
||||||
|
|
||||||
|
For scripts, you can just write these directly with no notation.
|
||||||
|
Only polling scripts are supported.
|
||||||
|
The script exit code is used, where `0` is `true` and any other code is `false.
|
||||||
|
|
||||||
|
For variables, use the standard `#name` notation.
|
||||||
|
An empty string, `0` and `false` are treated as false.
|
||||||
|
Any other value is true.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
show_if = "exit 0" # script
|
||||||
|
show_if = "#show_module" # variable
|
||||||
|
```
|
9
docs/Ironvars.md
Normal file
9
docs/Ironvars.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Ironvars are runtime variables that can be referenced in several places in your config,
|
||||||
|
then set using the IPC server (such as via the CLI) using the `set` command.
|
||||||
|
|
||||||
|
Any UTF-8 string *without whitespace* is a valid key.
|
||||||
|
Any UTF-8 string is a valid value.
|
||||||
|
|
||||||
|
Reference values using `#my_variable`. These update as soon as the value changes.
|
||||||
|
|
||||||
|
You can set defaults using the `ironvar_defaults` key in your top-level config.
|
|
@ -2,10 +2,16 @@
|
||||||
|
|
||||||
- [Compiling from source](compiling)
|
- [Compiling from source](compiling)
|
||||||
- [Configuration guide](configuration-guide)
|
- [Configuration guide](configuration-guide)
|
||||||
- [Scripts](scripts)
|
|
||||||
- [Images](images)
|
- [Images](images)
|
||||||
- [Styling guide](styling-guide)
|
- [Styling guide](styling-guide)
|
||||||
|
|
||||||
|
# Dynamic content
|
||||||
|
|
||||||
|
- [Controlling Ironbar](controlling-ironbar)
|
||||||
|
- [Dynamic values](dynamic-values)
|
||||||
|
- [Scripts](scripts)
|
||||||
|
- [Ironvars](ironvars)
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
- [Config](config)
|
- [Config](config)
|
||||||
|
@ -28,4 +34,4 @@
|
||||||
- [Sys_Info](sys-info)
|
- [Sys_Info](sys-info)
|
||||||
- [Tray](tray)
|
- [Tray](tray)
|
||||||
- [Upower](upower)
|
- [Upower](upower)
|
||||||
- [Workspaces](workspaces)
|
- [Workspaces](workspaces)
|
||||||
|
|
|
@ -9,17 +9,15 @@ Supports plain text and images.
|
||||||
|
|
||||||
> Type: `clipboard`
|
> Type: `clipboard`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `icon` | `string/image` | `` | Icon to show on the widget button. |
|
| `icon` | `string` or [image](images) | `` | Icon to show on the widget button. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. |
|
||||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
See [here](images) for information on images.
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
||||||
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||||
|
|
||||||
|
If you only intend to run a single script, prefer the [script](script) module,
|
||||||
|
or [label](label) if you only need a single text label.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
@ -18,11 +21,11 @@ You can think of these like HTML elements and their attributes.
|
||||||
Every widget has the following options available; `type` is mandatory.
|
Every widget has the following options available; `type` is mandatory.
|
||||||
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
|---------|-------------------------------------------------------------------------------|---------|-------------------------------|
|
||||||
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. |
|
||||||
| `name` | `string` | `null` | Widget name. |
|
| `name` | `string` | `null` | Widget name. |
|
||||||
| `class` | `string` | `null` | Widget class name. |
|
| `class` | `string` | `null` | Widget class name. |
|
||||||
|
|
||||||
#### Box
|
#### Box
|
||||||
|
|
||||||
|
@ -30,20 +33,20 @@ A container to place nested widgets inside.
|
||||||
|
|
||||||
> Type: `box`
|
> Type: `box`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------|
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. |
|
||||||
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. |
|
||||||
|
|
||||||
#### Label
|
#### Label
|
||||||
|
|
||||||
A text label. Pango markup and embedded scripts are supported.
|
A text label. Pango markup is supported.
|
||||||
|
|
||||||
> Type `label`
|
> Type `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|----------|--------------|---------------------------------------------------------------------|
|
|---------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
|
|
||||||
#### Button
|
#### Button
|
||||||
|
|
||||||
|
@ -51,10 +54,10 @@ A clickable button, which can run a command when clicked.
|
||||||
|
|
||||||
> Type `button`
|
> Type `button`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|------------|--------------------|--------------|---------------------------------------------------------------------|
|
|------------|-------------------------------------------------|---------|---------------------------------------------------------------------|
|
||||||
| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. |
|
||||||
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). |
|
||||||
|
|
||||||
#### Image
|
#### Image
|
||||||
|
|
||||||
|
@ -62,10 +65,10 @@ An image or icon from disk or http.
|
||||||
|
|
||||||
> Type `image`
|
> Type `image`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|--------|-----------|---------|---------------------------------------------------------------------------------------------|
|
|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. |
|
| `src` | [image](images) via [Dynamic String](dynamic-values#dynamic-string) | `null` | Image source. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
||||||
|
|
||||||
#### Slider
|
#### Slider
|
||||||
|
|
||||||
|
@ -76,18 +79,16 @@ A draggable slider.
|
||||||
Note that `on_change` will provide the **floating point** value as an argument.
|
Note that `on_change` will provide the **floating point** value as an argument.
|
||||||
If your input program requires an integer, you will need to round it.
|
If your input program requires an integer, you will need to round it.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
||||||
| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. |
|
| `min` | `float` | `0` | Minimum slider value. |
|
||||||
| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). |
|
| `max` | `float` | `100` | Maximum slider value. |
|
||||||
| `min` | `float` | `0` | Minimum slider value. |
|
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
||||||
| `max` | `float` | `100` | Maximum slider value. |
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. |
|
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
||||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
|
||||||
| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. |
|
|
||||||
|
|
||||||
The example slider widget below shows a volume control for MPC,
|
The example slider widget below shows a volume control for MPC,
|
||||||
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
which updates the server when changed, and polls the server for volume changes to keep the slider in sync.
|
||||||
|
@ -115,14 +116,12 @@ A progress bar.
|
||||||
|
|
||||||
Note that `value` expects a numeric value **between 0-`max`** as output.
|
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
||||||
| `src` | `image` | `null` | Image source. See [here](images) for information on images. |
|
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. |
|
||||||
| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. |
|
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
||||||
| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. |
|
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||||
| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. |
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
| `max` | `float` | `100` | Maximum progress bar value. |
|
|
||||||
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
|
||||||
|
|
||||||
The example below shows progress for the current playing song in MPD,
|
The example below shows progress for the current playing song in MPD,
|
||||||
and displays the elapsed/length timestamps as a label above:
|
and displays the elapsed/length timestamps as a label above:
|
||||||
|
|
|
@ -7,15 +7,15 @@ Displays the title and/or icon of the currently focused window.
|
||||||
|
|
||||||
> Type: `focused`
|
> Type: `focused`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
|
| `show_icon` | `boolean` | `true` | Whether to show the app's icon. |
|
||||||
| `show_title` | `boolean` | `true` | Whether to show the app's title |
|
| `show_title` | `boolean` | `true` | Whether to show the app's title. |
|
||||||
| `icon_size` | `integer` | `32` | Size of icon in pixels |
|
| `icon_size` | `integer` | `32` | Size of icon in pixels. |
|
||||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
Displays custom text, with the ability to embed [scripts](https://github.com/JakeStanger/ironbar/wiki/scripts#embedding).
|
Displays custom text, with markup support.
|
||||||
|
|
||||||
|
If you only intend to run a single script, prefer the [script](script) module.
|
||||||
|
For more advanced use-cases, use [custom](custom).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
> Type: `label`
|
> Type: `label`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|---------|----------|---------|-----------------------------------------|
|
|---------|-------------------------------------------------|---------|------------------------|
|
||||||
| `label` | `string` | `null` | Text, optionally with embedded scripts. |
|
| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
|
@ -11,27 +11,27 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
||||||
|
|
||||||
> Type: `music`
|
> Type: `music`
|
||||||
|
|
||||||
| | Type | Default | Description |
|
| | Type | Default | Description |
|
||||||
|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
| `player_type` | `'mpris'` or `'mpd'` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
|
||||||
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
|
||||||
| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
| `truncate` | `'start'` or `'middle'` or `'end'` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
|
||||||
| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
| `truncate.mode` | `'start'` or `'middle'` or `'end'` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
|
||||||
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
|
||||||
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
|
||||||
| `icons.play` | `string/image` | `` | Icon to show when playing. |
|
| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. |
|
||||||
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
|
| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. |
|
||||||
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
|
| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. |
|
||||||
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
|
| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. |
|
||||||
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
|
| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. |
|
||||||
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
|
| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. |
|
||||||
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
|
| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. |
|
||||||
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
|
| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. |
|
||||||
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. |
|
||||||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||||
|
|
||||||
See [here](images) for information on images.
|
See [here](images) for information on images.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
Executes a script and shows the result of `stdout` on a label.
|
Executes a script and shows the result of `stdout` on a label.
|
||||||
Pango markup is supported.
|
Pango markup is supported.
|
||||||
|
|
||||||
|
If you want to be able to embed multiple scripts and/or variables, prefer the [label](label) module.
|
||||||
|
For more advanced use-cases, use [custom](custom).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
> Type: `script`
|
> Type: `script`
|
||||||
|
|
|
@ -8,12 +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/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. |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||||
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::send;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// MPSC async -> sync channel.
|
/// MPSC async -> GTK sync channel.
|
||||||
/// The sender uses `tokio::sync::mpsc`
|
/// The sender uses `tokio::sync::mpsc`
|
||||||
/// while the receiver uses `glib::MainContext::channel`.
|
/// while the receiver uses `glib::MainContext::channel`.
|
||||||
///
|
///
|
||||||
|
|
19
src/cli/mod.rs
Normal file
19
src/cli/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::ipc::commands::Command;
|
||||||
|
use crate::ipc::responses::Response;
|
||||||
|
use clap::Parser;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Serialize, Deserialize)]
|
||||||
|
#[command(version)]
|
||||||
|
pub struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_response(response: Response) {
|
||||||
|
match response {
|
||||||
|
Response::Ok => println!("ok"),
|
||||||
|
Response::OkValue { value } => println!("ok\n{value}"),
|
||||||
|
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||||
use crate::script::{Script, ScriptInput};
|
use crate::script::{Script, ScriptInput};
|
||||||
use crate::send;
|
|
||||||
use gtk::gdk::ScrollDirection;
|
use gtk::gdk::ScrollDirection;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::spawn;
|
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
/// Common configuration options
|
/// Common configuration options
|
||||||
|
@ -15,7 +13,7 @@ pub struct CommonConfig {
|
||||||
pub class: Option<String>,
|
pub class: Option<String>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
pub show_if: Option<ScriptInput>,
|
pub show_if: Option<DynamicBool>,
|
||||||
pub transition_type: Option<TransitionType>,
|
pub transition_type: Option<TransitionType>,
|
||||||
pub transition_duration: Option<u32>,
|
pub transition_duration: Option<u32>,
|
||||||
|
|
||||||
|
@ -114,7 +112,7 @@ impl CommonConfig {
|
||||||
|
|
||||||
if let Some(tooltip) = self.tooltip {
|
if let Some(tooltip) = self.tooltip {
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
DynamicString::new(&tooltip, move |string| {
|
dynamic_string(&tooltip, move |string| {
|
||||||
container.set_tooltip_text(Some(&string));
|
container.set_tooltip_text(Some(&string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
@ -127,23 +125,13 @@ impl CommonConfig {
|
||||||
container.show_all();
|
container.show_all();
|
||||||
},
|
},
|
||||||
|show_if| {
|
|show_if| {
|
||||||
let script = Script::new_polling(show_if);
|
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
script
|
|
||||||
.run(None, |_, success| {
|
|
||||||
send!(tx, success);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let revealer = revealer.clone();
|
let revealer = revealer.clone();
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
|
|
||||||
rx.attach(None, move |success| {
|
show_if.subscribe(move |success| {
|
||||||
if success {
|
if success {
|
||||||
container.show_all();
|
container.show_all();
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,8 @@ pub struct Config {
|
||||||
/// GTK icon theme to use.
|
/// GTK icon theme to use.
|
||||||
pub icon_theme: Option<String>,
|
pub icon_theme: Option<String>,
|
||||||
|
|
||||||
|
pub ironvar_defaults: Option<HashMap<Box<str>, String>>,
|
||||||
|
|
||||||
pub start: Option<Vec<ModuleConfig>>,
|
pub start: Option<Vec<ModuleConfig>>,
|
||||||
pub center: Option<Vec<ModuleConfig>>,
|
pub center: Option<Vec<ModuleConfig>>,
|
||||||
pub end: Option<Vec<ModuleConfig>>,
|
pub end: Option<Vec<ModuleConfig>>,
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
use crate::script::{OutputStream, Script};
|
|
||||||
use crate::{lock, send};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tokio::spawn;
|
|
||||||
|
|
||||||
/// A segment of a dynamic string,
|
|
||||||
/// containing either a static string
|
|
||||||
/// or a script.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DynamicStringSegment {
|
|
||||||
Static(String),
|
|
||||||
Dynamic(Script),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string with embedded scripts for dynamic content.
|
|
||||||
pub struct DynamicString;
|
|
||||||
|
|
||||||
impl DynamicString {
|
|
||||||
/// Creates a new dynamic string, based off the input template.
|
|
||||||
/// Runs `f` with the compiled string each time one of the scripts updates.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rs
|
|
||||||
/// DynamicString::new(&text, move |string| {
|
|
||||||
/// label.set_markup(&string);
|
|
||||||
/// Continue(true)
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
pub fn new<F>(input: &str, f: F) -> Self
|
|
||||||
where
|
|
||||||
F: FnMut(String) -> Continue + 'static,
|
|
||||||
{
|
|
||||||
let segments = Self::parse_input(input);
|
|
||||||
|
|
||||||
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
for (i, segment) in segments.into_iter().enumerate() {
|
|
||||||
match segment {
|
|
||||||
DynamicStringSegment::Static(str) => {
|
|
||||||
lock!(label_parts).push(str);
|
|
||||||
}
|
|
||||||
DynamicStringSegment::Dynamic(script) => {
|
|
||||||
let tx = tx.clone();
|
|
||||||
let label_parts = label_parts.clone();
|
|
||||||
|
|
||||||
// insert blank value to preserve segment order
|
|
||||||
lock!(label_parts).push(String::new());
|
|
||||||
|
|
||||||
spawn(async move {
|
|
||||||
script
|
|
||||||
.run(None, |out, _| {
|
|
||||||
if let OutputStream::Stdout(out) = out {
|
|
||||||
let mut label_parts = lock!(label_parts);
|
|
||||||
|
|
||||||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
|
||||||
|
|
||||||
let string = label_parts.join("");
|
|
||||||
send!(tx, string);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize
|
|
||||||
{
|
|
||||||
let label_parts = lock!(label_parts).join("");
|
|
||||||
send!(tx, label_parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
rx.attach(None, f);
|
|
||||||
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the input string into static and dynamic segments
|
|
||||||
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
|
||||||
if !input.contains("{{") {
|
|
||||||
return vec![DynamicStringSegment::Static(input.to_string())];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut segments = vec![];
|
|
||||||
|
|
||||||
let mut chars = input.chars().collect::<Vec<_>>();
|
|
||||||
while !chars.is_empty() {
|
|
||||||
let char_pair = if chars.len() > 1 {
|
|
||||||
Some(&chars[..=1])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (token, skip) = if let Some(['{', '{']) = char_pair {
|
|
||||||
const SKIP_BRACKETS: usize = 4; // two braces either side
|
|
||||||
|
|
||||||
let str = chars
|
|
||||||
.windows(2)
|
|
||||||
.skip(2)
|
|
||||||
.take_while(|win| win != &['}', '}'])
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let len = str.len();
|
|
||||||
|
|
||||||
(
|
|
||||||
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
|
||||||
len + SKIP_BRACKETS,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let mut str = chars
|
|
||||||
.windows(2)
|
|
||||||
.take_while(|win| win != &['{', '{'])
|
|
||||||
.map(|w| w[0])
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
// if segment is at end of string, last char gets missed above due to uneven window.
|
|
||||||
if chars.len() == str.len() + 1 {
|
|
||||||
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
|
||||||
str.push(remaining_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = str.len();
|
|
||||||
|
|
||||||
(DynamicStringSegment::Static(str), len)
|
|
||||||
};
|
|
||||||
|
|
||||||
// quick runtime check to make sure the parser is working as expected
|
|
||||||
assert_ne!(skip, 0);
|
|
||||||
|
|
||||||
segments.push(token);
|
|
||||||
chars.drain(..skip);
|
|
||||||
}
|
|
||||||
|
|
||||||
segments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test() {
|
|
||||||
// TODO: see if we can run gtk tests in ci
|
|
||||||
if gtk::init().is_ok() {
|
|
||||||
let label = gtk::Label::new(None);
|
|
||||||
DynamicString::new(
|
|
||||||
"Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}",
|
|
||||||
move |string| {
|
|
||||||
label.set_label(&string);
|
|
||||||
Continue(true)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
78
src/dynamic_value/dynamic_bool.rs
Normal file
78
src/dynamic_value/dynamic_bool.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::script::Script;
|
||||||
|
use crate::send;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use glib::Continue;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum DynamicBool {
|
||||||
|
/// Either a script or variable, to be determined.
|
||||||
|
Unknown(String),
|
||||||
|
Script(Script),
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
Variable(Box<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicBool {
|
||||||
|
pub fn subscribe<F>(self, f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(bool) -> Continue + 'static,
|
||||||
|
{
|
||||||
|
let value = match self {
|
||||||
|
Self::Unknown(input) => {
|
||||||
|
if input.starts_with('#') {
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ipc")] {
|
||||||
|
Self::Variable(input.into())
|
||||||
|
} else {
|
||||||
|
Self::Unknown(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let script = Script::from(input.as_str());
|
||||||
|
Self::Script(script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
rx.attach(None, f);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
match value {
|
||||||
|
DynamicBool::Script(script) => {
|
||||||
|
script
|
||||||
|
.run(None, |_, success| {
|
||||||
|
send!(tx, success);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
DynamicBool::Variable(variable) => {
|
||||||
|
let variable_manager = get_variable_manager();
|
||||||
|
|
||||||
|
let variable_name = variable[1..].into(); // remove hash
|
||||||
|
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||||
|
|
||||||
|
while let Ok(value) = rx.recv().await {
|
||||||
|
let has_value = value.map(|s| is_truthy(&s)).unwrap_or_default();
|
||||||
|
send!(tx, has_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicBool::Unknown(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a string ironvar is 'truthy'
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
fn is_truthy(string: &str) -> bool {
|
||||||
|
!(string.is_empty() || string == "0" || string == "false")
|
||||||
|
}
|
321
src/dynamic_value/dynamic_string.rs
Normal file
321
src/dynamic_value/dynamic_string.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::script::{OutputStream, Script};
|
||||||
|
use crate::{lock, send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
/// A segment of a dynamic string,
|
||||||
|
/// containing either a static string
|
||||||
|
/// or a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DynamicStringSegment {
|
||||||
|
Static(String),
|
||||||
|
Script(Script),
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
Variable(Box<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new dynamic string, based off the input template.
|
||||||
|
/// Runs `f` with the compiled string each time one of the scripts or variables updates.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rs
|
||||||
|
/// dynamic_string(&text, move |string| {
|
||||||
|
/// label.set_markup(&string);
|
||||||
|
/// Continue(true)
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn dynamic_string<F>(input: &str, f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(String) -> Continue + 'static,
|
||||||
|
{
|
||||||
|
let tokens = parse_input(input);
|
||||||
|
|
||||||
|
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
for (i, segment) in tokens.into_iter().enumerate() {
|
||||||
|
match segment {
|
||||||
|
DynamicStringSegment::Static(str) => {
|
||||||
|
lock!(label_parts).push(str);
|
||||||
|
}
|
||||||
|
DynamicStringSegment::Script(script) => {
|
||||||
|
let tx = tx.clone();
|
||||||
|
let label_parts = label_parts.clone();
|
||||||
|
|
||||||
|
// insert blank value to preserve segment order
|
||||||
|
lock!(label_parts).push(String::new());
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(None, |out, _| {
|
||||||
|
if let OutputStream::Stdout(out) = out {
|
||||||
|
let mut label_parts = lock!(label_parts);
|
||||||
|
|
||||||
|
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||||
|
|
||||||
|
let string = label_parts.join("");
|
||||||
|
send!(tx, string);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
DynamicStringSegment::Variable(name) => {
|
||||||
|
let tx = tx.clone();
|
||||||
|
let label_parts = label_parts.clone();
|
||||||
|
|
||||||
|
// insert blank value to preserve segment order
|
||||||
|
lock!(label_parts).push(String::new());
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
let variable_manager = get_variable_manager();
|
||||||
|
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
|
||||||
|
|
||||||
|
while let Ok(value) = rx.recv().await {
|
||||||
|
if let Some(value) = value {
|
||||||
|
let mut label_parts = lock!(label_parts);
|
||||||
|
|
||||||
|
let _: String = std::mem::replace(&mut label_parts[i], value);
|
||||||
|
|
||||||
|
let string = label_parts.join("");
|
||||||
|
send!(tx, string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rx.attach(None, f);
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
{
|
||||||
|
let label_parts = lock!(label_parts).join("");
|
||||||
|
send!(tx, label_parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the input string into static and dynamic segments
|
||||||
|
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||||
|
// short-circuit parser if it's all static
|
||||||
|
if !input.contains("{{") && !input.contains('#') {
|
||||||
|
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tokens = vec![];
|
||||||
|
|
||||||
|
let mut chars = input.chars().collect::<Vec<_>>();
|
||||||
|
while !chars.is_empty() {
|
||||||
|
let char_pair = if chars.len() > 1 {
|
||||||
|
Some(&chars[..=1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (token, skip) = match char_pair {
|
||||||
|
Some(['{', '{']) => parse_script(&chars),
|
||||||
|
Some(['#', '#']) => (DynamicStringSegment::Static("#".to_string()), 2),
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
Some(['#', _]) => parse_variable(&chars),
|
||||||
|
_ => parse_static(&chars),
|
||||||
|
};
|
||||||
|
|
||||||
|
// quick runtime check to make sure the parser is working as expected
|
||||||
|
assert_ne!(skip, 0);
|
||||||
|
|
||||||
|
tokens.push(token);
|
||||||
|
chars.drain(..skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_script(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||||
|
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||||
|
|
||||||
|
let str = chars
|
||||||
|
.windows(2)
|
||||||
|
.skip(2)
|
||||||
|
.take_while(|win| win != &['}', '}'])
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len() + SKIP_BRACKETS;
|
||||||
|
let script = Script::from(str.as_str());
|
||||||
|
|
||||||
|
(DynamicStringSegment::Script(script), len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
fn parse_variable(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||||
|
const SKIP_HASH: usize = 1;
|
||||||
|
|
||||||
|
let str = chars
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.take_while(|&c| !c.is_whitespace())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len() + SKIP_HASH;
|
||||||
|
let value = str.into();
|
||||||
|
|
||||||
|
(DynamicStringSegment::Variable(value), len)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_static(chars: &[char]) -> (DynamicStringSegment, usize) {
|
||||||
|
let mut str = chars
|
||||||
|
.windows(2)
|
||||||
|
.take_while(|&win| win != ['{', '{'] && win[0] != '#')
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||||
|
if chars.len() == str.len() + 1 {
|
||||||
|
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||||
|
str.push(remaining_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(DynamicStringSegment::Static(str), len)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static() {
|
||||||
|
const INPUT: &str = "hello world";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_odd_char_count() {
|
||||||
|
const INPUT: &str = "hello";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(value) if value == INPUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script() {
|
||||||
|
const INPUT: &str = "{{echo hello}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo hello")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable() {
|
||||||
|
const INPUT: &str = "#variable";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Variable(name) if name.to_string() == "variable")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script() {
|
||||||
|
const INPUT: &str = "hello {{echo world}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 2);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_variable() {
|
||||||
|
const INPUT: &str = "hello #subject";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 2);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script_static() {
|
||||||
|
const INPUT: &str = "hello {{echo world}} foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_variable_static() {
|
||||||
|
const INPUT: &str = "hello #subject foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Variable(name) if name.to_string() == "subject")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_script_variable() {
|
||||||
|
const INPUT: &str = "hello {{echo world}} #foo";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 4);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "hello "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[1], DynamicStringSegment::Script(script) if script.cmd == "echo world")
|
||||||
|
);
|
||||||
|
assert!(matches!(&tokens[2], DynamicStringSegment::Static(str) if str == " "));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[3], DynamicStringSegment::Variable(name) if name.to_string() == "foo")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_hash() {
|
||||||
|
const INPUT: &str = "number ###num";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 3);
|
||||||
|
assert!(matches!(&tokens[0], DynamicStringSegment::Static(str) if str == "number "));
|
||||||
|
assert!(matches!(&tokens[1], DynamicStringSegment::Static(str) if str == "#"));
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[2], DynamicStringSegment::Variable(name) if name.to_string() == "num")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script_with_hash() {
|
||||||
|
const INPUT: &str = "{{echo #hello}}";
|
||||||
|
let tokens = parse_input(INPUT);
|
||||||
|
|
||||||
|
assert_eq!(tokens.len(), 1);
|
||||||
|
assert!(
|
||||||
|
matches!(&tokens[0], DynamicStringSegment::Script(script) if script.cmd == "echo #hello")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
src/dynamic_value/mod.rs
Normal file
7
src/dynamic_value/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#![doc = include_str!("../../docs/Dynamic values.md")]
|
||||||
|
|
||||||
|
mod dynamic_bool;
|
||||||
|
mod dynamic_string;
|
||||||
|
|
||||||
|
pub use dynamic_bool::DynamicBool;
|
||||||
|
pub use dynamic_string::dynamic_string;
|
28
src/ipc/client.rs
Normal file
28
src/ipc/client.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use super::Ipc;
|
||||||
|
use crate::ipc::{Command, Response};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use color_eyre::{Help, Report};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
|
impl Ipc {
|
||||||
|
/// Sends a command to the IPC server.
|
||||||
|
/// The server response is returned.
|
||||||
|
pub async fn send(&self, command: Command) -> Result<Response> {
|
||||||
|
let mut stream = match UnixStream::connect(&self.path).await {
|
||||||
|
Ok(stream) => Ok(stream),
|
||||||
|
Err(err) => Err(Report::new(err)
|
||||||
|
.wrap_err("Failed to connect to Ironbar IPC server")
|
||||||
|
.suggestion("Is Ironbar running?")),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let write_buffer = serde_json::to_vec(&command)?;
|
||||||
|
stream.write_all(&write_buffer).await?;
|
||||||
|
|
||||||
|
let mut read_buffer = vec![0; 1024];
|
||||||
|
let bytes = stream.read(&mut read_buffer).await?;
|
||||||
|
|
||||||
|
let response = serde_json::from_slice(&read_buffer[..bytes])?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
37
src/ipc/commands.rs
Normal file
37
src/ipc/commands.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use clap::Subcommand;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Command {
|
||||||
|
/// Return "ok"
|
||||||
|
Ping,
|
||||||
|
|
||||||
|
/// Open the GTK inspector
|
||||||
|
Inspect,
|
||||||
|
|
||||||
|
/// Set an `ironvar` value.
|
||||||
|
/// This creates it if it does not already exist, and updates it if it does.
|
||||||
|
/// Any references to this variable are automatically and immediately updated.
|
||||||
|
/// Keys and values can be any valid UTF-8 string.
|
||||||
|
Set {
|
||||||
|
/// Variable key. Can be any valid UTF-8 string.
|
||||||
|
key: Box<str>,
|
||||||
|
/// Variable value. Can be any valid UTF-8 string.
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get the current value of an `ironvar`.
|
||||||
|
Get {
|
||||||
|
/// Variable key.
|
||||||
|
key: Box<str>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Load an additional CSS stylesheet.
|
||||||
|
/// The sheet is automatically hot-reloaded.
|
||||||
|
LoadCss {
|
||||||
|
/// The path to the sheet.
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
33
src/ipc/mod.rs
Normal file
33
src/ipc/mod.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
mod client;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod responses;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub use commands::Command;
|
||||||
|
pub use responses::Response;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ipc {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ipc {
|
||||||
|
/// Creates a new IPC instance.
|
||||||
|
/// This can be used as both a server and client.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
|
||||||
|
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
|
||||||
|
.join("ironbar-ipc.sock");
|
||||||
|
|
||||||
|
if format!("{}", ipc_socket_file.display()).len() > 100 {
|
||||||
|
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
path: ipc_socket_file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/ipc/responses.rs
Normal file
18
src/ipc/responses.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
OkValue { value: String },
|
||||||
|
Err { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new `Response::Error`.
|
||||||
|
pub fn error(message: &str) -> Self {
|
||||||
|
Self::Err {
|
||||||
|
message: Some(message.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
src/ipc/server.rs
Normal file
144
src/ipc/server.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use super::Ipc;
|
||||||
|
use crate::bridge_channel::BridgeChannel;
|
||||||
|
use crate::ipc::{Command, Response};
|
||||||
|
use crate::ironvar::get_variable_manager;
|
||||||
|
use crate::style::load_css;
|
||||||
|
use crate::{read_lock, send_async, try_send, write_lock};
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
use 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 {
|
||||||
|
/// Starts the IPC server on its socket.
|
||||||
|
///
|
||||||
|
/// Once started, the server will begin accepting connections.
|
||||||
|
pub fn start(&self) {
|
||||||
|
let bridge = BridgeChannel::<Command>::new();
|
||||||
|
let cmd_tx = bridge.create_sender();
|
||||||
|
let (res_tx, mut res_rx) = mpsc::channel(32);
|
||||||
|
|
||||||
|
let path = self.path.clone();
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
warn!("Socket already exists. Did Ironbar exit abruptly?");
|
||||||
|
warn!("Attempting IPC shutdown to allow binding to address");
|
||||||
|
self.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
info!("Starting IPC on {}", path.display());
|
||||||
|
|
||||||
|
let listener = match UnixListener::bind(&path) {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"{:?}",
|
||||||
|
Report::new(err).wrap_err("Unable to start IPC server")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((stream, _addr)) => {
|
||||||
|
if let Err(err) =
|
||||||
|
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
|
||||||
|
{
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.recv(move |command| {
|
||||||
|
let res = Self::handle_command(command);
|
||||||
|
try_send!(res_tx, res);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes an incoming connections,
|
||||||
|
/// reads the command message, and sends the response.
|
||||||
|
///
|
||||||
|
/// The connection is closed once the response has been written.
|
||||||
|
async fn handle_connection(
|
||||||
|
mut stream: UnixStream,
|
||||||
|
cmd_tx: &Sender<Command>,
|
||||||
|
res_rx: &mut Receiver<Response>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (mut stream_read, mut stream_write) = stream.split();
|
||||||
|
|
||||||
|
let mut read_buffer = vec![0; 1024];
|
||||||
|
let bytes = stream_read.read(&mut read_buffer).await?;
|
||||||
|
|
||||||
|
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
|
||||||
|
|
||||||
|
debug!("Received command: {command:?}");
|
||||||
|
|
||||||
|
send_async!(cmd_tx, command);
|
||||||
|
let res = res_rx
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.unwrap_or(Response::Err { message: None });
|
||||||
|
let res = serde_json::to_vec(&res)?;
|
||||||
|
|
||||||
|
stream_write.write_all(&res).await?;
|
||||||
|
stream_write.shutdown().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes an input command, runs it and returns with the appropriate response.
|
||||||
|
///
|
||||||
|
/// This runs on the main thread, allowing commands to interact with GTK.
|
||||||
|
fn handle_command(command: Command) -> Response {
|
||||||
|
match command {
|
||||||
|
Command::Inspect => {
|
||||||
|
gtk::Window::set_interactive_debugging(true);
|
||||||
|
Response::Ok
|
||||||
|
}
|
||||||
|
Command::Set { key, value } => {
|
||||||
|
let variable_manager = get_variable_manager();
|
||||||
|
let mut variable_manager = write_lock!(variable_manager);
|
||||||
|
match variable_manager.set(key, value) {
|
||||||
|
Ok(_) => Response::Ok,
|
||||||
|
Err(err) => Response::error(&format!("{err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Get { key } => {
|
||||||
|
let variable_manager = get_variable_manager();
|
||||||
|
let value = read_lock!(variable_manager).get(&key);
|
||||||
|
match value {
|
||||||
|
Some(value) => Response::OkValue { value },
|
||||||
|
None => Response::error("Variable not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::LoadCss { path } => {
|
||||||
|
if path.exists() {
|
||||||
|
load_css(path);
|
||||||
|
Response::Ok
|
||||||
|
} else {
|
||||||
|
Response::error("File not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Ping => Response::Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuts down the IPC server,
|
||||||
|
/// removing the socket file in the process.
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
fs::remove_file(&self.path).ok();
|
||||||
|
}
|
||||||
|
}
|
107
src/ironvar.rs
Normal file
107
src/ironvar.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
#![doc = include_str!("../docs/Ironvars.md")]
|
||||||
|
|
||||||
|
use crate::{arc_rw, send};
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref VARIABLE_MANAGER: Arc<RwLock<VariableManager>> = arc_rw!(VariableManager::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_variable_manager() -> Arc<RwLock<VariableManager>> {
|
||||||
|
VARIABLE_MANAGER.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global singleton manager for `IronVar` variables.
|
||||||
|
pub struct VariableManager {
|
||||||
|
variables: HashMap<Box<str>, IronVar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VariableManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
variables: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value for a variable,
|
||||||
|
/// creating it if it does not exist.
|
||||||
|
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
|
||||||
|
if Self::key_is_valid(&key) {
|
||||||
|
if let Some(var) = self.variables.get_mut(&key) {
|
||||||
|
var.set(Some(value));
|
||||||
|
} else {
|
||||||
|
let var = IronVar::new(Some(value));
|
||||||
|
self.variables.insert(key, var);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Report::msg("Invalid key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current value of an `ironvar`.
|
||||||
|
/// Prefer to use `subscribe` where possible.
|
||||||
|
pub fn get(&self, key: &str) -> Option<String> {
|
||||||
|
self.variables.get(key).and_then(IronVar::get)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to an `ironvar`, creating it if it does not exist.
|
||||||
|
/// Any time the var is set, its value is sent on the channel.
|
||||||
|
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
||||||
|
self.variables
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| IronVar::new(None))
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_is_valid(key: &str) -> bool {
|
||||||
|
!key.is_empty()
|
||||||
|
&& key
|
||||||
|
.chars()
|
||||||
|
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ironbar dynamic variable representation.
|
||||||
|
/// Interact with them through the `VARIABLE_MANAGER` `VariableManager` singleton.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct IronVar {
|
||||||
|
value: Option<String>,
|
||||||
|
tx: broadcast::Sender<Option<String>>,
|
||||||
|
_rx: broadcast::Receiver<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IronVar {
|
||||||
|
/// Creates a new variable.
|
||||||
|
fn new(value: Option<String>) -> Self {
|
||||||
|
let (tx, rx) = broadcast::channel(32);
|
||||||
|
|
||||||
|
Self { value, tx, _rx: rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current variable value.
|
||||||
|
/// Prefer to subscribe to changes where possible.
|
||||||
|
fn get(&self) -> Option<String> {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the current variable value.
|
||||||
|
/// The change is broadcast to all receivers.
|
||||||
|
fn set(&mut self, value: Option<String>) {
|
||||||
|
self.value = value.clone();
|
||||||
|
send!(self.tx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to the variable.
|
||||||
|
/// The latest value is immediately sent to all receivers.
|
||||||
|
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
|
||||||
|
let rx = self.tx.subscribe();
|
||||||
|
send!(self.tx, self.value.clone());
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/// Sends a message on an asynchronous `Sender` using `send()`
|
/// Sends a message on an asynchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send_async!(tx, "my message");
|
/// send_async!(tx, "my message");
|
||||||
|
@ -16,7 +16,7 @@ macro_rules! send_async {
|
||||||
/// Sends a message on an synchronous `Sender` using `send()`
|
/// Sends a message on an synchronous `Sender` using `send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// send!(tx, "my message");
|
/// send!(tx, "my message");
|
||||||
|
@ -31,7 +31,7 @@ macro_rules! send {
|
||||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||||
/// Panics if the message cannot be sent.
|
/// Panics if the message cannot be sent.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// try_send!(tx, "my message");
|
/// try_send!(tx, "my message");
|
||||||
|
@ -46,7 +46,7 @@ macro_rules! try_send {
|
||||||
/// Locks a `Mutex`.
|
/// Locks a `Mutex`.
|
||||||
/// Panics if the `Mutex` cannot be locked.
|
/// Panics if the `Mutex` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = lock!(my_mutex);
|
/// let mut val = lock!(my_mutex);
|
||||||
|
@ -62,7 +62,7 @@ macro_rules! lock {
|
||||||
/// Gets a read lock on a `RwLock`.
|
/// Gets a read lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let val = read_lock!(my_rwlock);
|
/// let val = read_lock!(my_rwlock);
|
||||||
|
@ -77,7 +77,7 @@ macro_rules! read_lock {
|
||||||
/// Gets a write lock on a `RwLock`.
|
/// Gets a write lock on a `RwLock`.
|
||||||
/// Panics if the `RwLock` cannot be locked.
|
/// Panics if the `RwLock` cannot be locked.
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// # Usage:
|
||||||
///
|
///
|
||||||
/// ```rs
|
/// ```rs
|
||||||
/// let mut val = write_lock!(my_rwlock);
|
/// let mut val = write_lock!(my_rwlock);
|
||||||
|
@ -88,3 +88,33 @@ macro_rules! write_lock {
|
||||||
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
$rwlock.write().expect($crate::error::ERR_WRITE_LOCK)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps `val` in a new `Arc<Mutex<T>>`.
|
||||||
|
///
|
||||||
|
/// # Usage:
|
||||||
|
///
|
||||||
|
/// ```rs
|
||||||
|
/// let val = arc_mut!(MyService::new());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! arc_mut {
|
||||||
|
($val:expr) => {
|
||||||
|
std::sync::Arc::new(std::Sync::Mutex::new($val))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps `val` in a new `Arc<RwLock<T>>`.
|
||||||
|
///
|
||||||
|
/// # Usage:
|
||||||
|
///
|
||||||
|
/// ```rs
|
||||||
|
/// let val = arc_rw!(MyService::new());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! arc_rw {
|
||||||
|
($val:expr) => {
|
||||||
|
std::sync::Arc::new(std::sync::RwLock::new($val))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
78
src/main.rs
78
src/main.rs
|
@ -2,13 +2,19 @@
|
||||||
|
|
||||||
mod bar;
|
mod bar;
|
||||||
mod bridge_channel;
|
mod bridge_channel;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
mod cli;
|
||||||
mod clients;
|
mod clients;
|
||||||
mod config;
|
mod config;
|
||||||
mod desktop_file;
|
mod desktop_file;
|
||||||
mod dynamic_string;
|
mod dynamic_value;
|
||||||
mod error;
|
mod error;
|
||||||
mod gtk_helpers;
|
mod gtk_helpers;
|
||||||
mod image;
|
mod image;
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
mod ipc;
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
mod ironvar;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod modules;
|
mod modules;
|
||||||
|
@ -20,6 +26,9 @@ mod unique_id;
|
||||||
use crate::bar::create_bar;
|
use crate::bar::create_bar;
|
||||||
use crate::config::{Config, MonitorConfig};
|
use crate::config::{Config, MonitorConfig};
|
||||||
use crate::style::load_css;
|
use crate::style::load_css;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
use clap::Parser;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use dirs::config_dir;
|
use dirs::config_dir;
|
||||||
|
@ -32,8 +41,9 @@ use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::mpsc;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::{block_in_place, spawn_blocking};
|
||||||
|
|
||||||
use crate::error::ExitCode;
|
use crate::error::ExitCode;
|
||||||
use clients::wayland::{self, WaylandClient};
|
use clients::wayland::{self, WaylandClient};
|
||||||
|
@ -47,6 +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();
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "cli")] {
|
||||||
|
run_with_args().await
|
||||||
|
} else {
|
||||||
|
start_ironbar().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
async fn run_with_args() {
|
||||||
|
let args = cli::Args::parse();
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Some(command) => {
|
||||||
|
let ipc = ipc::Ipc::new();
|
||||||
|
match ipc.send(command).await {
|
||||||
|
Ok(res) => cli::handle_response(res),
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => start_ironbar().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_ironbar() {
|
||||||
info!("Ironbar version {}", VERSION);
|
info!("Ironbar version {}", VERSION);
|
||||||
info!("Starting application");
|
info!("Starting application");
|
||||||
|
|
||||||
|
@ -64,6 +100,13 @@ async fn main() {
|
||||||
|
|
||||||
running.set(true);
|
running.set(true);
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ipc")] {
|
||||||
|
let ipc = ipc::Ipc::new();
|
||||||
|
ipc.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let display = Display::default().map_or_else(
|
let display = Display::default().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
let report = Report::msg("Failed to get default GTK display");
|
let report = Report::msg("Failed to get default GTK display");
|
||||||
|
@ -78,7 +121,7 @@ async fn main() {
|
||||||
ConfigLoader::load,
|
ConfigLoader::load,
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = match config_res {
|
let mut config: Config = match config_res {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
|
@ -88,6 +131,16 @@ async fn main() {
|
||||||
|
|
||||||
debug!("Loaded config file");
|
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, wayland_client, &config) {
|
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(ExitCode::CreateBars as i32);
|
exit(ExitCode::CreateBars as i32);
|
||||||
|
@ -112,14 +165,27 @@ async fn main() {
|
||||||
if style_path.exists() {
|
if style_path.exists() {
|
||||||
load_css(style_path);
|
load_css(style_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
rx.recv().expect("to receive from channel");
|
||||||
|
|
||||||
|
info!("Shutting down");
|
||||||
|
|
||||||
|
#[cfg(feature = "ipc")]
|
||||||
|
ipc.shutdown();
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
|
||||||
|
.expect("Error setting Ctrl-C handler");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ignore CLI args
|
// Ignore CLI args
|
||||||
// Some are provided by swaybar_config but not currently supported
|
// Some are provided by swaybar_config but not currently supported
|
||||||
app.run_with_args(&Vec::<&str>::new());
|
app.run_with_args(&Vec::<&str>::new());
|
||||||
|
|
||||||
info!("Shutting down");
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates each of the bars across each of the (configured) outputs.
|
/// Creates each of the bars across each of the (configured) outputs.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
use crate::{build, try_send};
|
use crate::{build, try_send};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
@ -25,7 +25,7 @@ impl CustomWidget for ButtonWidget {
|
||||||
label.set_use_markup(true);
|
label.set_use_markup(true);
|
||||||
button.add(&label);
|
button.add(&label);
|
||||||
|
|
||||||
DynamicString::new(&text, move |string| {
|
dynamic_string(&text, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{CustomWidget, CustomWidgetContext};
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
use crate::build;
|
use crate::build;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::image::ImageProvider;
|
use crate::image::ImageProvider;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Image;
|
use gtk::Image;
|
||||||
|
@ -29,7 +29,7 @@ impl CustomWidget for ImageWidget {
|
||||||
let gtk_image = gtk_image.clone();
|
let gtk_image = gtk_image.clone();
|
||||||
let icon_theme = context.icon_theme.clone();
|
let icon_theme = context.icon_theme.clone();
|
||||||
|
|
||||||
DynamicString::new(&self.src, move |src| {
|
dynamic_string(&self.src, move |src| {
|
||||||
ImageProvider::parse(&src, &icon_theme, 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()));
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{CustomWidget, CustomWidgetContext};
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
use crate::build;
|
use crate::build;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -22,7 +22,7 @@ impl CustomWidget for LabelWidget {
|
||||||
|
|
||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
DynamicString::new(&self.label, move |string| {
|
dynamic_string(&self.label, move |string| {
|
||||||
label.set_markup(&string);
|
label.set_markup(&string);
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::modules::custom::set_length;
|
use crate::modules::custom::set_length;
|
||||||
use crate::script::{OutputStream, Script, ScriptInput};
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
use crate::{build, send};
|
use crate::{build, send};
|
||||||
|
@ -69,7 +69,7 @@ impl CustomWidget for ProgressWidget {
|
||||||
let progress = progress.clone();
|
let progress = progress.clone();
|
||||||
progress.set_show_text(true);
|
progress.set_show_text(true);
|
||||||
|
|
||||||
DynamicString::new(&text, move |string| {
|
dynamic_string(&text, move |string| {
|
||||||
progress.set_text(Some(&string));
|
progress.set_text(Some(&string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::dynamic_string::DynamicString;
|
use crate::dynamic_value::dynamic_string;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, 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;
|
||||||
|
@ -31,7 +31,7 @@ impl Module<Label> for LabelModule {
|
||||||
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
DynamicString::new(&self.label, move |string| {
|
dynamic_string(&self.label, move |string| {
|
||||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::send;
|
use crate::send;
|
||||||
use color_eyre::{Help, Report};
|
use color_eyre::{Help, Report};
|
||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
|
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||||
use gtk::prelude::CssProviderExt;
|
use gtk::prelude::CssProviderExt;
|
||||||
use gtk::{gdk, gio, CssProvider, StyleContext};
|
use gtk::{gdk, gio, CssProvider, StyleContext};
|
||||||
use notify::event::{DataChange, ModifyKind};
|
use notify::event::{DataChange, ModifyKind};
|
||||||
|
@ -29,7 +30,11 @@ pub fn load_css(style_path: PathBuf) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||||
StyleContext::add_provider_for_screen(&screen, &provider, 800);
|
StyleContext::add_provider_for_screen(
|
||||||
|
&screen,
|
||||||
|
&provider,
|
||||||
|
GTK_STYLE_PROVIDER_PRIORITY_USER as u32,
|
||||||
|
);
|
||||||
|
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue