diff --git a/Cargo.lock b/Cargo.lock index 86b034f..0d17d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,55 @@ dependencies = [ "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]] name = "anyhow" version = "1.0.71" @@ -395,6 +444,48 @@ dependencies = [ "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]] name = "color-eyre" version = "0.6.2" @@ -422,6 +513,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -515,6 +612,16 @@ dependencies = [ "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]] name = "darling" version = "0.14.4" @@ -1475,7 +1582,9 @@ dependencies = [ "async_once", "cfg-if", "chrono", + "clap", "color-eyre", + "ctrlc", "dirs", "futures-lite", "futures-util", @@ -1492,6 +1601,7 @@ dependencies = [ "regex", "reqwest", "serde", + "serde_json", "smithay-client-toolkit", "stray", "strip-ansi-escapes", @@ -1511,6 +1621,18 @@ dependencies = [ "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]] name = "itoa" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 9d97cad..746e5e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,12 @@ version = "0.13.0" edition = "2021" license = "MIT" description = "Customisable GTK Layer Shell wlroots/sway bar" +repository = "https://github.com/jakestanger/ironbar" [features] default = [ + "cli", + "ipc", "http", "config+all", "clipboard", @@ -17,8 +20,11 @@ default = [ "upower", "workspaces+all" ] + +cli = ["dep:clap", "ipc"] +ipc = ["dep:serde_json"] + http = ["dep:reqwest"] -upower = ["upower_dbus", "zbus", "futures-lite"] "config+all" = ["config+json", "config+yaml", "config+toml", "config+corn", "config+ron"] "config+json" = ["universal-config/json"] @@ -40,6 +46,8 @@ sys_info = ["sysinfo", "regex"] tray = ["stray"] +upower = ["upower_dbus", "zbus", "futures-lite"] + workspaces = ["futures-util"] "workspaces+all" = ["workspaces", "workspaces+sway", "workspaces+hyprland"] "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"] } smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] } universal-config = { version = "0.4.0", default_features = false } +ctrlc = "3.4.0" lazy_static = "1.4.0" async_once = "0.2.6" cfg-if = "1.0.0" +# cli +clap = { version = "4.2.7", optional = true, features = ["derive"] } + +# ipc +serde_json = { version = "1.0.96", optional = true } + # http reqwest = { version = "0.11.18", optional = true } diff --git a/docs/Compiling.md b/docs/Compiling.md index 10a79a7..19a8ab2 100644 --- a/docs/Compiling.md +++ b/docs/Compiling.md @@ -58,6 +58,8 @@ cargo build --release --no-default-features \ |---------------------|-----------------------------------------------------------------------------------| | **Core** | | | 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+json | Enables configuration support for JSON. | | config+yaml | Enables configuration support for YAML. | diff --git a/docs/Configuration guide.md b/docs/Configuration guide.md index a4e1684..1ebf69b 100644 --- a/docs/Configuration guide.md +++ b/docs/Configuration guide.md @@ -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: -| Name | Type | Default | Description | -|-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------| -| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | -| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | -| `height` | `integer` | `42` | The bar's height in pixels. | -| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. | -| `margin.top` | `integer` | `0` | The margin on the top of the bar | -| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | -| `margin.left` | `integer` | `0` | The margin on the left of the bar | -| `margin.right` | `integer` | `0` | The margin on the right of the bar | -| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | -| `start` | `Module[]` | `[]` | Array of left or top modules. | -| `center` | `Module[]` | `[]` | Array of center modules. | -| `end` | `Module[]` | `[]` | Array of right or bottom modules. | +| Name | Type | Default | Description | +|--------------------|----------------------------------------|-----------|-----------------------------------------------------------------------------------------| +| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | +| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | +| `height` | `integer` | `42` | The bar's height in pixels. | +| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. | +| `margin.top` | `integer` | `0` | The margin on the top of the bar | +| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | +| `margin.left` | `integer` | `0` | The margin on the left of the bar | +| `margin.right` | `integer` | `0` | The margin on the right of the bar | +| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | +| `ironvar_defaults` | `Map` | `{}` | Map of [ironvar](ironvars) keys against their default values. | +| `start` | `Module[]` | `[]` | Array of left or top modules. | +| `center` | `Module[]` | `[]` | Array of center modules. | +| `end` | `Module[]` | `[]` | Array of right or bottom modules. | ### 3.2 Module-level options @@ -306,9 +307,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her | 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_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 diff --git a/docs/Controlling Ironbar.md b/docs/Controlling Ironbar.md new file mode 100644 index 0000000..2b97b37 --- /dev/null +++ b/docs/Controlling Ironbar.md @@ -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" +} +``` \ No newline at end of file diff --git a/docs/Dynamic values.md b/docs/Dynamic values.md new file mode 100644 index 0000000..371b0e8 --- /dev/null +++ b/docs/Dynamic values.md @@ -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 +``` \ No newline at end of file diff --git a/docs/Ironvars.md b/docs/Ironvars.md new file mode 100644 index 0000000..ae75806 --- /dev/null +++ b/docs/Ironvars.md @@ -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. \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index e4583bf..2553fe8 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -2,10 +2,16 @@ - [Compiling from source](compiling) - [Configuration guide](configuration-guide) - - [Scripts](scripts) - [Images](images) - [Styling guide](styling-guide) +# Dynamic content + +- [Controlling Ironbar](controlling-ironbar) +- [Dynamic values](dynamic-values) +- [Scripts](scripts) +- [Ironvars](ironvars) + # Examples - [Config](config) @@ -28,4 +34,4 @@ - [Sys_Info](sys-info) - [Tray](tray) - [Upower](upower) -- [Workspaces](workspaces) \ No newline at end of file +- [Workspaces](workspaces) diff --git a/docs/modules/Clipboard.md b/docs/modules/Clipboard.md index fa3f35e..3daa5be 100644 --- a/docs/modules/Clipboard.md +++ b/docs/modules/Clipboard.md @@ -9,17 +9,15 @@ Supports plain text and images. > Type: `clipboard` -| Name | Type | Default | Description | -|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| -| `icon` | `string/image` | `󰨸` | Icon to show on the widget button. | -| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | -| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. | -| `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.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. | - -See [here](images) for information on images. +| Name | Type | Default | Description | +|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `icon` | `string` or [image](images) | `󰨸` | Icon to show on the widget button. | +| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | +| `max_items` | `integer` | `10` | Maximum number of items to show in the popup. | +| `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.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. |
JSON diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md index bd42ec4..298e0a3 100644 --- a/docs/modules/Custom.md +++ b/docs/modules/Custom.md @@ -1,6 +1,9 @@ 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. +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. + ![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://f.jstanger.dev/github/ironbar/custom-power-menu.png?raw) ## 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. You can also add common [module-level options](https://github.com/JakeStanger/ironbar/wiki/configuration-guide#32-module-level-options) on a widget. -| Name | Type | Default | Description | -|---------|-------------------------------------------------------------------|---------|-------------------------------| -| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. | -| `name` | `string` | `null` | Widget name. | -| `class` | `string` | `null` | Widget class name. | +| Name | Type | Default | Description | +|---------|-------------------------------------------------------------------------------|---------|-------------------------------| +| `type` | `'box'` or `'label'` or `'button'` or `'image'` or `'slider'` or `'progress'` | `null` | Type of GTK widget to create. | +| `name` | `string` | `null` | Widget name. | +| `class` | `string` | `null` | Widget class name. | #### Box @@ -30,20 +33,20 @@ A container to place nested widgets inside. > Type: `box` -| Name | Type | Default | Description | -|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------| -| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. | -| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. | +| Name | Type | Default | Description | +|---------------|------------------------------------------------------------|----------------|-------------------------------------------------------------------| +| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Whether child widgets should be horizontally or vertically added. | +| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. | #### Label -A text label. Pango markup and embedded scripts are supported. +A text label. Pango markup is supported. > Type `label` -| Name | Type | Default | Description | -|---------|----------|--------------|---------------------------------------------------------------------| -| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | +| Name | Type | Default | Description | +|---------|-------------------------------------------------|---------|---------------------------------------------------------------------| +| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Widget text label. Pango markup and embedded scripts are supported. | #### Button @@ -51,10 +54,10 @@ A clickable button, which can run a command when clicked. > Type `button` -| Name | Type | Default | Description | -|------------|--------------------|--------------|---------------------------------------------------------------------| -| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | -| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). | +| Name | Type | Default | Description | +|------------|-------------------------------------------------|---------|---------------------------------------------------------------------| +| `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). | #### Image @@ -62,10 +65,10 @@ An image or icon from disk or http. > Type `image` -| Name | Type | Default | Description | -|--------|-----------|---------|---------------------------------------------------------------------------------------------| -| `src` | `image` | `null` | Image source. See [here](images) for information on images. Embedded scripts are supported. | -| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | +| Name | Type | Default | Description | +|--------|---------------------------------------------------------------------|---------|-------------------------------------------------------| +| `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. | #### Slider @@ -76,18 +79,16 @@ A draggable slider. Note that `on_change` will provide the **floating point** value as an argument. If your input program requires an integer, you will need to round it. -| Name | Type | Default | Description | -|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------| -| `src` | `image` | `null` | Image source. See [here](images) for information on images. | -| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | -| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. | -| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. | -| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). | -| `min` | `float` | `0` | Minimum slider value. | -| `max` | `float` | `100` | Maximum slider value. | -| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. | -| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | -| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. | +| Name | Type | Default | Description | +|---------------|------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------| +| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the slider. | +| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. | +| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). | +| `min` | `float` | `0` | Minimum slider value. | +| `max` | `float` | `100` | Maximum slider value. | +| `step` | `float` | - | The increment to change when scrolling with the mouse wheel. If left blank, will use the default determined by the environment. | +| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | +| `show_label` | `boolean` | `true` | Whether to show the value label above the slider. | The example slider widget below shows a volume control for MPC, which updates the server when changed, and polls the server for volume changes to keep the slider in sync. @@ -115,14 +116,12 @@ A progress bar. Note that `value` expects a numeric value **between 0-`max`** as output. -| Name | Type | Default | Description | -|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------| -| `src` | `image` | `null` | Image source. See [here](images) for information on images. | -| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | -| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. | -| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. | -| `max` | `float` | `100` | Maximum progress bar value. | -| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | +| Name | Type | Default | Description | +|---------------|------------------------------------------------------------|--------------|---------------------------------------------------------------------------------| +| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `horizontal` | Orientation of the progress bar. | +| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. | +| `max` | `float` | `100` | Maximum progress bar value. | +| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | The example below shows progress for the current playing song in MPD, and displays the elapsed/length timestamps as a label above: diff --git a/docs/modules/Focused.md b/docs/modules/Focused.md index 4172e5f..bacf72e 100644 --- a/docs/modules/Focused.md +++ b/docs/modules/Focused.md @@ -7,15 +7,15 @@ Displays the title and/or icon of the currently focused window. > Type: `focused` -| Name | Type | Default | Description | -|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| -| `show_icon` | `boolean` | `true` | Whether to show the app's icon | -| `show_title` | `boolean` | `true` | Whether to show the app's title | -| `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.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.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | +| Name | Type | Default | Description | +|-----------------------|---------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `show_icon` | `boolean` | `true` | Whether to show the app's icon. | +| `show_title` | `boolean` | `true` | Whether to show the app's title. | +| `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.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.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
JSON diff --git a/docs/modules/Label.md b/docs/modules/Label.md index 2196c19..c203554 100644 --- a/docs/modules/Label.md +++ b/docs/modules/Label.md @@ -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 > Type: `label` -| Name | Type | Default | Description | -|---------|----------|---------|-----------------------------------------| -| `label` | `string` | `null` | Text, optionally with embedded scripts. | +| Name | Type | Default | Description | +|---------|-------------------------------------------------|---------|------------------------| +| `label` | [Dynamic String](dynamic-values#dynamic-string) | `null` | Text to show on label. |
JSON diff --git a/docs/modules/Music.md b/docs/modules/Music.md index 2f912ee..959aad3 100644 --- a/docs/modules/Music.md +++ b/docs/modules/Music.md @@ -11,27 +11,27 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di > Type: `music` -| | Type | Default | Description | -|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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. | -| `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.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. | -| `icons.play` | `string/image` | `` | Icon to show when playing. | -| `icons.pause` | `string/image` | `` | Icon to show when paused. | -| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. | -| `icons.next` | `string/image` | `怜` | Icon to show on next button. | -| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. | -| `icons.track` | `string/image` | `` | Icon to show next to track title. | -| `icons.album` | `string/image` | `` | Icon to show next to album name. | -| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. | -| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. | -| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | -| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. | -| `host` | `string` | `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. | +| | Type | Default | Description | +|-----------------------|---------------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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. | +| `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.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. | +| `icons.play` | `string` or [image](images) | `` | Icon to show when playing. | +| `icons.pause` | `string` or [image](images) | `` | Icon to show when paused. | +| `icons.prev` | `string` or [image](images) | `玲` | Icon to show on previous button. | +| `icons.next` | `string` or [image](images) | `怜` | Icon to show on next button. | +| `icons.volume` | `string` or [image](images) | `墳` | Icon to show under popup volume slider. | +| `icons.track` | `string` or [image](images) | `` | Icon to show next to track title. | +| `icons.album` | `string` or [image](images) | `` | Icon to show next to album name. | +| `icons.artist` | `string` or [image](images) | `ﴁ` | Icon to show next to artist name. | +| `show_status_icon` | `boolean` | `true` | Whether to show the play/pause icon on the widget. | +| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | +| `cover_image_size` | `integer` | `128` | Size to render album art image at inside popup. | +| `host` | `string` | `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. | See [here](images) for information on images. diff --git a/docs/modules/Script.md b/docs/modules/Script.md index 8adea5b..afab086 100644 --- a/docs/modules/Script.md +++ b/docs/modules/Script.md @@ -1,6 +1,9 @@ Executes a script and shows the result of `stdout` on a label. 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 > Type: `script` diff --git a/docs/modules/Workspaces.md b/docs/modules/Workspaces.md index af91e50..5963c6e 100644 --- a/docs/modules/Workspaces.md +++ b/docs/modules/Workspaces.md @@ -8,12 +8,12 @@ Shows all current workspaces. Clicking a workspace changes focus to it. > Type: `workspaces` -| Name | Type | Default | Description | -|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name_map` | `Map` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | -| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | -| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. | -| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. | +| Name | Type | Default | Description | +|----------------|--------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name_map` | `Map` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | +| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | +| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. | +| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
JSON diff --git a/src/bridge_channel.rs b/src/bridge_channel.rs index 2208e0c..101eb10 100644 --- a/src/bridge_channel.rs +++ b/src/bridge_channel.rs @@ -2,7 +2,7 @@ use crate::send; use tokio::spawn; use tokio::sync::mpsc; -/// MPSC async -> sync channel. +/// MPSC async -> GTK sync channel. /// The sender uses `tokio::sync::mpsc` /// while the receiver uses `glib::MainContext::channel`. /// diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..feb2933 --- /dev/null +++ b/src/cli/mod.rs @@ -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, +} + +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()), + } +} diff --git a/src/config/common.rs b/src/config/common.rs index 3bfe085..b22b38e 100644 --- a/src/config/common.rs +++ b/src/config/common.rs @@ -1,11 +1,9 @@ -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::{dynamic_string, DynamicBool}; use crate::script::{Script, ScriptInput}; -use crate::send; use gtk::gdk::ScrollDirection; use gtk::prelude::*; use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType}; use serde::Deserialize; -use tokio::spawn; use tracing::trace; /// Common configuration options @@ -15,7 +13,7 @@ pub struct CommonConfig { pub class: Option, pub name: Option, - pub show_if: Option, + pub show_if: Option, pub transition_type: Option, pub transition_duration: Option, @@ -114,7 +112,7 @@ impl CommonConfig { if let Some(tooltip) = self.tooltip { let container = container.clone(); - DynamicString::new(&tooltip, move |string| { + dynamic_string(&tooltip, move |string| { container.set_tooltip_text(Some(&string)); Continue(true) }); @@ -127,23 +125,13 @@ impl CommonConfig { container.show_all(); }, |show_if| { - let script = Script::new_polling(show_if); let container = container.clone(); - let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); - - spawn(async move { - script - .run(None, |_, success| { - send!(tx, success); - }) - .await; - }); { let revealer = revealer.clone(); let container = container.clone(); - rx.attach(None, move |success| { + show_if.subscribe(move |success| { if success { container.show_all(); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 1bc04c6..0ed2652 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -100,6 +100,8 @@ pub struct Config { /// GTK icon theme to use. pub icon_theme: Option, + pub ironvar_defaults: Option, String>>, + pub start: Option>, pub center: Option>, pub end: Option>, diff --git a/src/dynamic_string.rs b/src/dynamic_string.rs deleted file mode 100644 index b27e2d3..0000000 --- a/src/dynamic_string.rs +++ /dev/null @@ -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(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 { - if !input.contains("{{") { - return vec![DynamicStringSegment::Static(input.to_string())]; - } - - let mut segments = vec![]; - - let mut chars = input.chars().collect::>(); - 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::(); - - 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::(); - - // 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) - }, - ); - } - } -} diff --git a/src/dynamic_value/dynamic_bool.rs b/src/dynamic_value/dynamic_bool.rs new file mode 100644 index 0000000..094408f --- /dev/null +++ b/src/dynamic_value/dynamic_bool.rs @@ -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), +} + +impl DynamicBool { + pub fn subscribe(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") +} diff --git a/src/dynamic_value/dynamic_string.rs b/src/dynamic_value/dynamic_string.rs new file mode 100644 index 0000000..d9332f0 --- /dev/null +++ b/src/dynamic_value/dynamic_string.rs @@ -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), +} + +/// 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(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 { + // 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::>(); + 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::(); + + 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::(); + + 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::(); + + // 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") + ); + } +} diff --git a/src/dynamic_value/mod.rs b/src/dynamic_value/mod.rs new file mode 100644 index 0000000..83ad88c --- /dev/null +++ b/src/dynamic_value/mod.rs @@ -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; diff --git a/src/ipc/client.rs b/src/ipc/client.rs new file mode 100644 index 0000000..b7e1da9 --- /dev/null +++ b/src/ipc/client.rs @@ -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 { + 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) + } +} diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs new file mode 100644 index 0000000..e5712fb --- /dev/null +++ b/src/ipc/commands.rs @@ -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, + /// Variable value. Can be any valid UTF-8 string. + value: String, + }, + + /// Get the current value of an `ironvar`. + Get { + /// Variable key. + key: Box, + }, + + /// Load an additional CSS stylesheet. + /// The sheet is automatically hot-reloaded. + LoadCss { + /// The path to the sheet. + path: PathBuf, + }, +} diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs new file mode 100644 index 0000000..87aab89 --- /dev/null +++ b/src/ipc/mod.rs @@ -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, + } + } +} diff --git a/src/ipc/responses.rs b/src/ipc/responses.rs new file mode 100644 index 0000000..df5a802 --- /dev/null +++ b/src/ipc/responses.rs @@ -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 }, +} + +impl Response { + /// Creates a new `Response::Error`. + pub fn error(message: &str) -> Self { + Self::Err { + message: Some(message.to_string()), + } + } +} diff --git a/src/ipc/server.rs b/src/ipc/server.rs new file mode 100644 index 0000000..7aebde3 --- /dev/null +++ b/src/ipc/server.rs @@ -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::::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, + res_rx: &mut Receiver, + ) -> 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::(&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(); + } +} diff --git a/src/ironvar.rs b/src/ironvar.rs new file mode 100644 index 0000000..a83f07f --- /dev/null +++ b/src/ironvar.rs @@ -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> = arc_rw!(VariableManager::new()); +} + +pub fn get_variable_manager() -> Arc> { + VARIABLE_MANAGER.clone() +} + +/// Global singleton manager for `IronVar` variables. +pub struct VariableManager { + variables: HashMap, 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, 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 { + 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) -> broadcast::Receiver> { + 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, + tx: broadcast::Sender>, + _rx: broadcast::Receiver>, +} + +impl IronVar { + /// Creates a new variable. + fn new(value: Option) -> 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 { + self.value.clone() + } + + /// Sets the current variable value. + /// The change is broadcast to all receivers. + fn set(&mut self, value: Option) { + 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> { + let rx = self.tx.subscribe(); + send!(self.tx, self.value.clone()); + rx + } +} diff --git a/src/macros.rs b/src/macros.rs index 49e91a2..fa2ea30 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,7 +1,7 @@ /// Sends a message on an asynchronous `Sender` using `send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// send_async!(tx, "my message"); @@ -16,7 +16,7 @@ macro_rules! send_async { /// Sends a message on an synchronous `Sender` using `send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// send!(tx, "my message"); @@ -31,7 +31,7 @@ macro_rules! send { /// Sends a message on an synchronous `Sender` using `try_send()` /// Panics if the message cannot be sent. /// -/// Usage: +/// # Usage: /// /// ```rs /// try_send!(tx, "my message"); @@ -46,7 +46,7 @@ macro_rules! try_send { /// Locks a `Mutex`. /// Panics if the `Mutex` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let mut val = lock!(my_mutex); @@ -62,7 +62,7 @@ macro_rules! lock { /// Gets a read lock on a `RwLock`. /// Panics if the `RwLock` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let val = read_lock!(my_rwlock); @@ -77,7 +77,7 @@ macro_rules! read_lock { /// Gets a write lock on a `RwLock`. /// Panics if the `RwLock` cannot be locked. /// -/// Usage: +/// # Usage: /// /// ```rs /// let mut val = write_lock!(my_rwlock); @@ -88,3 +88,33 @@ macro_rules! write_lock { $rwlock.write().expect($crate::error::ERR_WRITE_LOCK) }; } + +/// Wraps `val` in a new `Arc>`. +/// +/// # 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>`. +/// +/// # 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)) + }; +} diff --git a/src/main.rs b/src/main.rs index 8523a52..6cd5022 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,19 @@ mod bar; mod bridge_channel; +#[cfg(feature = "cli")] +mod cli; mod clients; mod config; mod desktop_file; -mod dynamic_string; +mod dynamic_value; mod error; mod gtk_helpers; mod image; +#[cfg(feature = "ipc")] +mod ipc; +#[cfg(feature = "ipc")] +mod ironvar; mod logging; mod macros; mod modules; @@ -20,6 +26,9 @@ mod unique_id; use crate::bar::create_bar; use crate::config::{Config, MonitorConfig}; use crate::style::load_css; +use cfg_if::cfg_if; +#[cfg(feature = "cli")] +use clap::Parser; use color_eyre::eyre::Result; use color_eyre::Report; use dirs::config_dir; @@ -32,8 +41,9 @@ use std::future::Future; use std::path::PathBuf; use std::process::exit; use std::rc::Rc; +use std::sync::mpsc; use tokio::runtime::Handle; -use tokio::task::block_in_place; +use tokio::task::{block_in_place, spawn_blocking}; use crate::error::ExitCode; use clients::wayland::{self, WaylandClient}; @@ -47,6 +57,32 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); async fn main() { 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!("Starting application"); @@ -64,6 +100,13 @@ async fn main() { running.set(true); + cfg_if! { + if #[cfg(feature = "ipc")] { + let ipc = ipc::Ipc::new(); + ipc.start(); + } + } + let display = Display::default().map_or_else( || { let report = Report::msg("Failed to get default GTK display"); @@ -78,7 +121,7 @@ async fn main() { ConfigLoader::load, ); - let config = match config_res { + let mut config: Config = match config_res { Ok(config) => config, Err(err) => { error!("{:?}", err); @@ -88,6 +131,16 @@ async fn main() { 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) { error!("{:?}", err); exit(ExitCode::CreateBars as i32); @@ -112,14 +165,27 @@ async fn main() { if style_path.exists() { load_css(style_path); } + + let (tx, rx) = mpsc::channel(); + + spawn_blocking(move || { + rx.recv().expect("to receive from channel"); + + info!("Shutting down"); + + #[cfg(feature = "ipc")] + ipc.shutdown(); + + exit(0); + }); + + ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) + .expect("Error setting Ctrl-C handler"); }); // Ignore CLI args // Some are provided by swaybar_config but not currently supported app.run_with_args(&Vec::<&str>::new()); - - info!("Shutting down"); - exit(0); } /// Creates each of the bars across each of the (configured) outputs. diff --git a/src/modules/custom/button.rs b/src/modules/custom/button.rs index bb67a88..f9ea7a2 100644 --- a/src/modules/custom/button.rs +++ b/src/modules/custom/button.rs @@ -1,5 +1,5 @@ use super::{CustomWidget, CustomWidgetContext, ExecEvent}; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::popup::Popup; use crate::{build, try_send}; use gtk::prelude::*; @@ -25,7 +25,7 @@ impl CustomWidget for ButtonWidget { label.set_use_markup(true); button.add(&label); - DynamicString::new(&text, move |string| { + dynamic_string(&text, move |string| { label.set_markup(&string); Continue(true) }); diff --git a/src/modules/custom/image.rs b/src/modules/custom/image.rs index 3ae11fb..edbcc26 100644 --- a/src/modules/custom/image.rs +++ b/src/modules/custom/image.rs @@ -1,6 +1,6 @@ use super::{CustomWidget, CustomWidgetContext}; use crate::build; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use crate::image::ImageProvider; use gtk::prelude::*; use gtk::Image; @@ -29,7 +29,7 @@ impl CustomWidget for ImageWidget { let gtk_image = gtk_image.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) .map(|image| image.load_into_image(gtk_image.clone())); diff --git a/src/modules/custom/label.rs b/src/modules/custom/label.rs index 4b9d682..8fa3d27 100644 --- a/src/modules/custom/label.rs +++ b/src/modules/custom/label.rs @@ -1,6 +1,6 @@ use super::{CustomWidget, CustomWidgetContext}; use crate::build; -use crate::dynamic_string::DynamicString; +use crate::dynamic_value::dynamic_string; use gtk::prelude::*; use gtk::Label; use serde::Deserialize; @@ -22,7 +22,7 @@ impl CustomWidget for LabelWidget { { let label = label.clone(); - DynamicString::new(&self.label, move |string| { + dynamic_string(&self.label, move |string| { label.set_markup(&string); Continue(true) }); diff --git a/src/modules/custom/progress.rs b/src/modules/custom/progress.rs index d6bd285..c7d1eba 100644 --- a/src/modules/custom/progress.rs +++ b/src/modules/custom/progress.rs @@ -1,5 +1,5 @@ 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::script::{OutputStream, Script, ScriptInput}; use crate::{build, send}; @@ -69,7 +69,7 @@ impl CustomWidget for ProgressWidget { let progress = progress.clone(); progress.set_show_text(true); - DynamicString::new(&text, move |string| { + dynamic_string(&text, move |string| { progress.set_text(Some(&string)); Continue(true) }); diff --git a/src/modules/label.rs b/src/modules/label.rs index 0ca67c7..a10a362 100644 --- a/src/modules/label.rs +++ b/src/modules/label.rs @@ -1,5 +1,5 @@ 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::try_send; use color_eyre::Result; @@ -31,7 +31,7 @@ impl Module