1 Commits

Author SHA1 Message Date
Jake Stanger
c73585324c start porting to gtk4 2023-04-30 15:35:40 +01:00
62 changed files with 1783 additions and 2467 deletions

147
Cargo.lock generated
View File

@@ -251,12 +251,6 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.0" version = "0.21.0"
@@ -1518,7 +1512,7 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
[[package]] [[package]]
name = "ironbar" name = "ironbar"
version = "0.12.0" version = "0.11.0"
dependencies = [ dependencies = [
"async_once", "async_once",
"cfg-if", "cfg-if",
@@ -1555,7 +1549,6 @@ dependencies = [
"walkdir", "walkdir",
"wayland-client", "wayland-client",
"wayland-protocols", "wayland-protocols",
"wayland-protocols-wlr",
"zbus 3.11.1", "zbus 3.11.1",
] ]
@@ -1825,6 +1818,18 @@ dependencies = [
"memoffset 0.6.5", "memoffset 0.6.5",
] ]
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset 0.6.5",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.25.1" version = "0.25.1"
@@ -2211,15 +2216,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-xml"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.3.15" version = "0.3.15"
@@ -2348,7 +2344,7 @@ version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
dependencies = [ dependencies = [
"base64 0.21.0", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -2379,17 +2375,6 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "ron"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
"serde",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.22" version = "0.1.22"
@@ -2449,12 +2434,6 @@ dependencies = [
"windows-sys 0.42.0", "windows-sys 0.42.0",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -2653,9 +2632,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]] [[package]]
name = "smithay-client-toolkit" name = "smithay-client-toolkit"
version = "0.17.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1476c3d89bb67079264b88aaf4f14358353318397e083b7c4e8c14517f55de7" checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"calloop", "calloop",
@@ -2663,14 +2642,11 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"memmap2", "memmap2",
"nix 0.26.2", "nix 0.24.3",
"thiserror", "pkg-config",
"wayland-backend",
"wayland-client", "wayland-client",
"wayland-cursor", "wayland-cursor",
"wayland-protocols", "wayland-protocols",
"wayland-protocols-wlr",
"wayland-scanner",
] ]
[[package]] [[package]]
@@ -3210,13 +3186,12 @@ checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
[[package]] [[package]]
name = "universal-config" name = "universal-config"
version = "0.4.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923efc70bfb900a20e1c86fa462fad94c336ef212de25589ef9924fb5e6cd2b6" checksum = "5dae262d0546bed6c0955faad12c07ec989768c2550643609410178fddb39909"
dependencies = [ dependencies = [
"dirs 5.0.0", "dirs 5.0.0",
"libcorn", "libcorn",
"ron",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -3415,88 +3390,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]] [[package]]
name = "wayland-backend" name = "wayland-client"
version = "0.1.2" version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b48e27457e8da3b2260ac60d0a94512f5cba36448679f3747c0865b7893ed8" checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715"
dependencies = [ dependencies = [
"cc", "bitflags 1.3.2",
"downcast-rs", "downcast-rs",
"io-lifetimes", "libc",
"nix 0.26.2", "nix 0.24.3",
"scoped-tls", "wayland-commons",
"wayland-scanner",
"wayland-sys",
]
[[package]]
name = "wayland-commons"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902"
dependencies = [
"nix 0.24.3",
"once_cell",
"smallvec", "smallvec",
"wayland-sys", "wayland-sys",
] ]
[[package]]
name = "wayland-client"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bde68449abab1a808e5227b6e295f4ae3680911eb7711b4a2cb90141edb780"
dependencies = [
"bitflags 1.3.2",
"calloop",
"nix 0.26.2",
"wayland-backend",
"wayland-scanner",
]
[[package]] [[package]]
name = "wayland-cursor" name = "wayland-cursor"
version = "0.30.0" version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d0c3a0d5b4b688b07b0442362d3ed6bf04724fcc16cd69ab6285b90dbc487aa" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661"
dependencies = [ dependencies = [
"nix 0.26.2", "nix 0.24.3",
"wayland-client", "wayland-client",
"xcursor", "xcursor",
] ]
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.30.0" version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fefbeb8a360abe67ab7c2efe1d297a1a50ee011f5460791bc18870c26bb84e2" checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"wayland-backend",
"wayland-client", "wayland-client",
"wayland-scanner", "wayland-commons",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fce991093320e4a6a525876e6b629ab24da25f9baef0c2e0080ad173ec89588a"
dependencies = [
"bitflags 1.3.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner", "wayland-scanner",
] ]
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.30.0" version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4834c14b3edf1d9986c83ca79b1e7e3afbe9874c7c144702f6467063259ce45d" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml",
"quote 1.0.26", "quote 1.0.26",
"xml-rs",
] ]
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.30.1" version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4"
dependencies = [ dependencies = [
"dlib",
"log",
"pkg-config", "pkg-config",
] ]
@@ -3724,6 +3683,12 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "2.3.2" version = "2.3.2"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironbar" name = "ironbar"
version = "0.12.0" version = "0.11.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"
@@ -20,12 +20,11 @@ default = [
http = ["dep:reqwest"] http = ["dep:reqwest"]
upower = ["upower_dbus", "zbus", "futures-lite"] 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+json" = ["universal-config/json"] "config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"] "config+yaml" = ["universal-config/yaml"]
"config+toml" = ["universal-config/toml"] "config+toml" = ["universal-config/toml"]
"config+corn" = ["universal-config/corn"] "config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
clipboard = ["nix"] clipboard = ["nix"]
@@ -47,9 +46,9 @@ workspaces = ["futures-util"]
[dependencies] [dependencies]
# core # core
gtk = "0.17.0" gtk = { package = "gtk4", version = "0.6.6" }
gtk-layer-shell = "0.6.0" gtk-layer-shell = { package = "gtk4-layer-shell", version = "0.0.3" }
glib = "0.17.5" glib = "0.17.9"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] } tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process", "sync", "io-util", "net"] }
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
@@ -62,11 +61,10 @@ indexmap = "1.9.1"
dirs = "5.0.0" dirs = "5.0.0"
walkdir = "2.3.2" walkdir = "2.3.2"
notify = { version = "5.0.0", default-features = false } notify = { version = "5.0.0", default-features = false }
wayland-client = "0.30.0" wayland-client = "0.29.5"
wayland-protocols = { version = "0.30.0", features = ["unstable", "client"] } wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
wayland-protocols-wlr = { version = "0.1.0", features = ["client"] } smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
smithay-client-toolkit = { version = "0.17.0", default-features = false, features = ["calloop"] } universal-config = { version = "0.3.0", default_features = false }
universal-config = { version = "0.4.0", default_features = false }
lazy_static = "1.4.0" lazy_static = "1.4.0"
async_once = "0.2.6" async_once = "0.2.6"
@@ -76,7 +74,7 @@ cfg-if = "1.0.0"
reqwest = { version = "0.11.14", optional = true } reqwest = { version = "0.11.14", optional = true }
# clipboard # clipboard
nix = { version = "0.26.2", optional = true, features = ["event"] } nix = { version = "0.26.2", optional = true }
# clock # clock
chrono = { version = "0.4.19", optional = true } chrono = { version = "0.4.19", optional = true }

View File

@@ -12,7 +12,6 @@ install target/release/ironbar ~/.local/bin/ironbar
## Build requirements ## Build requirements
To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed. To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
You also need rust; only the latest stable version is supported.
### Arch ### Arch
@@ -23,9 +22,7 @@ pacman -S gtk3 gtk-layer-shell
### Ubuntu/Debian ### Ubuntu/Debian
```shell ```shell
apt install build-essential libgtk-3-dev libgtk-layer-shell-dev apt install libgtk-3-dev libgtk-layer-shell-dev
# for http support
apt install libssl-dev
``` ```
### Fedora ### Fedora
@@ -62,8 +59,7 @@ cargo build --release --no-default-features \
| 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. |
| config+toml | Enables configuration support for TOML. | | config+toml | Enables configuration support for TOML. |
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). | | config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | | | **Modules** | |
| clipboard | Enables the `clipboard` module. | | clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. | | clock | Enables the `clock` module. |
@@ -72,7 +68,6 @@ cargo build --release --no-default-features \
| music+mpd | Enables the `music` module with MPD support. | | music+mpd | Enables the `music` module with MPD support. |
| sys_info | Enables the `sys_info` module. | | sys_info | Enables the `sys_info` module. |
| tray | Enables the `tray` module. | | tray | Enables the `tray` module. |
| upower | Enables the `upower` module. |
| workspaces+all | Enables the `workspaces` module with support for all compositors. | | workspaces+all | Enables the `workspaces` module with support for all compositors. |
| workspaces+sway | Enables the `workspaces` module with support for Sway. | | workspaces+sway | Enables the `workspaces` module with support for Sway. |
| workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. | | workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. |

View File

@@ -310,12 +310,9 @@ For information on the `Script` type, and embedding scripts in strings, see [her
| `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 #### Other
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-----------|--------------------|---------|-----------------------------------------------------------------------------------| |-------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------|
| `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. | | `tooltip` | `string` | `null` | Shows this text on hover. Supports embedding scripts between `{{double braces}}`. |
| `name` | `string` | `null` | Sets the unique widget name, allowing you to style it using `#name`. |
| `class` | `string` | `null` | Sets one or more CSS classes, allowing you to style it using `.class`. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -4,37 +4,17 @@ To style the bar, create a file at `~/.config/ironbar/style.css`.
Style changes are hot-loaded so there is no need to reload the bar. Style changes are hot-loaded so there is no need to reload the bar.
Since the bar is GTK-based, it uses [GTK's implementation of CSS](https://docs.gtk.org/gtk3/css-overview.html), A reminder: since the bar is GTK-based, it uses GTK's implementation of CSS,
which only includes a subset of the full web spec (plus a few non-standard properties). which only includes a subset of the full web spec (plus a few non-standard properties).
The below table describes the selectors provided by the bar itself. The below table describes the selectors provided by the bar itself.
Information on styling individual modules can be found on their pages in the sidebar. Information on styling individual modules can be found on their pages in the sidebar.
| Selector | Description | | Selector | Description |
|----------------|--------------------------------------------| |----------------|-------------------------------------------|
| `.background` | Top-level window. | | `.background` | Top-level window |
| `#bar` | Bar root box. | | `#bar` | Bar root box |
| `#bar #start` | Bar left or top modules container box. | | `#bar #start` | Bar left or top modules container box |
| `#bar #center` | Bar center modules container box. | | `#bar #center` | Bar center modules container box |
| `#bar #end` | Bar right or bottom modules container box. | | `#bar #end` | Bar right or bottom modules container box |
| `.container` | All of the above. | | `.container` | All of the above |
| `.popup` | Any popup box. |
Every widget can be selected using a `kebab-case` class name matching its name.
You can also target popups by prefixing `popup-` to the name. For example, you can use `.clock` and `.popup-clock` respectively.
Setting the `name` option on a widget allows you to target that specific instance using `#name`.
You can also add additional classes to re-use styles. In both cases, `popup-` is automatically prefixed to the popup (`#popup-name` or `.popup-my-class`).
You can also target all GTK widgets of a certain type directly using their name. For example, `button:hover` will select the hover state on *all* buttons.
These names are all lower case with no separator, so `MenuBar` -> `menubar`.
GTK CSS does not support custom properties, but it does have its own custom `@define-color` syntax which you can use for re-using colours:
```css
@define-color color_bg #2d2d2d;
box, menubar {
background-color: @color_bg;
}
```

View File

@@ -27,5 +27,4 @@
- [Script](script) - [Script](script)
- [Sys_Info](sys-info) - [Sys_Info](sys-info)
- [Tray](tray) - [Tray](tray)
- [Upower](upower)
- [Workspaces](workspaces) - [Workspaces](workspaces)

View File

@@ -84,13 +84,11 @@ end:
| Selector | Description | | Selector | Description |
|--------------------------------------|------------------------------------------------------| |--------------------------------------|------------------------------------------------------|
| `.clipboard` | Clipboard widget. | | `#clipboard` | Clipboard widget. |
| `.clipboard .btn` | Clipboard widget button. | | `#clipboard .btn` | Clipboard widget button. |
| `.popup-clipboard` | Clipboard popup box. | | `#popup-clipboard` | Clipboard popup box. |
| `.popup-clipboard .item` | Clipboard row item inside the popup. | | `#popup-clipboard .item` | Clipboard row item inside the popup. |
| `.popup-clipboard .item .btn` | Clipboard row item radio button. | | `#popup-clipboard .item .btn` | Clipboard row item radio button. |
| `.popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). | | `#popup-clipboard .item .btn.text` | Clipboard row item radio button (text values only). |
| `.popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). | | `#popup-clipboard .item .btn.image` | Clipboard row item radio button (image values only). |
| `.popup-clipboard .item .btn-remove` | Clipboard row item remove button. | | `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -71,9 +71,7 @@ end:
| Selector | Description | | Selector | Description |
|--------------------------------|------------------------------------------------------------------------------------| |--------------------------------|------------------------------------------------------------------------------------|
| `.clock` | Clock widget button | | `#clock` | Clock widget button |
| `.popup-clock` | Clock popup box | | `#popup-clock` | Clock popup box |
| `.popup-clock .calendar-clock` | Clock inside the popup | | `#popup-clock #calendar-clock` | Clock inside the popup |
| `.popup-clock .calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. | | `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -392,13 +392,10 @@ let {
## Styling ## Styling
Since the widgets are all custom, you can use their `name` and `class` attributes, then target them using `#name` and `.class`. Since the widgets are all custom, you can use the `name` and `class` attributes, then target them using `#name` and `.class`.
The following top-level selectors are always available: The following top-level selector is always available:
| Selector | Description | | Selector | Description |
|-----------------|--------------------------------| |-----------|-------------------------|
| `.custom` | Custom widget container. | | `#custom` | Custom widget container |
| `.popup-custom` | Custom widget popup container. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -87,9 +87,7 @@ end:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|-------------------|--------------------| |--------------------------|--------------------|
| `.focused` | Focused widget box | | `#focused` | Focused widget box |
| `.focused .icon` | App icon | | `#focused #icon` | App icon |
| `.focused .label` | App name | | `#focused #label` | App name |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -66,7 +66,5 @@ end:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|----------|------------------------------------------------------------------------------------| |--------------------------------|------------------------------------------------------------------------------------|
| `.label` | Label widget | | `#label` | Label widget |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -90,12 +90,10 @@ start:
| Selector | Description | | Selector | Description |
|-------------------------------|--------------------------| |-------------------------------|--------------------------|
| `.launcher` | Launcher widget box | | `#launcher` | Launcher widget box |
| `.launcher .item` | App button | | `#launcher .item` | App button |
| `.launcher .item.open` | App button (open app) | | `#launcher .item.open` | App button (open app) |
| `.launcher .item.focused` | App button (focused app) | | `#launcher .item.focused` | App button (focused app) |
| `.launcher .item.urgent` | App button (urgent app) | | `#launcher .item.urgent` | App button (urgent app) |
| `.popup-launcher` | Popup container | | `#launcher-popup` | Popup container |
| `.popup-launcher .popup-item` | Window button in popup | | `#launcher-popup .popup-item` | Window button in popup |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -135,26 +135,23 @@ and will be replaced with values from the currently playing track:
| Selector | Description | | Selector | Description |
|-------------------------------------|------------------------------------------| |-------------------------------------|------------------------------------------|
| `.music` | Tray widget button | | `#music` | Tray widget button |
| `.music .contents` | Tray widget button contents box | | `#popup-music` | Popup box |
| `.popup-music` | Popup box | | `#popup-music #album-art` | Album art image inside popup box |
| `.popup-music .album-art` | Album art image inside popup box | | `#popup-music #title` | Track title container inside popup box |
| `.popup-music .title` | Track title container inside popup box | | `#popup-music #title .icon` | Track title icon label inside popup box |
| `.popup-music .title .icon` | Track title icon label inside popup box | | `#popup-music #title .label` | Track title label inside popup box |
| `.popup-music .title .label` | Track title label inside popup box | | `#popup-music #album` | Track album container inside popup box |
| `.popup-music .album` | Track album container inside popup box | | `#popup-music #album .icon` | Track album icon label inside popup box |
| `.popup-music .album .icon` | Track album icon label inside popup box | | `#popup-music #album .label` | Track album label inside popup box |
| `.popup-music .album .label` | Track album label inside popup box | | `#popup-music #artist` | Track artist container inside popup box |
| `.popup-music .artist` | Track artist container inside popup box | | `#popup-music #artist .icon` | Track artist icon label inside popup box |
| `.popup-music .artist .icon` | Track artist icon label inside popup box | | `#popup-music #artist .label` | Track artist label inside popup box |
| `.popup-music .artist .label` | Track artist label inside popup box | | `#popup-music #controls` | Controls container inside popup box |
| `.popup-music .controls` | Controls container inside popup box | | `#popup-music #controls #btn-prev` | Previous button inside popup box |
| `.popup-music .controls .btn-prev` | Previous button inside popup box | | `#popup-music #controls #btn-play` | Play button inside popup box |
| `.popup-music .controls .btn-play` | Play button inside popup box | | `#popup-music #controls #btn-pause` | Pause button inside popup box |
| `.popup-music .controls .btn-pause` | Pause button inside popup box | | `#popup-music #controls #btn-next` | Next button inside popup box |
| `.popup-music .controls .btn-next` | Next button inside popup box | | `#popup-music #volume` | Volume container inside popup box |
| `.popup-music .volume` | Volume container inside popup box | | `#popup-music #volume #slider` | Volume slider popup box |
| `.popup-music .volume .slider` | Volume slider popup box | | `#popup-music #volume .icon` | Volume icon label inside popup box |
| `.popup-music .volume .icon` | Volume icon label inside popup box |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -83,7 +83,5 @@ end:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|-----------|---------------------| |---------------|---------------------|
| `.script` | Script widget label | | `#script` | Script widget label |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -172,7 +172,5 @@ The following tokens can be used in the `format` configuration option:
| Selector | Description | | Selector | Description |
|------------------|------------------------------| |------------------|------------------------------|
| `.sysinfo` | Sysinfo widget box | | `#sysinfo` | Sysinfo widget box |
| `.sysinfo .item` | Individual information label | | `#sysinfo #item` | Individual information label |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -60,7 +60,5 @@ end:
| Selector | Description | | Selector | Description |
|---------------|------------------| |---------------|------------------|
| `.tray` | Tray widget box | | `#tray` | Tray widget box |
| `.tray .item` | Tray icon button | | `#tray .item` | Tray icon button |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -72,11 +72,9 @@ end:
| Selector | Description | | Selector | Description |
|---------------------------------|-----------------------------| |---------------------------------|-----------------------------|
| `.upower` | Upower widget container. | | `#upower` | Upower widget container. |
| `.upower .icon` | Upower widget battery icon. | | `#upower #icon` | Upower widget battery icon. |
| `.upower .button` | Upower widget button. | | `#upower #button` | Upower widget button. |
| `.upower .button .label` | Upower widget button label. | | `#upower #button #label` | Upower widget button label. |
| `.popup-upower` | Upower popup box. | | `#popup-upower` | Clock popup box. |
| `.popup-upower .upower-details` | Label inside the popup. | | `#popup-upower #upower-details` | Label inside the popup. |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -91,8 +91,6 @@ end:
| Selector | Description | | Selector | Description |
|-----------------------------|--------------------------------------| |-----------------------------|--------------------------------------|
| `.workspaces` | Workspaces widget box | | `#workspaces` | Workspaces widget box |
| `.workspaces .item` | Workspace button | | `#workspaces .item` | Workspace button |
| `.workspaces .item.focused` | Workspace button (workspace focused) | | `#workspaces .item.focused` | Workspace button (workspace focused) |
For more information on styling, please see the [styling guide](styling-guide).

View File

@@ -1,194 +1,240 @@
@define-color color_bg #2d2d2d;
@define-color color_bg_dark #1c1c1c;
@define-color color_border #424242;
@define-color color_border_active #6699cc;
@define-color color_text #ffffff;
@define-color color_urgent #8f0a0a;
/* -- base styles -- */
* { * {
/* `otf-font-awesome` is required to be installed for icons */
font-family: Noto Sans Nerd Font, sans-serif; font-family: Noto Sans Nerd Font, sans-serif;
font-size: 16px; font-size: 16px;
border: none; border: none;
border-radius: 0;
}
box, menubar, button {
background-color: @color_bg;
}
button, label {
color: @color_text;
}
button:hover {
background-color: @color_bg_dark;
} }
#bar { #bar {
border-top: 1px solid @color_border; border-top: 1px solid #424242;
} }
.popup { .background, .container {
border: 1px solid @color_border; background-color: #2d2d2d;
padding: 1em;
} }
#workspaces .item {
/* -- clipboard -- */ color: white;
background-color: #2d2d2d;
.clipboard { border-radius: 0;
margin-left: 5px;
font-size: 1.1em;
} }
.popup-clipboard .item { #workspaces .item.focused {
padding-bottom: 0.3em; box-shadow: inset 0 -3px;
border-bottom: 1px solid @color_border; background-color: #1c1c1c;
} }
#workspaces *:not(.focused):hover {
box-shadow: inset 0 -3px;
}
/* -- clock -- */ #launcher .item {
border-radius: 0;
background-color: #2d2d2d;
margin-right: 4px;
}
.clock { #launcher .item:not(.focused):hover {
background-color: #1c1c1c;
}
#launcher .open {
border-bottom: 2px solid #6699cc;
}
#launcher .focused {
color: white;
background-color: #1c1c1c;
border-bottom: 4px solid #6699cc;
}
#launcher .urgent {
color: white;
background-color: #8f0a0a;
}
#popup-launcher .popup-item {
color: white;
background-color: #2d2d2d;
border-radius: 0;
}
#popup-launcher .popup-item:hover {
background-color: #1c1c1c;
}
#popup-launcher .popup-item:not(:first-child) {
border-top: 1px solid white;
}
#clock {
color: white;
background-color: #2d2d2d;
font-weight: bold; font-weight: bold;
margin-left: 5px; margin-left: 5px;
} }
.popup-clock .calendar-clock { #clock:hover {
color: @color_text; background-color: #1c1c1c;
}
#script {
padding-left: 10px;
color: white;
}
#sysinfo {
margin-left: 10px;
color: white;
}
#sysinfo #item {
margin-left: 5px;
}
#tray {
margin-left: 10px;
}
#tray .item {
background-color: #2d2d2d;
}
#music {
background-color: #2d2d2d;
color: white;
}
.popup {
background-color: #2d2d2d;
border: 1px solid #424242;
}
#popup-clock {
padding: 1em;
}
#calendar-clock {
color: white;
font-size: 2.5em; font-size: 2.5em;
padding-bottom: 0.1em; padding-bottom: 0.1em;
} }
.popup-clock .calendar { #calendar {
background-color: @color_bg; background-color: #2d2d2d;
color: @color_text; color: white;
} }
.popup-clock .calendar .header { #calendar .header {
padding-top: 1em; padding-top: 1em;
border-top: 1px solid @color_border; border-top: 1px solid #424242;
font-size: 1.5em; font-size: 1.5em;
} }
.popup-clock .calendar:selected { #calendar:selected {
background-color: @color_border_active; background-color: #6699cc;
} }
#music:hover {
/* -- launcher -- */ background-color: #1c1c1c;
.launcher .item {
margin-right: 4px;
} }
.launcher .item:not(.focused):hover { #popup-music {
background-color: @color_bg_dark; color: white;
padding: 1em;
} }
.launcher .open { #popup-music #album-art {
border-bottom: 1px solid @color_text;
}
.launcher .focused {
border-bottom: 2px solid @color_border_active;
}
.launcher .urgent {
border-bottom-color: @color_urgent;
}
.popup-launcher {
padding: 0;
}
.popup-launcher .popup-item:not(:first-child) {
border-top: 1px solid @color_border;
}
/* -- music -- */
.music:hover * {
background-color: @color_bg_dark;
}
.popup-music .album-art {
margin-right: 1em; margin-right: 1em;
} }
.popup-music .title .icon *, .popup-music .title .label { #popup-music #title .icon *, #popup-music #title .label {
font-size: 1.7em; font-size: 1.7em;
} }
.popup-music .controls *:disabled { #popup-music #controls * {
color: @color_border; border-radius: 0;
background-color: #2d2d2d;
color: white;
} }
.popup-music .volume scale slider { #popup-music #controls *:disabled {
border-radius: 100%; color: #424242;
} }
/* volume icon */ #popup-music #volume > box:last-child label {
.popup-music .volume > box:last-child label {
margin-left: 6px; margin-left: 6px;
} }
#focused {
/* -- script -- */ color: white;
.script {
padding-left: 10px;
} }
.power-menu {
/* -- sys_info -- */
.sysinfo {
margin-left: 10px; margin-left: 10px;
} }
.sysinfo .item { .power-menu #power-btn {
margin-left: 5px; color: white;
background-color: #2d2d2d;
} }
.power-menu #power-btn:hover {
/* -- tray -- */ background-color: #1c1c1c;
.tray {
margin-left: 10px;
} }
.popup-power-menu {
/* -- workspaces -- */ padding: 1em;
.workspaces .item.focused {
box-shadow: inset 0 -3px;
background-color: @color_bg_dark;
} }
.workspaces .item:hover {
box-shadow: inset 0 -3px;
}
/* -- custom: power menu -- */
.popup-power-menu #header { .popup-power-menu #header {
color: white;
font-size: 1.4em; font-size: 1.4em;
border-bottom: 1px solid white;
padding-bottom: 0.4em; padding-bottom: 0.4em;
margin-bottom: 0.6em; margin-bottom: 0.8em;
border-bottom: 1px solid @color_border;
} }
.popup-power-menu .power-btn { .popup-power-menu .power-btn {
border: 1px solid @color_border; color: white;
background-color: #2d2d2d;
border: 1px solid white;
padding: 0.6em 1em; padding: 0.6em 1em;
} }
.popup-power-menu #buttons > *:nth-child(1) .power-btn { .popup-power-menu .power-btn + .power-btn {
margin-right: 1em; margin-left: 1em;
} }
.popup-power-menu .power-btn:hover {
background-color: #1c1c1c;
}
#clipboard * {
font-size: 1.8em;
}
#popup-clipboard {
padding: 1em;
color: white;
}
#popup-clipboard .item {
border-bottom: 1px solid #424242;
}
#popup-clipboard .btn > *:nth-child(2) {
padding: 10px;
}
#popup-clipboard .btn-remove {
background-color: #2d2d2d;
color: white;
font-size: 1.2em;
border-left: 1px solid #424242;
}
#popup-clipboard .btn-remove:hover {
color: #fcc;
}

36
flake.lock generated
View File

@@ -1,15 +1,12 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1681202837, "lastModified": 1659877975,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401", "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -20,11 +17,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1682786779, "lastModified": 1680213900,
"narHash": "sha256-m7QFzPS/CE8hbkbIVK4UStihAQMtczr0vSpOgETOM1g=", "narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b", "rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -48,11 +45,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1682821181, "lastModified": 1680229280,
"narHash": "sha256-7MYRqO9Ge46sULbQwJbcH/IMDNBdxCGUO9w7bEOc3CI=", "narHash": "sha256-9UoyQCeKUmHcsIdpsAgcz41LAIDkWhI2PhVDjckrpg0=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "1be440e9119e69b68151cd9c84876ff3063a2e45", "rev": "aa480d799023141e1b9e5d6108700de63d9ad002",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -60,21 +57,6 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Migrates CSS selectors from widget names to CSS classes.
# These changed as part of the 0.12 release.
# ⚠ This script will **NOT** check for custom styles and may mangle them!
# ⚠ It is *highly recommended* that you back up your existing styles before running this!
style_path="$HOME/.config/ironbar/style.css"
# general
sed -i 's/#icon/.icon/g' "$style_path"
sed -i 's/#label/.label/g' "$style_path"
sed -i 's/#image/.image/g' "$style_path"
# clipboard
sed -i 's/#clipboard/.clipboard/g' "$style_path"
sed -i 's/#popup-clipboard/.popup-clipboard/g' "$style_path"
# clock
sed -i 's/#clock/.clock/g' "$style_path"
sed -i 's/#popup-clock/.popup-clock/g' "$style_path"
sed -i 's/#calendar-clock/.calendar-clock/g' "$style_path"
sed -i 's/#calendar/.calendar/g' "$style_path"
# custom
sed -i 's/#custom/.custom/g' "$style_path"
sed -i 's/#popup-custom/.popup-custom/g' "$style_path"
# focused
sed -i 's/#focused/.focused/g' "$style_path"
# launcher
sed -i 's/#launcher/.launcher/g' "$style_path"
sed -i 's/#popup-launcher/.popup-launcher/g' "$style_path"
sed -i 's/#launcher-popup/.popup-launcher/g' "$style_path" # was incorrect in docs
# music
sed -i 's/#music/.music/g' "$style_path"
sed -i 's/#contents/.contents/g' "$style_path"
sed -i 's/#popup-music/.popup-music/g' "$style_path"
sed -i 's/#album-art/.album-art/g' "$style_path"
sed -i 's/#title/.title/g' "$style_path"
sed -i 's/#album/.album/g' "$style_path"
sed -i 's/#artist/.artist/g' "$style_path"
sed -i 's/#controls/.controls/g' "$style_path"
sed -i 's/#btn-prev/.btn-prev/g' "$style_path"
sed -i 's/#btn-play/.btn-play/g' "$style_path"
sed -i 's/#btn-pause/.btn-pause/g' "$style_path"
sed -i 's/#btn-next/.btn-next/g' "$style_path"
sed -i 's/#volume/.volume/g' "$style_path"
sed -i 's/#slider/.slider/g' "$style_path"
# script
sed -i 's/#script/.script/g' "$style_path"
# sys_info
sed -i 's/#sysinfo/.sysinfo/g' "$style_path"
sed -i 's/#item/.item/g' "$style_path"
# tray
sed -i 's/#tray/.tray/g' "$style_path"
# upower
sed -i 's/#upower/.upower/g' "$style_path"
sed -i 's/#button/.button/g' "$style_path"
sed -i 's/#popup-upower/.popup-upower/g' "$style_path"
sed -i 's/#upower-details/.upower-details/g' "$style_path"
# workspaces
sed -i 's/#workspaces/.workspaces/g' "$style_path"
sed -i 's/#item/.item/g' "$style_path"

View File

@@ -1,7 +1,5 @@
use crate::config::{BarPosition, MarginConfig, ModuleConfig}; use crate::config::{BarPosition, MarginConfig, ModuleConfig};
use crate::modules::{ use crate::modules::{create_module, wrap_widget, ModuleInfo, ModuleLocation};
create_module, set_widget_identifiers, wrap_widget, ModuleInfo, ModuleLocation,
};
use crate::popup::Popup; use crate::popup::Popup;
use crate::Config; use crate::Config;
use color_eyre::Result; use color_eyre::Result;
@@ -9,6 +7,7 @@ use gtk::gdk::Monitor;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation}; use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use glib::signal::Inhibit;
use tracing::{debug, info}; use tracing::{debug, info};
/// Creates a new window for a bar, /// Creates a new window for a bar,
@@ -50,16 +49,16 @@ pub fn create_bar(
let center = create_container("center", orientation); let center = create_container("center", orientation);
let end = create_container("end", orientation); let end = create_container("end", orientation);
content.add(&start); content.append(&start);
content.set_center_widget(Some(&center)); content.set_center_widget(Some(&center));
content.pack_end(&end, false, false, 0); content.pack_end(&end, false, false, 0);
load_modules(&start, &center, &end, app, config, monitor, monitor_name)?; load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
win.add(&content); win.append(&content);
win.connect_destroy_event(|_, _| { win.connect_destroy_event(|_, _| {
info!("Shutting down"); info!("Shutting down");
gtk::main_quit(); // gtk::main_quit();
Inhibit(false) Inhibit(false)
}); });
@@ -197,11 +196,9 @@ fn add_modules(
macro_rules! add_module { macro_rules! add_module {
($module:expr, $id:expr) => {{ ($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist"); let common = $module.common.take().expect("Common config did not exist");
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?; let widget = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
set_widget_identifiers(&widget_parts, &common); let container = wrap_widget(&widget, common, orientation);
content.append(&container);
let container = wrap_widget(&widget_parts.widget, common, orientation);
content.add(&container);
}}; }};
} }

View File

@@ -6,7 +6,7 @@ use lazy_static::lazy_static;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, trace}; use tracing::debug;
#[derive(Debug)] #[derive(Debug)]
pub enum ClipboardEvent { pub enum ClipboardEvent {
@@ -26,8 +26,6 @@ pub struct ClipboardClient {
impl ClipboardClient { impl ClipboardClient {
fn new() -> Self { fn new() -> Self {
trace!("Initializing clipboard client");
let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new())); let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
let cache = Arc::new(Mutex::new(ClipboardCache::new())); let cache = Arc::new(Mutex::new(ClipboardCache::new()));
@@ -37,21 +35,11 @@ impl ClipboardClient {
let cache = cache.clone(); let cache = cache.clone();
spawn(async move { spawn(async move {
let (mut rx, item) = { let mut rx = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_clipboard() wl.subscribe_clipboard()
}; };
if let Some(item) = item {
let senders = lock!(senders);
let iter = senders.iter();
for (tx, _) in iter {
try_send!(tx, ClipboardEvent::Add(item.clone()));
}
lock!(cache).insert(item, senders.len());
}
while let Ok(item) = rx.recv().await { while let Ok(item) = rx.recv().await {
debug!("Received clipboard item (ID: {})", item.id); debug!("Received clipboard item (ID: {})", item.id);
@@ -71,6 +59,7 @@ impl ClipboardClient {
let iter = senders.iter(); let iter = senders.iter();
for (tx, sender_cache_size) in iter { for (tx, sender_cache_size) in iter {
if cache_size == *sender_cache_size { if cache_size == *sender_cache_size {
// let mut cache = lock!(cache);
let removed_id = lock!(cache) let removed_id = lock!(cache)
.remove_ref_first() .remove_ref_first()
.expect("Clipboard cache unexpectedly empty"); .expect("Clipboard cache unexpectedly empty");
@@ -94,11 +83,18 @@ impl ClipboardClient {
Self { senders, cache } Self { senders, cache }
} }
pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> { pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver<ClipboardEvent> {
let (tx, rx) = mpsc::channel(16); let (tx, rx) = mpsc::channel(16);
let wl = wayland::get_client().await;
wl.roundtrip();
{ {
let cache = lock!(self.cache); let mut cache = lock!(self.cache);
if let Some(item) = wl.get_clipboard() {
cache.insert_or_inc_ref(item);
}
let iter = cache.iter(); let iter = cache.iter();
for (_, (item, _)) in iter { for (_, (item, _)) in iter {
@@ -106,7 +102,10 @@ impl ClipboardClient {
} }
} }
lock!(self.senders).push((tx, cache_size)); {
let mut senders = lock!(self.senders);
senders.push((tx, cache_size));
}
rx rx
} }
@@ -172,6 +171,13 @@ impl ClipboardCache {
.map(|(item, _)| item) .map(|(item, _)| item)
} }
/// Inserts an entry with `ref_count` initial references,
/// or increments the `ref_count` by 1 if it already exists.
fn insert_or_inc_ref(&mut self, item: Arc<ClipboardItem>) {
let mut item = self.cache.entry(item.id).or_insert((item, 0));
item.1 += 1;
}
/// Removes the entry with key `id`. /// Removes the entry with key `id`.
/// This ignores references. /// This ignores references.
fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> { fn remove(&mut self, id: usize) -> Option<Arc<ClipboardItem>> {

View File

@@ -5,7 +5,6 @@ use color_eyre::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::collections::HashSet; use std::collections::HashSet;
use std::string;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@@ -260,7 +259,7 @@ impl From<Metadata> for Track {
.and_then(mpris::MetadataValue::as_str_array) .and_then(mpris::MetadataValue::as_str_array)
.and_then(|arr| arr.first().map(|val| (*val).to_string())), .and_then(|arr| arr.first().map(|val| (*val).to_string())),
track: value.track_number().map(|track| track as u64), track: value.track_number().map(|track| track as u64),
cover_path: value.art_url().map(string::ToString::to_string), cover_path: value.art_url().map(|s| s.to_string()),
} }
} }
} }

View File

@@ -1,90 +1,79 @@
use super::wlr_foreign_toplevel::handle::ToplevelHandle; use super::wlr_foreign_toplevel::{
use super::wlr_foreign_toplevel::manager::ToplevelManagerState; handle::{ToplevelEvent, ToplevelInfo},
use super::wlr_foreign_toplevel::ToplevelEvent; manager::listen_for_toplevels,
use super::Environment; };
use crate::error::ERR_CHANNEL_RECV; use super::{DData, Env, ToplevelHandler};
use crate::send; use crate::{error as err, send};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use color_eyre::Report; use color_eyre::Report;
use smithay_client_toolkit::output::{OutputInfo, OutputState}; use indexmap::IndexMap;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender}; use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
use smithay_client_toolkit::reexports::calloop::EventLoop; use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::registry::RegistryState; use smithay_client_toolkit::WaylandSource;
use smithay_client_toolkit::seat::SeatState;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::mpsc; use std::sync::{Arc, RwLock};
use tokio::sync::broadcast; use tokio::sync::{broadcast, oneshot};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use tracing::{debug, error, trace}; use tracing::{debug, error};
use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, WaylandSource}; use wayland_client::{ConnectError, Display, EventQueue};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
use super::ClipboardItem; use super::{ClipboardItem};
use super::wlr_data_control::manager::DataControlDeviceManagerState; use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler};
use crate::lock; use crate::{read_lock, write_lock};
use std::sync::{Arc, Mutex}; use tokio::spawn;
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Request { pub enum Request {
/// Sends a request for all the outputs.
/// These are then sent on the `output` channel.
Outputs,
/// Sends a request for all the seats.
/// These are then sent ont the `seat` channel.
Seats,
/// Sends a request for all the toplevels.
/// These are then sent on the `toplevel_init` channel.
Toplevels,
/// Sends a request for the current clipboard item.
/// This is then sent on the `clipboard_init` channel.
#[cfg(feature = "clipboard")]
Clipboard,
/// Copies the value to the clipboard /// Copies the value to the clipboard
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
CopyToClipboard(Arc<ClipboardItem>), CopyToClipboard(Arc<ClipboardItem>),
/// Forces a dispatch, flushing any currently queued events /// Forces a dispatch, flushing any currently queued events
Roundtrip, Refresh,
} }
pub struct WaylandClient { pub struct WaylandClient {
// External channels pub outputs: Vec<OutputInfo>,
pub seats: Vec<WlSeat>,
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>, toplevel_tx: broadcast::Sender<ToplevelEvent>,
_toplevel_rx: broadcast::Receiver<ToplevelEvent>, _toplevel_rx: broadcast::Receiver<ToplevelEvent>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>, clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
_clipboard_rx: broadcast::Receiver<Arc<ClipboardItem>>, clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>,
// Internal channels
toplevel_init_rx: mpsc::Receiver<HashMap<usize, ToplevelHandle>>,
output_rx: mpsc::Receiver<Vec<OutputInfo>>,
seat_rx: mpsc::Receiver<Vec<WlSeat>>,
#[cfg(feature = "clipboard")]
clipboard_init_rx: mpsc::Receiver<Option<Arc<ClipboardItem>>>,
request_tx: Sender<Request>, request_tx: Sender<Request>,
} }
impl WaylandClient { impl WaylandClient {
pub(super) async fn new() -> Self { pub(super) async fn new() -> Self {
let (output_tx, output_rx) = oneshot::channel();
let (seat_tx, seat_rx) = oneshot::channel();
let (toplevel_tx, toplevel_rx) = broadcast::channel(32); let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
let (toplevel_init_tx, toplevel_init_rx) = mpsc::channel(); let toplevels = Arc::new(RwLock::new(IndexMap::new()));
#[cfg(feature = "clipboard")] let toplevels2 = toplevels.clone();
let (clipboard_init_tx, clipboard_init_rx) = mpsc::channel();
let (output_tx, output_rx) = mpsc::channel();
let (seat_tx, seat_rx) = mpsc::channel();
let toplevel_tx2 = toplevel_tx.clone(); let toplevel_tx2 = toplevel_tx.clone();
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
let (clipboard_tx, clipboard_rx) = broadcast::channel(32); let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32);
let clipboard = Arc::new(RwLock::new(None));
let clipboard_tx2 = clipboard_tx.clone(); let clipboard_tx2 = clipboard_tx.clone();
} }
} }
@@ -93,100 +82,85 @@ impl WaylandClient {
// `queue` is not `Send` so we need to handle everything inside the task // `queue` is not `Send` so we need to handle everything inside the task
spawn_blocking(move || { spawn_blocking(move || {
let toplevels = toplevels2;
let toplevel_tx = toplevel_tx2; let toplevel_tx = toplevel_tx2;
#[cfg(feature = "clipboard")]
let clipboard_tx = clipboard_tx2;
let conn = let (env, _display, queue) =
Connection::connect_to_env().expect("Failed to connect to Wayland compositor"); Self::new_environment().expect("Failed to connect to Wayland compositor");
let (globals, queue) =
registry_queue_init(&conn).expect("Failed to retrieve Wayland globals");
let qh = queue.handle();
let mut event_loop = let mut event_loop =
EventLoop::<Environment>::try_new().expect("Failed to create new event loop"); EventLoop::<DData>::try_new().expect("Failed to create new event loop");
WaylandSource::new(queue) WaylandSource::new(queue)
.expect("Failed to create Wayland source from queue") .quick_insert(event_loop.handle())
.insert(event_loop.handle())
.expect("Failed to insert Wayland event queue into event loop"); .expect("Failed to insert Wayland event queue into event loop");
let loop_handle = event_loop.handle(); let outputs = Self::get_outputs(&env);
send!(output_tx, outputs);
// Initialize the registry handling let seats = env.get_all_seats();
// so other parts of Smithay's client toolkit may bind globals.
let registry_state = RegistryState::new(&globals);
let output_delegate = OutputState::new(&globals, &qh); // TODO: Actually handle seats properly
let seat_delegate = SeatState::new(&globals, &qh); #[cfg(feature = "clipboard")]
let default_seat = seats[0].detach();
#[cfg(feature = "clipboard")] send!(
let data_control_device_manager_delegate = seat_tx,
DataControlDeviceManagerState::bind(&globals, &qh) seats
.expect("data device manager is not available"); .into_iter()
.map(|seat| seat.detach())
.collect::<Vec<WlSeat>>()
);
let foreign_toplevel_manager_delegate = ToplevelManagerState::bind(&globals, &qh) let handle = event_loop.handle();
.expect("foreign toplevel manager is not available"); handle
.insert_source(ev_rx, move |event, _metadata, ddata| {
let mut env = Environment { // let env = &ddata.env;
registry_state,
output_state: output_delegate,
seat_state: seat_delegate,
#[cfg(feature = "clipboard")]
data_control_device_manager_state: data_control_device_manager_delegate,
foreign_toplevel_manager_state: foreign_toplevel_manager_delegate,
seats: vec![],
handles: HashMap::new(),
#[cfg(feature = "clipboard")]
clipboard: Arc::new(Mutex::new(None)),
toplevel_tx,
#[cfg(feature = "clipboard")]
clipboard_tx,
#[cfg(feature = "clipboard")]
data_control_devices: vec![],
#[cfg(feature = "clipboard")]
selection_offers: vec![],
#[cfg(feature = "clipboard")]
copy_paste_sources: vec![],
loop_handle: event_loop.handle(),
};
loop_handle
.insert_source(ev_rx, move |event, _metadata, env| {
trace!("{event:?}");
match event { match event {
Event::Msg(Request::Roundtrip) => debug!("Received refresh event"), Event::Msg(Request::Refresh) => debug!("Received refresh event"),
Event::Msg(Request::Outputs) => {
trace!("Received get outputs request");
send!(output_tx, env.output_info());
}
Event::Msg(Request::Seats) => {
trace!("Receive get seats request");
send!(seat_tx, env.seats.clone());
}
Event::Msg(Request::Toplevels) => {
trace!("Receive get toplevels request");
send!(toplevel_init_tx, env.handles.clone());
}
#[cfg(feature = "clipboard")]
Event::Msg(Request::Clipboard) => {
trace!("Receive get clipboard requests");
let clipboard = lock!(env.clipboard).clone();
send!(clipboard_init_tx, clipboard);
}
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
Event::Msg(Request::CopyToClipboard(value)) => { Event::Msg(Request::CopyToClipboard(value)) => {
env.copy_to_clipboard(value, &qh); super::wlr_data_control::copy_to_clipboard(
&ddata.env,
&default_seat,
&value,
)
.expect("Failed to copy to clipboard");
} }
Event::Closed => panic!("Channel unexpectedly closed"), Event::Closed => panic!("Channel unexpectedly closed"),
} }
}) })
.expect("Failed to insert channel into event queue"); .expect("Failed to insert channel into event queue");
let _toplevel_manager = env.require_global::<ZwlrForeignToplevelManagerV1>();
let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
super::wlr_foreign_toplevel::update_toplevels(
&toplevels,
handle,
event,
&toplevel_tx,
);
});
cfg_if! {
if #[cfg(feature = "clipboard")] {
let clipboard_tx = clipboard_tx2;
let handle = event_loop.handle();
let _offer_listener = listen_to_devices(&env, move |_seat, event, ddata| {
debug!("Received clipboard event");
super::wlr_data_control::receive_offer(event, &handle, clipboard_tx.clone(), ddata);
});
}
}
let mut data = DData {
env,
offer_tokens: HashMap::new(),
};
loop { loop {
trace!("Dispatching event loop"); if let Err(err) = event_loop.dispatch(None, &mut data) {
if let Err(err) = event_loop.dispatch(None, &mut env) {
error!( error!(
"{:?}", "{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events") Report::new(err).wrap_err("Failed to dispatch pending wayland events")
@@ -195,76 +169,119 @@ impl WaylandClient {
} }
}); });
// keep track of current clipboard item
#[cfg(feature = "clipboard")]
{
let clipboard = clipboard.clone();
spawn(async move {
while let Ok(item) = clipboard_rx.recv().await {
let mut clipboard = write_lock!(clipboard);
clipboard.replace(item);
}
});
}
let outputs = output_rx.await.expect(err::ERR_CHANNEL_RECV);
let seats = seat_rx.await.expect(err::ERR_CHANNEL_RECV);
Self { Self {
outputs,
seats,
#[cfg(feature = "clipboard")]
clipboard,
toplevels,
toplevel_tx, toplevel_tx,
_toplevel_rx: toplevel_rx, _toplevel_rx: toplevel_rx,
toplevel_init_rx,
#[cfg(feature = "clipboard")]
clipboard_init_rx,
output_rx,
seat_rx,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard_tx, clipboard_tx,
#[cfg(feature = "clipboard")]
_clipboard_rx: clipboard_rx,
request_tx: ev_tx, request_tx: ev_tx,
} }
} }
pub fn subscribe_toplevels( pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
&self, self.toplevel_tx.subscribe()
) -> (
broadcast::Receiver<ToplevelEvent>,
HashMap<usize, ToplevelHandle>,
) {
let rx = self.toplevel_tx.subscribe();
let receiver = &self.toplevel_init_rx;
send!(self.request_tx, Request::Toplevels);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
} }
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub fn subscribe_clipboard( pub fn subscribe_clipboard(&self) -> broadcast::Receiver<Arc<ClipboardItem>> {
&self, self.clipboard_tx.subscribe()
) -> (
broadcast::Receiver<Arc<ClipboardItem>>,
Option<Arc<ClipboardItem>>,
) {
let rx = self.clipboard_tx.subscribe();
let receiver = &self.clipboard_init_rx;
send!(self.request_tx, Request::Clipboard);
let data = receiver.recv().expect(ERR_CHANNEL_RECV);
(rx, data)
} }
/// Force a roundtrip on the wayland connection,
/// flushing any queued events and immediately receiving any new ones.
pub fn roundtrip(&self) { pub fn roundtrip(&self) {
trace!("Sending roundtrip request"); send!(self.request_tx, Request::Refresh);
send!(self.request_tx, Request::Roundtrip);
} }
pub fn get_outputs(&self) -> Vec<OutputInfo> { #[cfg(feature = "clipboard")]
trace!("Sending get outputs request"); pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> {
let clipboard = read_lock!(self.clipboard);
send!(self.request_tx, Request::Outputs); clipboard.as_ref().cloned()
self.output_rx.recv().expect(ERR_CHANNEL_RECV)
}
pub fn get_seats(&self) -> Vec<WlSeat> {
trace!("Sending get seats request");
send!(self.request_tx, Request::Seats);
self.seat_rx.recv().expect(ERR_CHANNEL_RECV)
} }
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) { pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
send!(self.request_tx, Request::CopyToClipboard(item)); send!(self.request_tx, Request::CopyToClipboard(item));
} }
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
let outputs = env.get_all_outputs();
outputs
.iter()
.filter_map(|output| with_output_info(output, Clone::clone))
.collect()
}
fn new_environment() -> Result<(Environment<Env>, Display, EventQueue), ConnectError> {
Display::connect_to_env().and_then(|display| {
let mut queue = display.create_event_queue();
let ret = {
let mut sctk_seats = smithay_client_toolkit::seat::SeatHandler::new();
let sctk_data_device_manager =
smithay_client_toolkit::data_device::DataDeviceHandler::init(&mut sctk_seats);
#[cfg(feature = "clipboard")]
let data_control_device = DataControlDeviceHandler::init(&mut sctk_seats);
let sctk_primary_selection_manager =
smithay_client_toolkit::primary_selection::PrimarySelectionHandler::init(
&mut sctk_seats,
);
let display = ::smithay_client_toolkit::reexports::client::Proxy::clone(&display);
let env = Environment::new(
&display.attach(queue.token()),
&mut queue,
Env {
sctk_compositor: smithay_client_toolkit::environment::SimpleGlobal::new(),
sctk_subcompositor: smithay_client_toolkit::environment::SimpleGlobal::new(
),
sctk_shm: smithay_client_toolkit::shm::ShmHandler::new(),
sctk_outputs: smithay_client_toolkit::output::OutputHandler::new(),
sctk_seats,
sctk_data_device_manager,
sctk_primary_selection_manager,
toplevel: ToplevelHandler::init(),
#[cfg(feature = "clipboard")]
data_control_device,
},
);
if let Ok(env) = env.as_ref() {
let _psm = env.get_primary_selection_manager();
}
env
};
match ret {
Ok(env) => Ok((env, display, queue)),
Err(_e) => display.protocol_error().map_or_else(
|| Err(ConnectError::NoCompositorListening),
|perr| {
panic!("[SCTK] A protocol error occured during initial setup: {perr}");
},
),
}
})
}
} }

View File

@@ -1,101 +0,0 @@
/// It is necessary to store macros in a separate file due to a compilation error.
/// I believe this stems from the feature flags.
/// Related issue: https://github.com/rust-lang/rust/issues/81066
// --- Data Control Device --- \\
#[macro_export]
macro_rules! delegate_data_control_device_manager {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1: smithay_client_toolkit::globals::GlobalData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_device {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1: $crate::clients::wayland::wlr_data_control::device::DataControlDeviceData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_offer {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1: $crate::clients::wayland::wlr_data_control::offer::DataControlOfferData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
#[macro_export]
macro_rules! delegate_data_control_source {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $udata,
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1: $crate::clients::wayland::wlr_data_control::source::DataControlSourceData
] => $crate::clients::wayland::wlr_data_control::manager::DataControlDeviceManagerState
);
};
}
// --- Foreign Toplevel --- \\
#[macro_export]
macro_rules! delegate_foreign_toplevel_manager {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: smithay_client_toolkit::globals::GlobalData
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
}
#[macro_export]
macro_rules! delegate_foreign_toplevel_handle {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, udata: [$($udata: ty),*$(,)?]) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $udata,
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty:
[
wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::clients::wayland::wlr_foreign_toplevel::handle::ToplevelHandleData
] => $crate::clients::wayland::wlr_foreign_toplevel::manager::ToplevelManagerState
);
};
}

View File

@@ -1,110 +1,70 @@
mod client; mod client;
mod macros;
mod wl_output;
mod wl_seat;
mod wlr_foreign_toplevel; mod wlr_foreign_toplevel;
use self::wlr_foreign_toplevel::manager::ToplevelManagerState;
use crate::{delegate_foreign_toplevel_handle, delegate_foreign_toplevel_manager};
use async_once::AsyncOnce;
use cfg_if::cfg_if;
use lazy_static::lazy_static;
use smithay_client_toolkit::output::OutputState;
use smithay_client_toolkit::reexports::calloop::LoopHandle;
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::SeatState;
use smithay_client_toolkit::{
delegate_output, delegate_registry, delegate_seat, registry_handlers,
};
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::broadcast; use async_once::AsyncOnce;
use wayland_client::protocol::wl_seat::WlSeat; use lazy_static::lazy_static;
use std::fmt::Debug;
use cfg_if::cfg_if;
use smithay_client_toolkit::default_environment;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::reexports::calloop::RegistrationToken;
use wayland_client::{Attached, Interface};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
pub use wlr_foreign_toplevel::handle::{ToplevelChange, ToplevelEvent, ToplevelInfo};
use wlr_foreign_toplevel::manager::{ToplevelHandler};
pub use self::client::WaylandClient; pub use client::WaylandClient;
pub use self::wlr_foreign_toplevel::{ToplevelEvent, ToplevelHandle, ToplevelInfo};
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
mod wlr_data_control; mod wlr_data_control;
use crate::{delegate_data_control_device, delegate_data_control_device_manager, delegate_data_control_offer, delegate_data_control_source}; use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use self::wlr_data_control::device::DataControlDevice; use wlr_data_control::manager::DataControlDeviceHandler;
use self::wlr_data_control::manager::DataControlDeviceManagerState;
use self::wlr_data_control::source::CopyPasteSource;
use self::wlr_data_control::SelectionOfferItem;
use std::sync::{Arc, Mutex};
pub use wlr_data_control::{ClipboardItem, ClipboardValue}; pub use wlr_data_control::{ClipboardItem, ClipboardValue};
pub struct DataControlDeviceEntry {
seat: WlSeat,
device: DataControlDevice,
}
} }
} }
pub struct Environment { /// A utility for lazy-loading globals.
pub registry_state: RegistryState, /// Taken from `smithay_client_toolkit` where it's not exposed
pub output_state: OutputState, #[derive(Debug)]
pub seat_state: SeatState, enum LazyGlobal<I: Interface> {
pub foreign_toplevel_manager_state: ToplevelManagerState, Unknown,
#[cfg(feature = "clipboard")] Seen { id: u32, version: u32 },
pub data_control_device_manager_state: DataControlDeviceManagerState, Bound(Attached<I>),
pub loop_handle: LoopHandle<'static, Self>,
pub seats: Vec<WlSeat>,
#[cfg(feature = "clipboard")]
pub data_control_devices: Vec<DataControlDeviceEntry>,
#[cfg(feature = "clipboard")]
pub selection_offers: Vec<SelectionOfferItem>,
#[cfg(feature = "clipboard")]
pub copy_paste_sources: Vec<CopyPasteSource>,
pub handles: HashMap<usize, ToplevelHandle>,
#[cfg(feature = "clipboard")]
clipboard: Arc<Mutex<Option<Arc<ClipboardItem>>>>,
toplevel_tx: broadcast::Sender<ToplevelEvent>,
#[cfg(feature = "clipboard")]
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
} }
// Now we need to say we are delegating the responsibility of output related events for our application data pub struct DData {
// type to the requisite delegate. env: Environment<Env>,
delegate_output!(Environment); offer_tokens: HashMap<u128, RegistrationToken>,
delegate_seat!(Environment); }
delegate_foreign_toplevel_manager!(Environment);
delegate_foreign_toplevel_handle!(Environment);
cfg_if! { cfg_if! {
if #[cfg(feature = "clipboard")] { if #[cfg(feature = "clipboard")] {
delegate_data_control_device_manager!(Environment); default_environment!(Env,
delegate_data_control_device!(Environment); fields = [
delegate_data_control_source!(Environment); toplevel: ToplevelHandler,
delegate_data_control_offer!(Environment); data_control_device: DataControlDeviceHandler
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
ZwlrDataControlManagerV1 => data_control_device
],
);
} else {
default_environment!(Env,
fields = [
toplevel: ToplevelHandler,
],
singles = [
ZwlrForeignToplevelManagerV1 => toplevel,
],
);
} }
} }
// In order for our delegate to know of the existence of globals, we need to implement registry
// handling for the program. This trait will forward events to the RegistryHandler trait
// implementations.
delegate_registry!(Environment);
// In order for delegate_registry to work, our application data type needs to provide a way for the
// implementation to access the registry state.
//
// We also need to indicate which delegates will get told about globals being created. We specify
// the types of the delegates inside the array.
impl ProvidesRegistryState for Environment {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
lazy_static! { lazy_static! {
static ref CLIENT: AsyncOnce<WaylandClient> = static ref CLIENT: AsyncOnce<WaylandClient> =
AsyncOnce::new(async { WaylandClient::new().await }); AsyncOnce::new(async { WaylandClient::new().await });

View File

@@ -1,55 +0,0 @@
use super::Environment;
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
use tracing::debug;
use wayland_client::protocol::wl_output;
use wayland_client::{Connection, QueueHandle};
impl Environment {
pub fn output_info(&mut self) -> Vec<OutputInfo> {
self.output_state
.outputs()
.filter_map(|output| self.output_state.info(&output))
.collect()
}
}
// In order to use OutputDelegate, we must implement this trait to indicate when something has happened to an
// output and to provide an instance of the output state to the delegate when dispatching events.
impl OutputHandler for Environment {
// First we need to provide a way to access the delegate.
//
// This is needed because delegate implementations for handling events use the application data type in
// their function signatures. This allows the implementation to access an instance of the type.
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
// Then there exist these functions that indicate the lifecycle of an output.
// These will be called as appropriate by the delegate implementation.
fn new_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
debug!("Handler received new output");
}
fn update_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
debug!("Handle received output destruction");
}
}

View File

@@ -1,63 +0,0 @@
use super::Environment;
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use tracing::debug;
use wayland_client::protocol::wl_seat;
use wayland_client::{Connection, QueueHandle};
impl SeatHandler for Environment {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
debug!("Handler received new seat");
self.seats.push(seat);
}
fn new_capability(
&mut self,
_: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
_: Capability,
) {
debug!("Handler received new capability");
#[cfg(feature = "clipboard")]
if !self
.data_control_devices
.iter_mut()
.any(|entry| entry.seat == seat)
{
debug!("Adding new data control device");
// create the data device here for this seat
let data_control_device_manager = &self.data_control_device_manager_state;
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
self.data_control_devices
.push(super::DataControlDeviceEntry {
seat: seat.clone(),
device: data_control_device,
});
}
if !self.seats.iter().any(|s| s == &seat) {
self.seats.push(seat);
}
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
_: Capability,
) {
debug!("Handler received capability removal");
// Not applicable
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
debug!("Handler received seat removal");
self.seats.retain(|s| s != &seat);
}
}

View File

@@ -1,166 +1,88 @@
use super::manager::DataControlDeviceManagerState; use super::offer::DataControlOffer;
use super::offer::{ use super::source::DataControlSource;
DataControlOfferData, DataControlOfferDataExt, DataControlOfferHandler, SelectionOffer,
};
use crate::error::ERR_WAYLAND_DATA;
use crate::lock; use crate::lock;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::warn; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Attached, DispatchData, Main};
use wayland_protocols_wlr::data_control::v1::client::{ use wayland_protocols::wlr::unstable::data_control::v1::client::{
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1}, zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1, zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
}; };
pub struct DataControlDevice { #[derive(Debug)]
pub device: ZwlrDataControlDeviceV1, struct Inner {
offer: Option<Arc<DataControlOffer>>,
} }
#[derive(Debug, Default)] impl Inner {
pub struct DataControlDeviceInner { fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) {
/// the active selection offer and its data self.offer.replace(Arc::new(DataControlOffer::new(offer)));
selection_offer: Arc<Mutex<Option<ZwlrDataControlOfferV1>>>,
/// the active undetermined offers and their data
pub undetermined_offers: Arc<Mutex<Vec<ZwlrDataControlOfferV1>>>,
}
#[derive(Debug, Default)]
pub struct DataControlDeviceData {
pub(super) inner: Arc<Mutex<DataControlDeviceInner>>,
}
pub trait DataControlDeviceDataExt: Send + Sync {
type DataControlOfferInner: DataControlOfferDataExt + Send + Sync + 'static;
fn data_control_device_data(&self) -> &DataControlDeviceData;
fn selection_mime_types(&self) -> Vec<String> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
.as_ref()
.map(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.mime_types()
})
.unwrap_or_default()
}
/// Get the active selection offer if it exists.
fn selection_offer(&self) -> Option<SelectionOffer> {
let inner = self.data_control_device_data();
lock!(lock!(inner.inner).selection_offer)
.as_ref()
.and_then(|offer| {
let data = offer
.data::<Self::DataControlOfferInner>()
.expect(ERR_WAYLAND_DATA);
data.as_selection_offer()
})
} }
} }
impl DataControlDeviceDataExt for DataControlDevice { #[derive(Debug, Clone)]
type DataControlOfferInner = DataControlOfferData; pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>);
fn data_control_device_data(&self) -> &DataControlDeviceData {
self.device.data().expect(ERR_WAYLAND_DATA)
}
}
impl DataControlDeviceDataExt for DataControlDeviceData { fn data_control_device_implem<F>(
type DataControlOfferInner = DataControlOfferData;
fn data_control_device_data(&self) -> &DataControlDeviceData {
self
}
}
/// Handler trait for `DataDevice` events.
///
/// The functions defined in this trait are called as `DataDevice` events are received from the compositor.
pub trait DataControlDeviceHandler: Sized {
/// Advertises a new selection.
fn selection(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
data_device: DataControlDevice,
);
}
impl<D, U, V> Dispatch<ZwlrDataControlDeviceV1, U, D> for DataControlDeviceManagerState<V>
where
D: Dispatch<ZwlrDataControlDeviceV1, U>
+ Dispatch<ZwlrDataControlOfferV1, V>
+ DataControlDeviceHandler
+ DataControlOfferHandler
+ 'static,
U: DataControlDeviceDataExt,
V: DataControlOfferDataExt + Default + 'static + Send + Sync,
{
event_created_child!(D, ZwlrDataControlDeviceV1, [
0 => (ZwlrDataControlOfferV1, V::default())
]);
fn event(
state: &mut D,
data_device: &ZwlrDataControlDeviceV1,
event: Event, event: Event,
data: &U, inner: &mut Inner,
conn: &Connection, implem: &mut F,
qh: &QueueHandle<D>, ddata: DispatchData,
) { ) where
let data = data.data_control_device_data(); F: FnMut(DataControlDeviceEvent, DispatchData),
let inner = lock!(data.inner); {
match event { match event {
Event::DataOffer { id } => { Event::DataOffer { id } => {
// XXX Drop done here to prevent Mutex deadlocks.S inner.new_offer(&id);
lock!(inner.undetermined_offers).push(id.clone());
let data = id
.data::<V>()
.expect(ERR_WAYLAND_DATA)
.data_control_offer_data();
data.init_undetermined_offer(&id);
// Append the data offer to our list of offers.
drop(inner);
} }
Event::Selection { id } => { Event::Selection { id: Some(offer) } => {
let mut selection_offer = lock!(inner.selection_offer); let inner_offer = inner
.offer
if let Some(offer) = id { .clone()
let mut undetermined = lock!(inner.undetermined_offers); .expect("Offer should exist at this stage");
if let Some(i) = undetermined.iter().position(|o| o == &offer) { if offer == inner_offer.offer {
undetermined.remove(i); implem(DataControlDeviceEvent(inner_offer), ddata);
} }
drop(undetermined);
let data = offer
.data::<V>()
.expect(ERR_WAYLAND_DATA)
.data_control_offer_data();
data.to_selection_offer();
// XXX Drop done here to prevent Mutex deadlocks.
*selection_offer = Some(offer.clone());
drop(selection_offer);
drop(inner);
state.selection(
conn,
qh,
DataControlDevice {
device: data_device.clone(),
},
);
} else {
*selection_offer = None;
}
}
Event::Finished => {
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
} }
_ => {} _ => {}
} }
}
pub struct DataControlDevice {
device: ZwlrDataControlDeviceV1,
_inner: Arc<Mutex<Inner>>,
}
impl DataControlDevice {
pub fn init_for_seat<F>(
manager: &Attached<ZwlrDataControlManagerV1>,
seat: &WlSeat,
mut callback: F,
) -> Self
where
F: FnMut(DataControlDeviceEvent, DispatchData) + 'static,
{
let inner = Arc::new(Mutex::new(Inner { offer: None }));
let device = manager.get_data_device(seat);
{
let inner = inner.clone();
device.quick_assign(move |_handle, event, ddata| {
let mut inner = lock!(inner);
data_control_device_implem(event, &mut inner, &mut callback, ddata);
});
}
Self {
device: device.detach(),
_inner: inner,
}
}
pub fn set_selection(&self, source: &Option<DataControlSource>) {
self.device
.set_selection(source.as_ref().map(|s| &s.source));
} }
} }

View File

@@ -1,132 +1,253 @@
use super::device::{DataControlDevice, DataControlDeviceData, DataControlDeviceDataExt}; use super::device::{DataControlDevice, DataControlDeviceEvent};
use super::offer::DataControlOfferData; use super::source::DataControlSource;
use super::source::{CopyPasteSource, DataControlSourceData, DataControlSourceDataExt}; use smithay_client_toolkit::data_device::WritePipe;
use smithay_client_toolkit::error::GlobalError; use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal}; use smithay_client_toolkit::seat::{SeatHandling, SeatListener};
use std::marker::PhantomData; use smithay_client_toolkit::MissingGlobal;
use tracing::debug; use std::cell::RefCell;
use wayland_client::globals::{BindError, GlobalList}; use std::rc::{self, Rc};
use tracing::warn;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Attached, DispatchData};
use wayland_protocols_wlr::data_control::v1::client::{ use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
};
pub struct DataControlDeviceManagerState<V = DataControlOfferData> { enum DataControlDeviceHandlerInner {
manager: ZwlrDataControlManagerV1, Ready {
_phantom: PhantomData<V>, manager: Attached<ZwlrDataControlManagerV1>,
devices: Vec<(WlSeat, DataControlDevice)>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
Pending {
seats: Vec<WlSeat>,
status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
},
} }
impl DataControlDeviceManagerState { impl DataControlDeviceHandlerInner {
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError> fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
where let (seats, status_listeners) = if let Self::Pending {
State: Dispatch<ZwlrDataControlManagerV1, GlobalData, State> + 'static, seats,
status_listeners,
} = self
{ {
let manager = globals.bind(qh, 1..=2, GlobalData)?; (std::mem::take(seats), status_listeners.clone())
debug!("Bound to ZwlDataControlManagerV1 global"); } else {
Ok(Self { warn!("Ignoring second zwlr_data_control_manager_v1");
return;
};
let mut devices = Vec::new();
for seat in seats {
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(&manager, &seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
}
*self = Self::Ready {
manager, manager,
_phantom: PhantomData, devices,
status_listeners,
};
}
fn get_manager(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
match self {
Self::Ready { manager, .. } => Some(manager.clone()),
Self::Pending { .. } => None,
}
}
fn new_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready {
manager,
devices,
status_listeners,
} => {
if devices.iter().any(|(s, _)| s == seat) {
// the seat already exists, nothing to do
return;
}
let my_seat = seat.clone();
let status_listeners = status_listeners.clone();
let device =
DataControlDevice::init_for_seat(manager, seat, move |event, dispatch_data| {
notify_status_listeners(&my_seat, &event, dispatch_data, &status_listeners);
});
devices.push((seat.clone(), device));
}
Self::Pending { seats, .. } => {
seats.push(seat.clone());
}
}
}
fn remove_seat(&mut self, seat: &WlSeat) {
match self {
Self::Ready { devices, .. } => devices.retain(|(s, _)| s != seat),
Self::Pending { seats, .. } => seats.retain(|s| s != seat),
}
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
match self {
Self::Ready { manager, .. } => {
let source = DataControlSource::new(manager, mime_types, callback);
Some(source)
}
Self::Pending { .. } => None,
}
}
fn with_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
match self {
Self::Ready { devices, .. } => {
let device = devices
.iter()
.find_map(|(s, device)| if s == seat { Some(device) } else { None });
device.map_or(Err(MissingGlobal), |device| {
f(device);
Ok(())
}) })
} }
Self::Pending { .. } => Err(MissingGlobal),
/// creates a data source for copy paste
pub fn create_copy_paste_source<'s, D, I>(
&self,
qh: &QueueHandle<D>,
mime_types: I,
) -> CopyPasteSource
where
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
I: IntoIterator<Item = &'s str>,
{
CopyPasteSource {
inner: self.create_data_control_source(qh, mime_types),
} }
} }
/// creates a data source
fn create_data_control_source<'s, D, I>(
&self,
qh: &QueueHandle<D>,
mime_types: I,
) -> ZwlrDataControlSourceV1
where
D: Dispatch<ZwlrDataControlSourceV1, DataControlSourceData> + 'static,
I: IntoIterator<Item = &'s str>,
{
let source =
self.create_data_control_source_with_data(qh, DataControlSourceData::default());
for mime in mime_types {
source.offer(mime.to_string());
}
source
}
/// create a new data source for a given seat with some user data
pub fn create_data_control_source_with_data<D, U>(
&self,
qh: &QueueHandle<D>,
data: U,
) -> ZwlrDataControlSourceV1
where
D: Dispatch<ZwlrDataControlSourceV1, U> + 'static,
U: DataControlSourceDataExt + 'static,
{
self.manager.create_data_source(qh, data)
}
/// create a new data device for a given seat
pub fn get_data_device<D>(&self, qh: &QueueHandle<D>, seat: &WlSeat) -> DataControlDevice
where
D: Dispatch<ZwlrDataControlDeviceV1, DataControlDeviceData> + 'static,
{
DataControlDevice {
device: self.get_data_control_device_with_data(
qh,
seat,
DataControlDeviceData::default(),
),
}
}
/// create a new data device for a given seat with some user data
pub fn get_data_control_device_with_data<D, U>(
&self,
qh: &QueueHandle<D>,
seat: &WlSeat,
data: U,
) -> ZwlrDataControlDeviceV1
where
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
U: DataControlDeviceDataExt + 'static,
{
self.manager.get_data_device(seat, qh, data)
}
} }
impl ProvidesBoundGlobal<ZwlrDataControlManagerV1, 2> for DataControlDeviceManagerState { pub struct DataControlDeviceHandler {
fn bound_global(&self) -> Result<ZwlrDataControlManagerV1, GlobalError> { inner: Rc<RefCell<DataControlDeviceHandlerInner>>,
Ok(self.manager.clone()) status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>>,
_seat_listener: SeatListener,
}
impl DataControlDeviceHandler {
pub fn init<S>(seat_handler: &mut S) -> Self
where
S: SeatHandling,
{
let status_listeners = Rc::new(RefCell::new(Vec::new()));
let inner = Rc::new(RefCell::new(DataControlDeviceHandlerInner::Pending {
seats: Vec::new(),
status_listeners: status_listeners.clone(),
}));
let seat_inner = inner.clone();
let seat_listener = seat_handler.listen(move |seat, seat_data, _| {
if seat_data.defunct {
seat_inner.borrow_mut().remove_seat(&seat);
} else {
seat_inner.borrow_mut().new_seat(&seat);
}
});
Self {
inner,
_seat_listener: seat_listener,
status_listeners,
}
} }
} }
impl<D> Dispatch<ZwlrDataControlManagerV1, GlobalData, D> for DataControlDeviceManagerState impl GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
where fn created(
D: Dispatch<ZwlrDataControlManagerV1, GlobalData>, &mut self,
{ registry: Attached<WlRegistry>,
fn event( id: u32,
_state: &mut D, version: u32,
_proxy: &ZwlrDataControlManagerV1, _ddata: DispatchData,
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
_data: &GlobalData,
_conn: &Connection,
_qhandle: &QueueHandle<D>,
) { ) {
unreachable!() // data control manager is supported until version 2
let version = std::cmp::min(version, 2);
let manager = registry.bind::<ZwlrDataControlManagerV1>(version, id);
self.inner.borrow_mut().init_manager((*manager).clone());
}
fn get(&self) -> Option<Attached<ZwlrDataControlManagerV1>> {
RefCell::borrow(&self.inner).get_manager()
} }
} }
type DataControlDeviceStatusCallback =
dyn FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
/// Notifies the callbacks of an event on the data device
fn notify_status_listeners(
seat: &WlSeat,
event: &DataControlDeviceEvent,
mut ddata: DispatchData,
listeners: &RefCell<Vec<rc::Weak<RefCell<DataControlDeviceStatusCallback>>>>,
) {
listeners.borrow_mut().retain(|lst| {
rc::Weak::upgrade(lst).map_or(false, |cb| {
(cb.borrow_mut())(seat.clone(), event.clone(), ddata.reborrow());
true
})
});
}
pub struct DataControlDeviceStatusListener {
_cb: Rc<RefCell<DataControlDeviceStatusCallback>>,
}
pub trait DataControlDeviceHandling {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice);
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static;
}
impl DataControlDeviceHandling for DataControlDeviceHandler {
fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
where
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
DataControlDeviceStatusListener { _cb: rc }
}
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
RefCell::borrow(&self.inner).with_device(seat, f)
}
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
RefCell::borrow(&self.inner).create_source(mime_types, callback)
}
}
pub fn listen_to_devices<E, F>(env: &Environment<E>, f: F) -> DataControlDeviceStatusListener
where
E: DataControlDeviceHandling,
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
{
env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
}

View File

@@ -3,28 +3,28 @@ pub mod manager;
pub mod offer; pub mod offer;
pub mod source; pub mod source;
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler}; use super::Env;
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer}; use crate::clients::wayland::DData;
use self::source::DataControlSourceHandler; use crate::send;
use crate::clients::wayland::Environment; use color_eyre::Report;
use crate::{lock, send}; use device::{DataControlDevice, DataControlDeviceEvent};
use device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ}; use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener};
use nix::sys::epoll::{epoll_create, epoll_ctl, epoll_wait, EpollEvent, EpollFlags, EpollOp}; use smithay_client_toolkit::data_device::WritePipe;
use smithay_client_toolkit::data_device_manager::WritePipe; use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::reexports::calloop::RegistrationToken; use smithay_client_toolkit::reexports::calloop::LoopHandle;
use std::cmp::min; use smithay_client_toolkit::MissingGlobal;
use std::fmt::{Debug, Formatter}; use source::DataControlSource;
use std::fs::File; use std::fs::File;
use std::io::{ErrorKind, Read, Write}; use std::io;
use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::{fs, io}; use std::time::UNIX_EPOCH;
use tokio::sync::broadcast;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use wayland_client::{Connection, QueueHandle}; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; use wayland_client::DispatchData;
static COUNTER: AtomicUsize = AtomicUsize::new(1); static COUNTER: AtomicUsize = AtomicUsize::new(1);
@@ -34,11 +34,6 @@ fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)
} }
pub struct SelectionOfferItem {
offer: SelectionOffer,
token: Option<RegistrationToken>,
}
#[derive(Debug, Clone, Eq)] #[derive(Debug, Clone, Eq)]
pub struct ClipboardItem { pub struct ClipboardItem {
pub id: usize, pub id: usize,
@@ -52,27 +47,77 @@ impl PartialEq<Self> for ClipboardItem {
} }
} }
#[derive(Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClipboardValue { pub enum ClipboardValue {
Text(String), Text(String),
Image(Bytes), Image(Bytes),
Other, Other,
} }
impl Debug for ClipboardValue { impl DataControlDeviceHandling for Env {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn listen<F>(&mut self, f: F) -> DataControlDeviceStatusListener
write!( where
f, F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
"{}", {
match self { self.data_control_device.listen(f)
Self::Text(text) => text.clone(),
Self::Image(bytes) => {
format!("[{} Bytes]", bytes.len())
} }
Self::Other => "[Unknown]".to_string(),
fn with_data_control_device<F>(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
where
F: FnOnce(&DataControlDevice),
{
self.data_control_device.with_data_control_device(seat, f)
} }
fn create_source<F>(&self, mime_types: Vec<String>, callback: F) -> Option<DataControlSource>
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
self.data_control_device.create_source(mime_types, callback)
}
}
pub fn copy_to_clipboard<E>(
env: &Environment<E>,
seat: &WlSeat,
item: &ClipboardItem,
) -> Result<(), MissingGlobal>
where
E: DataControlDeviceHandling,
{
debug!("Copying item with id {} [{}]", item.id, item.mime_type);
trace!("Copying: {item:?}");
let item = item.clone();
env.with_inner(|env| {
let mime_types = vec![INTERNAL_MIME_TYPE.to_string(), item.mime_type];
let source = env.create_source(mime_types, move |mime_type, mut pipe, _ddata| {
debug!(
"Triggering source callback for item with id {} [{}]",
item.id, mime_type
);
// FIXME: Not working for large (buffered) values in xwayland
let bytes = match &item.value {
ClipboardValue::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(
io::ErrorKind::Other,
"Attempted to copy unsupported mime type",
) )
),
};
if let Err(err) = pipe.write_all(bytes) {
error!("{err:?}");
} }
});
env.with_data_control_device(seat, |device| device.set_selection(&source))
})
} }
#[derive(Debug)] #[derive(Debug)]
@@ -88,8 +133,11 @@ enum MimeTypeCategory {
} }
impl MimeType { impl MimeType {
fn parse(mime_type: &str) -> Option<Self> { fn parse(mime_types: &[String]) -> Option<Self> {
match mime_type.to_lowercase().as_str() { mime_types
.iter()
.map(|s| s.to_lowercase())
.find_map(|mime_type| match mime_type.as_str() {
"text" "text"
| "string" | "string"
| "utf8_string" | "utf8_string"
@@ -98,42 +146,94 @@ impl MimeType {
| "text/plain;charset=iso-8859-1" | "text/plain;charset=iso-8859-1"
| "text/plain;charset=us-ascii" | "text/plain;charset=us-ascii"
| "text/plain;charset=unicode" => Some(Self { | "text/plain;charset=unicode" => Some(Self {
value: mime_type.to_string(), value: mime_type,
category: MimeTypeCategory::Text, category: MimeTypeCategory::Text,
}), }),
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp" "image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
| "image/x-bmp" | "image/icon" => Some(Self { | "image/x-bmp" | "image/icon" => Some(Self {
value: mime_type.to_string(), value: mime_type,
category: MimeTypeCategory::Image, category: MimeTypeCategory::Image,
}), }),
_ => None, _ => None,
} })
}
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
mime_types.iter().find_map(|mime| Self::parse(mime))
} }
} }
impl Environment { pub fn receive_offer(
pub fn copy_to_clipboard(&mut self, item: Arc<ClipboardItem>, qh: &QueueHandle<Self>) { event: DataControlDeviceEvent,
debug!("Copying item to clipboard: {item:?}"); handle: &LoopHandle<DData>,
tx: broadcast::Sender<Arc<ClipboardItem>>,
mut ddata: DispatchData,
) {
let timestamp = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Could not get epoch, system time is probably very wrong")
.as_nanos();
// TODO: Proper device tracking let offer = event.0;
let device = self.data_control_devices.first();
if let Some(device) = device {
let source = self
.data_control_device_manager_state
.create_copy_paste_source(qh, [INTERNAL_MIME_TYPE, item.mime_type.as_str()]);
source.set_selection(&device.device); let ddata = ddata
self.copy_paste_sources.push(source); .get::<DData>()
.expect("Expected dispatch data to exist");
lock!(self.clipboard).replace(item); let handle2 = handle.clone();
let res = offer.with_mime_types(|mime_types| {
debug!("Offer mime types: {mime_types:?}");
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
debug!("Skipping value provided by bar");
return Ok(());
}
let mime_type = MimeType::parse(mime_types);
debug!("Detected mime type: {mime_type:?}");
match mime_type {
Some(mime_type) => {
debug!("[{timestamp}] Sending clipboard read request ({mime_type:?})");
let read_pipe = offer.receive(mime_type.value.clone())?;
let source = handle.insert_source(read_pipe, move |(), file, ddata| {
debug!(
"[{timestamp}] Reading clipboard contents ({:?})",
&mime_type.category
);
match read_file(&mime_type, file) {
Ok(item) => {
send!(tx, Arc::new(item));
}
Err(err) => error!("{err:?}"),
}
if let Some(src) = ddata.offer_tokens.remove(&timestamp) {
handle2.remove(src);
}
})?;
ddata.offer_tokens.insert(timestamp, source);
}
None => {
// send an event so the clipboard module is aware it's changed
send!(
tx,
Arc::new(ClipboardItem {
id: usize::MAX,
mime_type: String::new(),
value: ClipboardValue::Other
})
);
} }
} }
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> { Ok::<(), Report>(())
});
if let Err(err) = res {
error!("{err:?}");
}
}
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
let value = match mime_type.category { let value = match mime_type.category {
MimeTypeCategory::Text => { MimeTypeCategory::Text => {
let mut txt = String::new(); let mut txt = String::new();
@@ -155,241 +255,4 @@ impl Environment {
value, value,
mime_type: mime_type.value.clone(), mime_type: mime_type.value.clone(),
}) })
}
}
impl DataControlDeviceHandler for Environment {
fn selection(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
data_device: DataControlDevice,
) {
debug!("Handler received selection event");
let mime_types = data_device.selection_mime_types();
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
return;
}
if let Some(offer) = data_device.selection_offer() {
self.selection_offers
.push(SelectionOfferItem { offer, token: None });
let cur_offer = self
.selection_offers
.last_mut()
.expect("Failed to get current offer");
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
lock!(self.clipboard).take();
// send an event so the clipboard module is aware it's changed
send!(
self.clipboard_tx,
Arc::new(ClipboardItem {
id: usize::MAX,
mime_type: String::new(),
value: ClipboardValue::Other
})
);
return;
};
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
let offer_clone = cur_offer.offer.clone();
let tx = self.clipboard_tx.clone();
let clipboard = self.clipboard.clone();
let token = self
.loop_handle
.insert_source(read_pipe, move |_, file, state| {
let item = state
.selection_offers
.iter()
.position(|o| o.offer == offer_clone)
.map(|p| state.selection_offers.remove(p))
.expect("Failed to find selection offer item");
match Self::read_file(&mime_type, file) {
Ok(item) => {
let item = Arc::new(item);
lock!(clipboard).replace(item.clone());
send!(tx, item);
}
Err(err) => error!("{err:?}"),
}
state
.loop_handle
.remove(item.token.expect("Missing item token"));
});
match token {
Ok(token) => {
cur_offer.token.replace(token);
}
Err(err) => error!("{err:?}"),
}
}
}
}
}
impl DataControlOfferHandler for Environment {
fn offer(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_offer: &mut DataControlDeviceOffer,
_mime_type: String,
) {
debug!("Handler received offer");
}
}
impl DataControlSourceHandler for Environment {
fn accept_mime(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_source: &ZwlrDataControlSourceV1,
mime: Option<String>,
) {
debug!("Accepted mime type: {mime:?}");
}
fn send_request(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: String,
write_pipe: WritePipe,
) {
debug!("Handler received source send request event ({mime})");
if let Some(item) = lock!(self.clipboard).clone() {
let fd = OwnedFd::from(write_pipe);
if self
.copy_paste_sources
.iter_mut()
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
{
trace!("Source found, writing to file");
let mut bytes = match &item.value {
ClipboardValue::Text(text) => text.as_bytes(),
ClipboardValue::Image(bytes) => bytes.as_ref(),
ClipboardValue::Other => panic!(
"{:?}",
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type",)
),
};
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
.expect("Failed to increase pipe size");
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
trace!("Num bytes: {}", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let mut epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
let epoll_fd = epoll_create().unwrap();
epoll_ctl(
epoll_fd,
EpollOp::EpollCtlAdd,
fd.as_raw_fd(),
&mut epoll_event,
)
.unwrap();
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
epoll_wait(epoll_fd, &mut events, 100).expect("Failed to wait to epoll");
match file.write(chunk) {
Ok(_) => bytes = &bytes[chunk.len()..],
Err(err) => {
error!("{err:?}");
break;
}
}
}
// for chunk in bytes.chunks(pipe_size as usize) {
// trace!("Writing chunk");
// file.write(chunk).expect("Failed to write chunk to buffer");
// file.flush().expect("Failed to flush to file");
// }
// match file.write_vectored(&bytes.chunks(pipe_size as usize).map(IoSlice::new).collect::<Vec<_>>()) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
// match file.write_all(bytes) {
// Ok(_) => debug!("Copied item"),
// Err(err) => error!("{err:?}"),
// }
} else {
error!("Failed to find source");
}
}
}
fn cancelled(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
) {
debug!("Handler received source cancelled event");
self.copy_paste_sources
.iter()
.position(|s| s.inner() == source)
.map(|pos| self.copy_paste_sources.remove(pos));
source.destroy();
}
}
/// Attempts to increase the fd pipe size to the requested number of bytes.
/// The kernel will automatically round this up to the nearest page size.
/// If the requested size is larger than the kernel max (normally 1MB),
/// it will be clamped at this.
///
/// Returns the new size if succeeded
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
// clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
.expect("Failed to find pipe-max-size virtual kernel file")
.trim()
.parse::<usize>()
.expect("Failed to parse pipe-max-size contents");
let size = min(size, max_pipe_size);
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
trace!("Current pipe size: {curr_size}");
let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
trace!("New pipe size: {res}");
if res < size as i32 {
return Err(io::Error::last_os_error());
}
res
} else {
size as i32
};
Ok(new_size)
} }

View File

@@ -1,183 +1,74 @@
use super::manager::DataControlDeviceManagerState;
use crate::lock; use crate::lock;
use nix::fcntl::OFlag; use nix::fcntl::OFlag;
use nix::unistd::{close, pipe2}; use nix::unistd::{close, pipe2};
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError; use smithay_client_toolkit::data_device::ReadPipe;
use smithay_client_toolkit::data_device_manager::ReadPipe; use std::io;
use std::ops::DerefMut;
use std::os::fd::FromRawFd; use std::os::fd::FromRawFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::{debug, warn}; use tracing::warn;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::Main;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{ use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{
Event, ZwlrDataControlOfferV1, Event, ZwlrDataControlOfferV1,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UndeterminedOffer { struct Inner {
pub(crate) data_offer: Option<ZwlrDataControlOfferV1>, mime_types: Vec<String>,
}
impl PartialEq for UndeterminedOffer {
fn eq(&self, other: &Self) -> bool {
self.data_offer == other.data_offer
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SelectionOffer { pub struct DataControlOffer {
pub data_offer: ZwlrDataControlOfferV1, inner: Arc<Mutex<Inner>>,
pub(crate) offer: ZwlrDataControlOfferV1,
} }
impl PartialEq for SelectionOffer { impl DataControlOffer {
fn eq(&self, other: &Self) -> bool { pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> Self {
self.data_offer == other.data_offer let inner = Arc::new(Mutex::new(Inner {
} mime_types: Vec::new(),
} }));
impl SelectionOffer { {
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> { let inner = inner.clone();
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DataControlDeviceOffer {
Selection(SelectionOffer),
Undetermined(UndeterminedOffer),
}
impl Default for DataControlDeviceOffer {
fn default() -> Self {
Self::Undetermined(UndeterminedOffer { data_offer: None })
}
}
#[derive(Debug, Default)]
pub struct DataControlOfferData {
pub(crate) inner: Arc<Mutex<DataControlDeviceOfferInner>>,
}
#[derive(Debug, Default)]
pub struct DataControlDeviceOfferInner {
pub(crate) offer: DataControlDeviceOffer,
pub(crate) mime_types: Vec<String>,
}
impl DataControlOfferData {
pub(crate) fn push_mime_type(&self, mime_type: String) {
lock!(self.inner).mime_types.push(mime_type);
}
pub(crate) fn to_selection_offer(&self) {
let mut inner = lock!(self.inner);
match &mut inner.deref_mut().offer {
DataControlDeviceOffer::Selection(_) => {}
DataControlDeviceOffer::Undetermined(o) => {
inner.offer = DataControlDeviceOffer::Selection(SelectionOffer {
data_offer: o.data_offer.clone().expect("Missing current data offer"),
});
}
}
}
pub(crate) fn init_undetermined_offer(&self, offer: &ZwlrDataControlOfferV1) {
let mut inner = lock!(self.inner);
match &mut inner.deref_mut().offer {
DataControlDeviceOffer::Selection(_) => {
inner.offer = DataControlDeviceOffer::Undetermined(UndeterminedOffer {
data_offer: Some(offer.clone()),
});
}
DataControlDeviceOffer::Undetermined(o) => {
o.data_offer = Some(offer.clone());
}
}
}
}
pub trait DataControlOfferDataExt {
fn data_control_offer_data(&self) -> &DataControlOfferData;
fn mime_types(&self) -> Vec<String>;
fn as_selection_offer(&self) -> Option<SelectionOffer>;
}
impl DataControlOfferDataExt for DataControlOfferData {
fn data_control_offer_data(&self) -> &DataControlOfferData {
self
}
fn mime_types(&self) -> Vec<String> {
lock!(self.inner).mime_types.clone()
}
fn as_selection_offer(&self) -> Option<SelectionOffer> {
match &lock!(self.inner).offer {
DataControlDeviceOffer::Selection(o) => Some(o.clone()),
DataControlDeviceOffer::Undetermined(_) => None,
}
}
}
/// Handler trait for `DataOffer` events.
///
/// The functions defined in this trait are called as `DataOffer` events are received from the compositor.
pub trait DataControlOfferHandler: Sized {
// Called for each mime type the data offer advertises.
fn offer(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DataControlDeviceOffer,
mime_type: String,
);
}
impl<D, U> Dispatch<ZwlrDataControlOfferV1, U, D> for DataControlDeviceManagerState
where
D: Dispatch<ZwlrDataControlOfferV1, U> + DataControlOfferHandler,
U: DataControlOfferDataExt,
{
fn event(
state: &mut D,
_offer: &ZwlrDataControlOfferV1,
event: <ZwlrDataControlOfferV1 as Proxy>::Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
let data = data.data_control_offer_data();
offer.quick_assign(move |_, event, _| {
let mut inner = lock!(inner);
if let Event::Offer { mime_type } = event { if let Event::Offer { mime_type } = event {
debug!("Adding new offer with type '{mime_type}'"); inner.mime_types.push(mime_type);
data.push_mime_type(mime_type.clone());
state.offer(conn, qh, &mut lock!(data.inner).offer, mime_type);
} }
});
} }
}
/// Request to receive the data of a given mime type. Self {
/// offer: offer.detach(),
/// You can do this several times, as a reaction to motion of inner,
/// the dnd cursor, or to inspect the data in order to choose your }
/// response. }
///
/// Note that you should *not* read the contents right away in a pub fn with_mime_types<F, T>(&self, f: F) -> T
/// blocking way, as you may deadlock your application doing so. where
/// At least make sure you flush your events to the server before F: FnOnce(&[String]) -> T,
/// doing so. {
/// let inner = lock!(self.inner);
/// Fails if too many file descriptors were already open and a pipe f(&inner.mime_types)
/// could not be created. }
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<ReadPipe> {
pub fn receive(&self, mime_type: String) -> io::Result<ReadPipe> {
// create a pipe // create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?; let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
offer.receive(mime_type, writefd); self.offer.receive(mime_type, writefd);
if let Err(err) = close(writefd) { if let Err(err) = close(writefd) {
warn!("Failed to close write pipe: {}", err); warn!("Failed to close write pipe: {}", err);
} }
Ok(unsafe { FromRawFd::from_raw_fd(readfd) }) Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
}
}
impl Drop for DataControlOffer {
fn drop(&mut self) {
self.offer.destroy();
}
} }

View File

@@ -1,101 +1,54 @@
use super::device::DataControlDevice; use smithay_client_toolkit::data_device::WritePipe;
use super::manager::DataControlDeviceManagerState; use std::os::fd::FromRawFd;
use smithay_client_toolkit::data_device_manager::WritePipe; use wayland_client::{Attached, DispatchData};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_protocols::wlr::unstable::data_control::v1::client::{
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{ zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
Event, ZwlrDataControlSourceV1, zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1},
}; };
#[derive(Debug, Default)] fn data_control_source_impl<F>(
pub struct DataControlSourceData {}
pub trait DataControlSourceDataExt: Send + Sync {
fn data_source_data(&self) -> &DataControlSourceData;
}
impl DataControlSourceDataExt for DataControlSourceData {
fn data_source_data(&self) -> &DataControlSourceData {
self
}
}
/// Handler trait for `DataSource` events.
///
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
pub trait DataControlSourceHandler: Sized {
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
fn accept_mime(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1, source: &ZwlrDataControlSourceV1,
mime: Option<String>, event: Event,
); implem: &mut F,
ddata: DispatchData,
/// The client has requested the data for this source to be sent. ) where
/// Send the data, then close the fd. F: FnMut(String, WritePipe, DispatchData),
fn send_request(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
mime: String,
fd: WritePipe,
);
/// The data source is no longer valid
/// Cleanup & destroy this resource
fn cancelled(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
source: &ZwlrDataControlSourceV1,
);
}
impl<D, U> Dispatch<ZwlrDataControlSourceV1, U, D> for DataControlDeviceManagerState
where
D: Dispatch<ZwlrDataControlSourceV1, U> + DataControlSourceHandler,
U: DataControlSourceDataExt,
{ {
fn event(
state: &mut D,
source: &ZwlrDataControlSourceV1,
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
_data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
match event { match event {
Event::Send { mime_type, fd } => { Event::Send { mime_type, fd } => {
state.send_request(conn, qh, source, mime_type, fd.into()); let pipe = unsafe { FromRawFd::from_raw_fd(fd) };
} implem(mime_type, pipe, ddata);
Event::Cancelled => {
state.cancelled(conn, qh, source);
}
_ => {}
} }
Event::Cancelled => source.destroy(),
_ => unreachable!(),
} }
} }
#[derive(Debug, PartialEq, Eq, Clone)] pub struct DataControlSource {
pub struct CopyPasteSource { pub(crate) source: ZwlrDataControlSourceV1,
pub(crate) inner: ZwlrDataControlSourceV1,
} }
impl CopyPasteSource { impl DataControlSource {
/// Set the selection of the provided data device as a response to the event with with provided serial. pub fn new<F>(
pub fn set_selection(&self, device: &DataControlDevice) { manager: &Attached<ZwlrDataControlManagerV1>,
device.device.set_selection(Some(&self.inner)); mime_types: Vec<String>,
mut callback: F,
) -> Self
where
F: FnMut(String, WritePipe, DispatchData) + 'static,
{
let source = manager.create_data_source();
source.quick_assign(move |source, evt, ddata| {
data_control_source_impl(&source, evt, &mut callback, ddata);
});
for mime_type in mime_types {
source.offer(mime_type);
} }
pub const fn inner(&self) -> &ZwlrDataControlSourceV1 { Self {
&self.inner source: source.detach(),
} }
}
impl Drop for CopyPasteSource {
fn drop(&mut self) {
self.inner.destroy();
} }
} }

View File

@@ -1,15 +1,13 @@
use super::manager::ToplevelManagerState;
use crate::lock;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use tracing::trace; use tracing::trace;
use wayland_client::protocol::wl_output::WlOutput; use wayland_client::{DispatchData, Main};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{Event, ZwlrForeignToplevelHandleV1};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use crate::write_lock;
use wayland_protocols_wlr::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::{
Event, ZwlrForeignToplevelHandleV1, const STATE_ACTIVE: u32 = 2;
}; const STATE_FULLSCREEN: u32 = 3;
static COUNTER: AtomicUsize = AtomicUsize::new(1); static COUNTER: AtomicUsize = AtomicUsize::new(1);
@@ -17,119 +15,69 @@ fn get_id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct ToplevelHandle {
pub handle: ZwlrForeignToplevelHandleV1,
}
impl PartialEq for ToplevelHandle {
fn eq(&self, other: &Self) -> bool {
self.handle == other.handle
}
}
impl ToplevelHandle {
pub fn info(&self) -> Option<ToplevelInfo> {
trace!("Retrieving handle info");
let data = self.handle.data::<ToplevelHandleData>()?;
data.info()
}
pub fn focus(&self, seat: &WlSeat) {
trace!("Activating handle");
self.handle.activate(seat);
}
}
#[derive(Debug, Default)]
pub struct ToplevelHandleData {
pub inner: Arc<Mutex<ToplevelHandleDataInner>>,
}
impl ToplevelHandleData {
fn info(&self) -> Option<ToplevelInfo> {
lock!(self.inner).current_info.clone()
}
}
#[derive(Debug, Default)]
pub struct ToplevelHandleDataInner {
initial_done: bool,
output: Option<WlOutput>,
current_info: Option<ToplevelInfo>,
pending_info: ToplevelInfo,
}
#[derive(Debug, Clone)]
pub struct ToplevelInfo { pub struct ToplevelInfo {
pub id: usize, pub id: usize,
pub app_id: String, pub app_id: String,
pub title: String, pub title: String,
pub active: bool,
pub fullscreen: bool, pub fullscreen: bool,
pub focused: bool,
ready: bool,
} }
impl Default for ToplevelInfo { impl ToplevelInfo {
fn default() -> Self { fn new() -> Self {
let id = get_id();
Self { Self {
id: get_id(), id,
app_id: String::new(), ..Default::default()
title: String::new(),
fullscreen: false,
focused: false,
} }
} }
} }
pub trait ToplevelHandleDataExt { pub struct Toplevel;
fn toplevel_handle_data(&self) -> &ToplevelHandleData;
#[derive(Debug, Clone)]
pub struct ToplevelEvent {
pub toplevel: ToplevelInfo,
pub change: ToplevelChange,
} }
impl ToplevelHandleDataExt for ToplevelHandleData { #[derive(Debug, Clone, PartialEq, Eq)]
fn toplevel_handle_data(&self) -> &ToplevelHandleData { pub enum ToplevelChange {
self New,
} Close,
Title(String),
Focus(bool),
Fullscreen(bool),
} }
pub trait ToplevelHandleHandler: Sized { fn toplevel_implem<F>(event: Event, info: &mut ToplevelInfo, implem: &mut F, ddata: DispatchData)
fn new_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn update_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
fn remove_handle(&mut self, conn: &Connection, qh: &QueueHandle<Self>, handle: ToplevelHandle);
}
impl<D, U> Dispatch<ZwlrForeignToplevelHandleV1, U, D> for ToplevelManagerState
where where
D: Dispatch<ZwlrForeignToplevelHandleV1, U> + ToplevelHandleHandler, F: FnMut(ToplevelEvent, DispatchData),
U: ToplevelHandleDataExt,
{ {
fn event( trace!("event: {event:?} (info: {info:?})");
state: &mut D,
handle: &ZwlrForeignToplevelHandleV1,
event: Event,
data: &U,
conn: &Connection,
qh: &QueueHandle<D>,
) {
const STATE_ACTIVE: u32 = 2;
const STATE_FULLSCREEN: u32 = 3;
let data = data.toplevel_handle_data(); let change = match event {
Event::AppId { app_id } => {
trace!("Processing handle event: {event:?}"); info.app_id = app_id;
None
match event { }
Event::Title { title } => { Event::Title { title } => {
lock!(data.inner).pending_info.title = title; info.title = title.clone();
if info.ready {
Some(ToplevelChange::Title(title))
} else {
None
}
} }
Event::AppId { app_id } => lock!(data.inner).pending_info.app_id = app_id,
Event::State { state } => { Event::State { state } => {
// state is received as a `Vec<u8>` where every 4 bytes make up a `u32` // state is received as a `Vec<u8>` where every 4 bytes make up a `u32`
// the u32 then represents a value in the `State` enum. // the u32 then represents a value in the `State` enum.
assert_eq!(state.len() % 4, 0); assert_eq!(state.len() % 4, 0);
let state = (0..state.len() / 4) let state = (0..state.len() / 4)
.map(|i| { .map(|i| {
let slice: [u8; 4] = state[i * 4..i * 4 + 4] let slice: [u8; 4] = state[i * 4..i * 4 + 4]
@@ -139,46 +87,66 @@ where
}) })
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
lock!(data.inner).pending_info.focused = state.contains(&STATE_ACTIVE); let new_active = state.contains(&STATE_ACTIVE);
lock!(data.inner).pending_info.fullscreen = state.contains(&STATE_FULLSCREEN); let new_fullscreen = state.contains(&STATE_FULLSCREEN);
}
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
Event::Closed => state.remove_handle(
conn,
qh,
ToplevelHandle {
handle: handle.clone(),
},
),
Event::Done => {
{
let pending_info = lock!(data.inner).pending_info.clone();
lock!(data.inner).current_info = Some(pending_info);
}
if !lock!(data.inner).initial_done { let change = if info.ready && new_active != info.active {
lock!(data.inner).initial_done = true; Some(ToplevelChange::Focus(new_active))
state.new_handle( } else if info.ready && new_fullscreen != info.fullscreen {
conn, Some(ToplevelChange::Fullscreen(new_fullscreen))
qh,
ToplevelHandle {
handle: handle.clone(),
},
);
} else { } else {
state.update_handle( None
conn, };
qh,
ToplevelHandle { info.active = new_active;
handle: handle.clone(), info.fullscreen = new_fullscreen;
},
); change
}
Event::Closed => {
if info.ready {
Some(ToplevelChange::Close)
} else {
None
} }
} }
_ => {} Event::OutputEnter { output: _ }
| Event::OutputLeave { output: _ }
| Event::Parent { parent: _ } => None,
Event::Done => {
if info.ready || info.app_id.is_empty() {
None
} else {
info.ready = true;
Some(ToplevelChange::New)
}
} }
trace!("Event processed"); _ => unreachable!(),
};
if let Some(change) = change {
let event = ToplevelEvent {
change,
toplevel: info.clone(),
};
implem(event, ddata);
}
}
impl Toplevel {
pub fn init<F>(handle: &Main<ZwlrForeignToplevelHandleV1>, mut callback: F) -> Self
where
F: FnMut(ToplevelEvent, DispatchData) + 'static,
{
let inner = Arc::new(RwLock::new(ToplevelInfo::new()));
handle.quick_assign(move |_handle, event, ddata| {
let mut inner = write_lock!(inner);
toplevel_implem(event, &mut inner, &mut callback, ddata);
});
Self
} }
} }

View File

@@ -1,86 +1,163 @@
use super::handle::{ToplevelHandleData, ToplevelHandleDataExt, ToplevelHandleHandler}; use super::handle::{Toplevel, ToplevelEvent};
use smithay_client_toolkit::error::GlobalError; use crate::wayland::LazyGlobal;
use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal}; use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use std::marker::PhantomData; use std::cell::RefCell;
use tracing::{debug, warn}; use std::rc::{self, Rc};
use wayland_client::globals::{BindError, GlobalList}; use tracing::warn;
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle}; use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_protocols_wlr::foreign_toplevel::v1::client::{ use wayland_client::{Attached, DispatchData};
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
}; };
pub struct ToplevelManagerState<V = ToplevelHandleData> { struct ToplevelHandlerInner {
manager: ZwlrForeignToplevelManagerV1, manager: LazyGlobal<ZwlrForeignToplevelManagerV1>,
_phantom: PhantomData<V>, registry: Option<Attached<WlRegistry>>,
toplevels: Vec<Toplevel>,
} }
impl ToplevelManagerState { impl ToplevelHandlerInner {
pub fn bind<State>(globals: &GlobalList, qh: &QueueHandle<State>) -> Result<Self, BindError> const fn new() -> Self {
where let toplevels = vec![];
State: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, State> + 'static,
{ Self {
let manager = globals.bind(qh, 1..=3, GlobalData)?; registry: None,
debug!("Bound to ZwlForeignToplevelManagerV1 global"); manager: LazyGlobal::Unknown,
Ok(Self { toplevels,
manager, }
_phantom: PhantomData,
})
} }
} }
pub trait ToplevelManagerHandler: Sized { pub struct ToplevelHandler {
/// Advertises a new toplevel. inner: Rc<RefCell<ToplevelHandlerInner>>,
fn toplevel( status_listeners: Rc<RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>>,
}
impl ToplevelHandler {
pub fn init() -> Self {
let inner = Rc::new(RefCell::new(ToplevelHandlerInner::new()));
Self {
inner,
status_listeners: Rc::new(RefCell::new(Vec::new())),
}
}
}
impl GlobalHandler<ZwlrForeignToplevelManagerV1> for ToplevelHandler {
fn created(
&mut self, &mut self,
conn: &Connection, registry: Attached<WlRegistry>,
qh: &QueueHandle<Self>, id: u32,
manager: ToplevelManagerState, version: u32,
); _ddata: DispatchData,
}
impl ProvidesBoundGlobal<ZwlrForeignToplevelManagerV1, 3> for ToplevelManagerState {
fn bound_global(&self) -> Result<ZwlrForeignToplevelManagerV1, GlobalError> {
Ok(self.manager.clone())
}
}
impl<D, V> Dispatch<ZwlrForeignToplevelManagerV1, GlobalData, D> for ToplevelManagerState<V>
where
D: Dispatch<ZwlrForeignToplevelManagerV1, GlobalData>
+ Dispatch<ZwlrForeignToplevelHandleV1, V>
+ ToplevelManagerHandler
+ ToplevelHandleHandler
+ 'static,
V: ToplevelHandleDataExt + Default + 'static + Send + Sync,
{
event_created_child!(D, ZwlrForeignToplevelManagerV1, [
0 => (ZwlrForeignToplevelHandleV1, V::default())
]);
fn event(
state: &mut D,
toplevel_manager: &ZwlrForeignToplevelManagerV1,
event: Event,
_data: &GlobalData,
conn: &Connection,
qhandle: &QueueHandle<D>,
) { ) {
match event { let mut inner = RefCell::borrow_mut(&self.inner);
Event::Toplevel { toplevel: _ } => { if inner.registry.is_none() {
state.toplevel( inner.registry = Some(registry);
conn, }
qhandle, if matches!(inner.manager, LazyGlobal::Unknown) {
ToplevelManagerState { inner.manager = LazyGlobal::Seen { id, version }
manager: toplevel_manager.clone(), } else {
_phantom: PhantomData, warn!(
}, "Compositor advertised zwlr_foreign_toplevel_manager_v1 multiple times, ignoring."
); );
} }
Event::Finished => {
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
} }
_ => {}
fn get(&self) -> Option<Attached<ZwlrForeignToplevelManagerV1>> {
let mut inner = RefCell::borrow_mut(&self.inner);
match inner.manager {
LazyGlobal::Bound(ref mgr) => Some(mgr.clone()),
LazyGlobal::Unknown => None,
LazyGlobal::Seen { id, version } => {
let registry = inner.registry.as_ref().expect("Failed to get registry");
// current max protocol version = 3
let version = std::cmp::min(version, 3);
let manager = registry.bind::<ZwlrForeignToplevelManagerV1>(version, id);
{
let inner = self.inner.clone();
let status_listeners = self.status_listeners.clone();
manager.quick_assign(move |_, event, _ddata| {
let mut inner = RefCell::borrow_mut(&inner);
let status_listeners = status_listeners.clone();
match event {
zwlr_foreign_toplevel_manager_v1::Event::Toplevel {
toplevel: handle,
} => {
let toplevel =
Toplevel::init(&handle.clone(), move |event, ddata| {
notify_status_listeners(
&handle,
&event,
ddata,
&status_listeners,
);
});
inner.toplevels.push(toplevel);
}
zwlr_foreign_toplevel_manager_v1::Event::Finished => {}
_ => unreachable!(),
}
});
}
inner.manager = LazyGlobal::Bound((*manager).clone());
Some((*manager).clone())
}
} }
} }
} }
type ToplevelStatusCallback =
dyn FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
/// Notifies the callbacks of an event on the toplevel
fn notify_status_listeners(
toplevel: &ZwlrForeignToplevelHandleV1,
event: &ToplevelEvent,
mut ddata: DispatchData,
listeners: &RefCell<Vec<rc::Weak<RefCell<ToplevelStatusCallback>>>>,
) {
listeners.borrow_mut().retain(|lst| {
rc::Weak::upgrade(lst).map_or(false, |cb| {
(cb.borrow_mut())(toplevel.clone(), event.clone(), ddata.reborrow());
true
})
});
}
pub struct ToplevelStatusListener {
_cb: Rc<RefCell<ToplevelStatusCallback>>,
}
pub trait ToplevelHandling {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static;
}
impl ToplevelHandling for ToplevelHandler {
fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
where
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
let rc = Rc::new(RefCell::new(f)) as Rc<_>;
self.status_listeners.borrow_mut().push(Rc::downgrade(&rc));
ToplevelStatusListener { _cb: rc }
}
}
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener
where
E: ToplevelHandling,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
{
env.with_inner(move |inner| ToplevelHandling::listen(inner, f))
}

View File

@@ -1,84 +1,39 @@
use std::sync::RwLock;
use indexmap::IndexMap;
use tokio::sync::broadcast::Sender;
use tracing::trace;
use super::Env;
use handle::{ToplevelEvent, ToplevelChange, ToplevelInfo};
use manager::{ToplevelHandling, ToplevelStatusListener};
use wayland_client::DispatchData;
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use crate::{send, write_lock};
pub mod handle; pub mod handle;
pub mod manager; pub mod manager;
use self::handle::ToplevelHandleHandler; impl ToplevelHandling for Env {
use self::manager::{ToplevelManagerHandler, ToplevelManagerState}; fn listen<F>(&mut self, f: F) -> ToplevelStatusListener
use crate::clients::wayland::Environment; where
use tracing::{debug, error, trace}; F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
use wayland_client::{Connection, QueueHandle}; {
self.toplevel.listen(f)
use crate::send;
pub use handle::{ToplevelHandle, ToplevelInfo};
#[derive(Debug, Clone)]
pub enum ToplevelEvent {
New(ToplevelHandle),
Update(ToplevelHandle),
Remove(ToplevelHandle),
}
impl ToplevelManagerHandler for Environment {
fn toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_manager: ToplevelManagerState,
) {
debug!("Manager received new handle");
} }
} }
impl ToplevelHandleHandler for Environment { pub fn update_toplevels(
fn new_handle(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, handle: ToplevelHandle) { toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>,
debug!("Handler received new handle"); handle: ZwlrForeignToplevelHandleV1,
event: ToplevelEvent,
tx: &Sender<ToplevelEvent>,
) {
trace!("Received toplevel event: {:?}", event);
match handle.info() { if event.change == ToplevelChange::Close {
Some(info) => { write_lock!(toplevels).remove(&event.toplevel.id);
trace!("Adding new handle: {info:?}"); } else {
self.handles.insert(info.id, handle.clone()); write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
send!(self.toplevel_tx, ToplevelEvent::New(handle));
}
None => {
error!("Handle is missing information!");
}
}
} }
fn update_handle( send!(tx, event);
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ToplevelHandle,
) {
debug!("Handler received handle update");
match handle.info() {
Some(info) => {
trace!("Updating handle: {info:?}");
self.handles.insert(info.id, handle.clone());
send!(self.toplevel_tx, ToplevelEvent::Update(handle));
}
None => {
error!("Handle is missing information!");
}
}
}
fn remove_handle(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ToplevelHandle,
) {
debug!("Handler received handle close");
match handle.info() {
Some(info) => {
self.handles.remove(&info.id);
send!(self.toplevel_tx, ToplevelEvent::Remove(handle));
}
None => {
error!("Handle is missing information!");
}
}
}
} }

View File

@@ -1,20 +1,18 @@
use glib::signal::Inhibit;
use crate::dynamic_string::DynamicString; use crate::dynamic_string::DynamicString;
use crate::script::{Script, ScriptInput}; use crate::script::{Script, ScriptInput};
use crate::send; 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::{GestureClick, Orientation, Revealer, RevealerTransitionType, Widget};
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn; use tokio::spawn;
use tracing::trace; use tracing::trace;
/// Common configuration options /// Common configuration options
/// which can be set on every module. /// which can be set on every module.
#[derive(Debug, Default, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct CommonConfig { pub struct CommonConfig {
pub class: Option<String>,
pub name: Option<String>,
pub show_if: Option<ScriptInput>, pub show_if: Option<ScriptInput>,
pub transition_type: Option<TransitionType>, pub transition_type: Option<TransitionType>,
pub transition_duration: Option<u32>, pub transition_duration: Option<u32>,
@@ -40,16 +38,17 @@ pub enum TransitionType {
} }
impl TransitionType { impl TransitionType {
pub const fn to_revealer_transition_type( pub fn to_revealer_transition_type(&self, orientation: Orientation) -> RevealerTransitionType {
&self,
orientation: Orientation,
) -> RevealerTransitionType {
match (self, orientation) { match (self, orientation) {
(Self::SlideStart, Orientation::Horizontal) => RevealerTransitionType::SlideLeft, (TransitionType::SlideStart, Orientation::Horizontal) => {
(Self::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp, RevealerTransitionType::SlideLeft
(Self::SlideEnd, Orientation::Horizontal) => RevealerTransitionType::SlideRight, }
(Self::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown, (TransitionType::SlideStart, Orientation::Vertical) => RevealerTransitionType::SlideUp,
(Self::Crossfade, _) => RevealerTransitionType::Crossfade, (TransitionType::SlideEnd, Orientation::Horizontal) => {
RevealerTransitionType::SlideRight
}
(TransitionType::SlideEnd, Orientation::Vertical) => RevealerTransitionType::SlideDown,
(TransitionType::Crossfade, _) => RevealerTransitionType::Crossfade,
_ => RevealerTransitionType::None, _ => RevealerTransitionType::None,
} }
} }
@@ -57,14 +56,16 @@ impl TransitionType {
impl CommonConfig { impl CommonConfig {
/// Configures the module's container according to the common config options. /// Configures the module's container according to the common config options.
pub fn install_events(mut self, container: &EventBox, revealer: &Revealer) { pub fn install<W: IsA<Widget>>(mut self, widget: &W, revealer: &Revealer) {
self.install_show_if(container, revealer); self.install_show_if(widget, revealer);
let left_click_script = self.on_click_left.map(Script::new_polling); let left_click_script = self.on_click_left.map(Script::new_polling);
let middle_click_script = self.on_click_middle.map(Script::new_polling); let middle_click_script = self.on_click_middle.map(Script::new_polling);
let right_click_script = self.on_click_right.map(Script::new_polling); let right_click_script = self.on_click_right.map(Script::new_polling);
container.connect_button_press_event(move |_, event| { let gesture = GestureClick::new();
gesture.connect_pressed(move |_, event| {
let script = match event.button() { let script = match event.button() {
1 => left_click_script.as_ref(), 1 => left_click_script.as_ref(),
2 => middle_click_script.as_ref(), 2 => middle_click_script.as_ref(),
@@ -76,14 +77,12 @@ impl CommonConfig {
trace!("Running on-click script: {}", event.button()); trace!("Running on-click script: {}", event.button());
script.run_as_oneshot(None); script.run_as_oneshot(None);
} }
Inhibit(false)
}); });
let scroll_up_script = self.on_scroll_up.map(Script::new_polling); let scroll_up_script = self.on_scroll_up.map(Script::new_polling);
let scroll_down_script = self.on_scroll_down.map(Script::new_polling); let scroll_down_script = self.on_scroll_down.map(Script::new_polling);
container.connect_scroll_event(move |_, event| { widget.connect_scroll_event(move |_, event| {
let script = match event.direction() { let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(), ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(), ScrollDirection::Down => scroll_down_script.as_ref(),
@@ -101,7 +100,7 @@ impl CommonConfig {
macro_rules! install_oneshot { macro_rules! install_oneshot {
($option:expr, $method:ident) => { ($option:expr, $method:ident) => {
$option.map(Script::new_polling).map(|script| { $option.map(Script::new_polling).map(|script| {
container.$method(move |_, _| { widget.$method(move |_, _| {
script.run_as_oneshot(None); script.run_as_oneshot(None);
Inhibit(false) Inhibit(false)
}); });
@@ -113,7 +112,7 @@ impl CommonConfig {
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event); install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
if let Some(tooltip) = self.tooltip { if let Some(tooltip) = self.tooltip {
let container = container.clone(); let container = widget.clone();
DynamicString::new(&tooltip, move |string| { DynamicString::new(&tooltip, move |string| {
container.set_tooltip_text(Some(&string)); container.set_tooltip_text(Some(&string));
Continue(true) Continue(true)
@@ -121,14 +120,14 @@ impl CommonConfig {
} }
} }
fn install_show_if(&mut self, container: &EventBox, revealer: &Revealer) { fn install_show_if<W: IsA<Widget>>(&mut self, widget: &W, revealer: &Revealer) {
self.show_if.take().map_or_else( self.show_if.take().map_or_else(
|| { || {
container.show_all(); widget.set_visible(true)
}, },
|show_if| { |show_if| {
let script = Script::new_polling(show_if); let script = Script::new_polling(show_if);
let container = container.clone(); let widget = widget.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move { spawn(async move {
@@ -154,7 +153,7 @@ impl CommonConfig {
revealer.connect_child_revealed_notify(move |revealer| { revealer.connect_child_revealed_notify(move |revealer| {
if !revealer.reveals_child() { if !revealer.reveals_child() {
container.hide(); container.hide()
} }
}); });
}, },

View File

@@ -29,12 +29,13 @@ pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
for dir in dirs { for dir in dirs {
let mut walker = WalkDir::new(dir).max_depth(5).into_iter(); let mut walker = WalkDir::new(dir).max_depth(5).into_iter();
let entry = walker.find(|entry| { let entry = walker.find(|entry| match entry {
entry.as_ref().map_or(false, |entry| { Ok(entry) => {
let file_name = entry.file_name().to_string_lossy().to_lowercase(); let file_name = entry.file_name().to_string_lossy().to_lowercase();
let test_name = format!("{}.desktop", app_id.to_lowercase()); let test_name = format!("{}.desktop", app_id.to_lowercase());
file_name == test_name file_name == test_name
}) }
_ => false,
}); });
if let Some(Ok(entry)) = entry { if let Some(Ok(entry)) = entry {

View File

@@ -55,7 +55,7 @@ impl DynamicString {
if let OutputStream::Stdout(out) = out { if let OutputStream::Stdout(out) = out {
let mut label_parts = lock!(label_parts); let mut label_parts = lock!(label_parts);
let _: String = std::mem::replace(&mut label_parts[i], out); let _ = std::mem::replace(&mut label_parts[i], out);
let string = label_parts.join(""); let string = label_parts.join("");
send!(tx, string); send!(tx, string);

View File

@@ -5,11 +5,9 @@ pub enum ExitCode {
Config = 3, Config = 3,
} }
pub const ERR_OUTPUTS: &str = "GTK and Wayland are reporting a different set of outputs - this is a severe bug and should never happen"; pub const ERR_OUTPUTS: &str = "GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen";
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex"; pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
pub const ERR_READ_LOCK: &str = "Failed to get read lock"; pub const ERR_READ_LOCK: &str = "Failed to get read lock";
pub const ERR_WRITE_LOCK: &str = "Failed to get write lock"; pub const ERR_WRITE_LOCK: &str = "Failed to get write lock";
pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel"; pub const ERR_CHANNEL_SEND: &str = "Failed to send message to channel";
pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel"; pub const ERR_CHANNEL_RECV: &str = "Failed to receive message from channel";
pub const ERR_WAYLAND_DATA: &str = "Failed to get data for Wayland object";

View File

@@ -1,8 +0,0 @@
use glib::IsA;
use gtk::prelude::*;
use gtk::Widget;
/// Adds a new CSS class to a widget.
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
widget.style_context().add_class(class);
}

View File

@@ -1,5 +1,4 @@
use super::ImageProvider; use super::ImageProvider;
use crate::gtk_helpers::add_class;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation}; use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error; use tracing::error;
@@ -10,7 +9,7 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
add_class(&image, "image"); image.set_widget_name("image");
match ImageProvider::parse(input, icon_theme, size) match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone())) .and_then(|provider| provider.load_into_image(image.clone()))
@@ -37,9 +36,9 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) { if ImageProvider::is_definitely_image_input(input) {
let image = Image::new(); let image = Image::new();
add_class(&image, "image"); image.set_widget_name("image");
container.add(&image); container.append(&image);
if let Err(err) = ImageProvider::parse(input, icon_theme, size) if let Err(err) = ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image)) .and_then(|provider| provider.load_into_image(image))
@@ -48,9 +47,9 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
} }
} else { } else {
let label = Label::new(Some(input)); let label = Label::new(Some(input));
add_class(&label, "label"); label.set_widget_name("label");
container.add(&label); container.append(&label);
} }
container container

View File

@@ -58,7 +58,7 @@ fn install_tracing() -> Result<WorkerGuard> {
const DEFAULT_LOG: &str = "info"; const DEFAULT_LOG: &str = "info";
const DEFAULT_FILE_LOG: &str = "warn"; const DEFAULT_FILE_LOG: &str = "warn";
let fmt_layer = fmt::layer().with_target(true).with_line_number(true); let fmt_layer = fmt::layer().with_target(true);
let filter_layer = let filter_layer =
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?; EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;

View File

@@ -53,10 +53,9 @@ macro_rules! try_send {
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! lock { macro_rules! lock {
($mutex:expr) => {{ ($mutex:expr) => {
tracing::trace!("Locking {}", std::stringify!($mutex));
$mutex.lock().expect($crate::error::ERR_MUTEX_LOCK) $mutex.lock().expect($crate::error::ERR_MUTEX_LOCK)
}}; };
} }
/// Gets a read lock on a `RwLock`. /// Gets a read lock on a `RwLock`.

View File

@@ -7,7 +7,6 @@ mod config;
mod desktop_file; mod desktop_file;
mod dynamic_string; mod dynamic_string;
mod error; mod error;
mod gtk_helpers;
mod image; mod image;
mod logging; mod logging;
mod macros; mod macros;
@@ -22,15 +21,13 @@ use crate::style::load_css;
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;
use gtk::gdk::Display; use gtk::gdk::{Display, Monitor};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::Application; use gtk::Application;
use std::cell::Cell;
use std::env; use std::env;
use std::future::Future; 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 tokio::runtime::Handle; use tokio::runtime::Handle;
use tokio::task::block_in_place; use tokio::task::block_in_place;
@@ -43,7 +40,7 @@ const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<()> {
let _guard = logging::install_logging(); let _guard = logging::install_logging();
info!("Ironbar version {}", VERSION); info!("Ironbar version {}", VERSION);
@@ -53,16 +50,7 @@ async fn main() {
let app = Application::builder().application_id(GTK_APP_ID).build(); let app = Application::builder().application_id(GTK_APP_ID).build();
let running = Rc::new(Cell::new(false));
app.connect_activate(move |app| { app.connect_activate(move |app| {
if running.get() {
info!("Ironbar already running, returning");
return;
}
running.set(true);
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");
@@ -117,8 +105,7 @@ async fn main() {
// 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"); Ok(())
exit(0);
} }
/// Creates each of the bars across each of the (configured) outputs. /// Creates each of the bars across each of the (configured) outputs.
@@ -128,12 +115,15 @@ fn create_bars(
wl: &WaylandClient, wl: &WaylandClient,
config: &Config, config: &Config,
) -> Result<()> { ) -> Result<()> {
let outputs = wl.get_outputs(); let outputs = wl.outputs.as_slice();
debug!("Received {} outputs from Wayland", outputs.len()); debug!("Received {} outputs from Wayland", outputs.len());
debug!("Outputs: {:?}", outputs); debug!("Outputs: {:?}", outputs);
let num_monitors = display.n_monitors(); for monitor in display.monitors().iter::<Monitor>() {
let monitor = monitor.unwrap();
}
for i in 0..num_monitors { for i in 0..num_monitors {
let monitor = display let monitor = display
@@ -142,8 +132,7 @@ fn create_bars(
let output = outputs let output = outputs
.get(i as usize) .get(i as usize)
.ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?; .ok_or_else(|| Report::msg(error::ERR_OUTPUTS))?;
let monitor_name = &output.name;
let Some(monitor_name) = &output.name else { continue };
config.monitors.as_ref().map_or_else( config.monitors.as_ref().map_or_else(
|| { || {

View File

@@ -8,10 +8,11 @@ use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf; use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream}; use gtk::gio::{Cancellable, MemoryInputStream};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget}; use gtk::{Button, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use glib::signal::Inhibit;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error}; use tracing::{debug, error};
@@ -80,7 +81,7 @@ impl Module<Button> for ClipboardModule {
spawn(async move { spawn(async move {
let mut rx = { let mut rx = {
let client = clipboard::get_client(); let client = clipboard::get_client();
client.subscribe(max_items) client.subscribe(max_items).await
}; };
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
@@ -154,13 +155,17 @@ impl Module<Button> for ClipboardModule {
where where
Self: Sized, Self: Sized,
{ {
let container = gtk::Box::new(Orientation::Vertical, 10); let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
.spacing(10)
.name("popup-clipboard")
.build();
let entries = gtk::Box::new(Orientation::Vertical, 5); let entries = gtk::Box::new(Orientation::Vertical, 5);
container.add(&entries); container.append(&entries);
let hidden_option = RadioButton::new(); let hidden_option = RadioButton::new();
entries.add(&hidden_option); entries.append(&hidden_option);
let mut items = HashMap::new(); let mut items = HashMap::new();
@@ -179,7 +184,7 @@ impl Module<Button> for ClipboardModule {
let button = RadioButton::from_widget(&hidden_option); let button = RadioButton::from_widget(&hidden_option);
let label = Label::new(Some(value)); let label = Label::new(Some(value));
button.add(&label); button.append(&label);
if let Some(truncate) = self.truncate { if let Some(truncate) = self.truncate {
truncate.truncate_label(&label); truncate.truncate_label(&label);
@@ -214,7 +219,7 @@ impl Module<Button> for ClipboardModule {
button.set_active(true); // if just added, should be on clipboard button.set_active(true); // if just added, should be on clipboard
let button_wrapper = EventBox::new(); let button_wrapper = EventBox::new();
button_wrapper.add(&button); button_wrapper.append(&button);
button_wrapper.set_widget_name(&format!("copy-{id}")); button_wrapper.set_widget_name(&format!("copy-{id}"));
button_wrapper.set_above_child(true); button_wrapper.set_above_child(true);
@@ -257,12 +262,11 @@ impl Module<Button> for ClipboardModule {
}); });
} }
row.add(&button_wrapper); row.append(&button_wrapper);
row.pack_end(&remove_button, false, false, 0); row.pack_end(&remove_button, false, false, 0);
entries.add(&row); entries.append(&row);
entries.reorder_child(&row, 0); entries.reorder_child(&row, 0);
row.show_all();
items.insert(id, (row, button)); items.insert(id, (row, button));
} }
@@ -296,7 +300,6 @@ impl Module<Button> for ClipboardModule {
}); });
} }
container.show_all();
hidden_option.hide(); hidden_option.hide();
Some(container) Some(container)

View File

@@ -1,5 +1,4 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{send_async, try_send}; use crate::{send_async, try_send};
@@ -64,7 +63,7 @@ impl Module<Button> for ClockModule {
let button = Button::new(); let button = Button::new();
let label = Label::new(None); let label = Label::new(None);
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
button.add(&label); button.append(&label);
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| { button.connect_clicked(move |button| {
@@ -97,17 +96,21 @@ impl Module<Button> for ClockModule {
rx: glib::Receiver<Self::SendMessage>, rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo, _info: &ModuleInfo,
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let container = gtk::Box::new(Orientation::Vertical, 0); let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
.name("popup-clock")
.build();
let clock = Label::builder().halign(Align::Center).build(); let clock = Label::builder()
add_class(&clock, "calendar-clock"); .name("calendar-clock")
.halign(Align::Center)
.build();
let format = "%H:%M:%S"; let format = "%H:%M:%S";
container.add(&clock); container.append(&clock);
let calendar = Calendar::new(); let calendar = Calendar::builder().name("calendar").build();
add_class(&calendar, "calendar"); container.append(&calendar);
container.add(&calendar);
{ {
rx.attach(None, move |date| { rx.attach(None, move |date| {
@@ -117,8 +120,6 @@ impl Module<Button> for ClockModule {
}); });
} }
container.show_all();
Some(container) Some(container)
} }
} }

View File

@@ -23,7 +23,7 @@ impl CustomWidget for ButtonWidget {
if let Some(text) = self.label { if let Some(text) = self.label {
let label = Label::new(None); let label = Label::new(None);
label.set_use_markup(true); label.set_use_markup(true);
button.add(&label); button.append(&label);
DynamicString::new(&text, move |string| { DynamicString::new(&text, move |string| {
label.set_markup(&string); label.set_markup(&string);

View File

@@ -28,6 +28,8 @@ use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct CustomModule { pub struct CustomModule {
/// Container class name
class: Option<String>,
/// Widgets to add to the bar container /// Widgets to add to the bar container
bar: Vec<WidgetConfig>, bar: Vec<WidgetConfig>,
/// Widgets to add to the popup container /// Widgets to add to the popup container
@@ -195,6 +197,10 @@ impl Module<gtk::Box> for CustomModule {
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build(); let container = gtk::Box::builder().orientation(orientation).build();
if let Some(ref class) = self.class {
container.style_context().add_class(class);
}
let custom_context = CustomWidgetContext { let custom_context = CustomWidgetContext {
tx: &context.controller_tx, tx: &context.controller_tx,
bar_orientation: orientation, bar_orientation: orientation,
@@ -224,7 +230,13 @@ impl Module<gtk::Box> for CustomModule {
where where
Self: Sized, Self: Sized,
{ {
let container = gtk::Box::new(Orientation::Horizontal, 0); let container = gtk::Box::builder().name("popup-custom").build();
if let Some(class) = self.class {
container
.style_context()
.add_class(format!("popup-{class}").as_str());
}
if let Some(popup) = self.popup { if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext { let custom_context = CustomWidgetContext {
@@ -240,8 +252,6 @@ impl Module<gtk::Box> for CustomModule {
} }
} }
container.show_all();
Some(container) Some(container)
} }
} }

View File

@@ -8,6 +8,7 @@ use gtk::Scale;
use serde::Deserialize; use serde::Deserialize;
use std::cell::Cell; use std::cell::Cell;
use std::ops::Neg; use std::ops::Neg;
use glib::signal::Inhibit;
use tokio::spawn; use tokio::spawn;
use tracing::error; use tracing::error;

View File

@@ -1,9 +1,8 @@
use crate::clients::wayland::{self, ToplevelEvent}; use crate::clients::wayland::{self, ToplevelChange};
use crate::config::{CommonConfig, TruncateMode}; use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send}; use crate::{await_sync, read_lock, send_async};
use color_eyre::Result; use color_eyre::Result;
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
@@ -11,7 +10,7 @@ use gtk::Label;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error}; use tracing::error;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule { pub struct FocusedModule {
@@ -50,38 +49,40 @@ impl Module<gtk::Box> for FocusedModule {
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>, tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
_rx: Receiver<Self::ReceiveMessage>, _rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> { ) -> Result<()> {
let focused = await_sync(async {
let wl = wayland::get_client().await;
let toplevels = read_lock!(wl.toplevels);
toplevels
.iter()
.find(|(_, (top, _))| top.active)
.map(|(_, (top, _))| top.clone())
});
if let Some(top) = focused {
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
}
spawn(async move { spawn(async move {
let (mut wlrx, handles) = { let mut wlrx = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_toplevels() wl.subscribe_toplevels()
}; };
let focused = handles.values().find_map(|handle| { while let Ok(event) = wlrx.recv().await {
handle let update = match event.change {
.info() ToplevelChange::Focus(focus) => focus,
.and_then(|info| if info.focused { Some(info) } else { None }) ToplevelChange::Title(_) => event.toplevel.active,
}); _ => false,
if let Some(focused) = focused {
try_send!(
tx,
ModuleUpdateEvent::Update((focused.title.clone(), focused.app_id))
);
}; };
while let Ok(event) = wlrx.recv().await { if update {
if let ToplevelEvent::Update(handle) = event {
let info = handle.info().unwrap_or_default();
if info.focused {
debug!("Changing focus");
send_async!( send_async!(
tx, tx,
ModuleUpdateEvent::Update((info.title.clone(), info.app_id.clone())) ModuleUpdateEvent::Update((event.toplevel.title, event.toplevel.app_id))
); );
} }
} }
}
}); });
Ok(()) Ok(())
@@ -96,18 +97,15 @@ impl Module<gtk::Box> for FocusedModule {
let container = gtk::Box::new(info.bar_position.get_orientation(), 5); let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::new(); let icon = gtk::Image::builder().name("icon").build();
add_class(&icon, "icon"); let label = Label::builder().name("label").build();
let label = Label::new(None);
add_class(&label, "label");
if let Some(truncate) = self.truncate { if let Some(truncate) = self.truncate {
truncate.truncate_label(&label); truncate.truncate_label(&label);
} }
container.add(&icon); container.append(&icon);
container.add(&label); container.append(&label);
{ {
let icon_theme = icon_theme.clone(); let icon_theme = icon_theme.clone();

View File

@@ -1,19 +1,18 @@
use super::open_state::OpenState; use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle; use crate::clients::wayland::ToplevelInfo;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent; use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup; use crate::popup::Popup;
use crate::{read_lock, try_send}; use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation}; use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::RwLock; use std::sync::RwLock;
use glib::signal::Inhibit;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::error; use tracing::error;
use wayland_client::protocol::wl_seat::WlSeat;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Item { pub struct Item {
@@ -36,31 +35,25 @@ impl Item {
} }
/// Merges the provided node into this launcher item /// Merges the provided node into this launcher item
pub fn merge_toplevel(&mut self, handle: ToplevelHandle) -> Result<Window> { pub fn merge_toplevel(&mut self, node: ToplevelInfo) -> Window {
let info = handle let id = node.id;
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let id = info.id;
if self.windows.is_empty() { if self.windows.is_empty() {
self.name = info.title; self.name = node.title.clone();
} }
let window = Window::try_from(handle)?; let window: Window = node.into();
self.windows.insert(id, window.clone()); self.windows.insert(id, window.clone());
self.recalculate_open_state(); self.recalculate_open_state();
Ok(window) window
} }
pub fn unmerge_toplevel(&mut self, handle: &ToplevelHandle) { pub fn unmerge_toplevel(&mut self, node: &ToplevelInfo) {
if let Some(info) = handle.info() { self.windows.remove(&node.id);
self.windows.remove(&info.id);
self.recalculate_open_state(); self.recalculate_open_state();
} }
}
pub fn set_window_name(&mut self, window_id: usize, name: String) { pub fn set_window_name(&mut self, window_id: usize, name: String) {
if let Some(window) = self.windows.get_mut(&window_id) { if let Some(window) = self.windows.get_mut(&window_id) {
@@ -95,29 +88,22 @@ impl Item {
} }
} }
impl TryFrom<ToplevelHandle> for Item { impl From<ToplevelInfo> for Item {
type Error = Report; fn from(toplevel: ToplevelInfo) -> Self {
let open_state = OpenState::from_toplevel(&toplevel);
fn try_from(handle: ToplevelHandle) -> std::result::Result<Self, Self::Error> { let name = toplevel.title.clone();
let info = handle let app_id = toplevel.app_id.clone();
.info()
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let name = info.title.clone();
let app_id = info.app_id.clone();
let open_state = OpenState::from(&info);
let mut windows = IndexMap::new(); let mut windows = IndexMap::new();
let window = Window::try_from(handle)?; windows.insert(toplevel.id, toplevel.into());
windows.insert(info.id, window);
Ok(Self { Self {
app_id, app_id,
favorite: false, favorite: false,
open_state, open_state,
windows, windows,
name, name,
}) }
} }
} }
@@ -126,30 +112,17 @@ pub struct Window {
pub id: usize, pub id: usize,
pub name: String, pub name: String,
pub open_state: OpenState, pub open_state: OpenState,
handle: ToplevelHandle,
} }
impl TryFrom<ToplevelHandle> for Window { impl From<ToplevelInfo> for Window {
type Error = Report; fn from(node: ToplevelInfo) -> Self {
let open_state = OpenState::from_toplevel(&node);
fn try_from(handle: ToplevelHandle) -> Result<Self, Self::Error> { Self {
let info = handle id: node.id,
.info() name: node.title,
.ok_or_else(|| Report::msg("Toplevel is missing associated info"))?;
let open_state = OpenState::from(&info);
Ok(Self {
id: info.id,
name: info.title,
open_state, open_state,
handle,
})
} }
}
impl Window {
pub fn focus(&self, seat: &WlSeat) {
self.handle.focus(seat);
} }
} }
@@ -262,8 +235,6 @@ impl ItemButton {
}); });
} }
button.show_all();
Self { Self {
button, button,
persistent: item.favorite, persistent: item.favorite,

View File

@@ -3,12 +3,12 @@ mod open_state;
use self::item::{Item, ItemButton, Window}; use self::item::{Item, ItemButton, Window};
use self::open_state::OpenState; use self::open_state::OpenState;
use crate::clients::wayland::{self, ToplevelEvent}; use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file; use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions; use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, send_async, try_send, write_lock}; use crate::{lock, read_lock, try_send, write_lock};
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use glib::Continue; use glib::Continue;
use gtk::prelude::*; use gtk::prelude::*;
@@ -18,6 +18,7 @@ use serde::Deserialize;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -110,64 +111,70 @@ impl Module<gtk::Box> for LauncherModule {
let items = Arc::new(Mutex::new(items)); let items = Arc::new(Mutex::new(items));
let items2 = Arc::clone(&items); {
let tx2 = tx.clone(); let items = Arc::clone(&items);
let tx = tx.clone();
spawn(async move { spawn(async move {
let items = items2;
let tx = tx2;
let (mut wlrx, handles) = {
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
wl.subscribe_toplevels() let open_windows = read_lock!(wl.toplevels);
};
for handle in handles.values() {
let Some(info) = handle.info() else { continue };
let open_windows = open_windows.clone();
for (_, (window, _)) in open_windows {
let mut items = lock!(items); let mut items = lock!(items);
let item = items.get_mut(&info.app_id); let item = items.get_mut(&window.app_id);
match item { match item {
Some(item) => { Some(item) => {
item.merge_toplevel(handle.clone())?; item.merge_toplevel(window);
} }
None => { None => {
items.insert(info.app_id.clone(), Item::try_from(handle.clone())?); items.insert(window.app_id.clone(), window.into());
} }
} }
} }
{
let items = lock!(items); let items = lock!(items);
let items = items.iter(); let items = items.iter();
for (_, item) in items { for (_, item) in items {
try_send!( tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
tx, item.clone(),
ModuleUpdateEvent::Update(LauncherUpdate::AddItem(item.clone())) )))?;
);
} }
Ok::<(), Report>(())
});
} }
let items2 = Arc::clone(&items);
spawn(async move {
let items = items2;
let mut wlrx = {
let wl = wayland::get_client().await;
wl.subscribe_toplevels()
};
let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update)); let send_update = |update: LauncherUpdate| tx.send(ModuleUpdateEvent::Update(update));
while let Ok(event) = wlrx.recv().await { while let Ok(event) = wlrx.recv().await {
trace!("event: {:?}", event); trace!("event: {:?}", event);
match event { let window = event.toplevel;
ToplevelEvent::New(handle) => { let app_id = window.app_id.clone();
let Some(info) = handle.info() else { continue };
match event.change {
ToplevelChange::New => {
let new_item = { let new_item = {
let mut items = lock!(items); let mut items = lock!(items);
let item = items.get_mut(&info.app_id); let item = items.get_mut(&app_id);
match item { match item {
None => { None => {
let item: Item = handle.try_into()?; let item: Item = window.into();
items.insert(info.app_id.clone(), item.clone()); items.insert(app_id.clone(), item.clone());
ItemOrWindow::Item(item) ItemOrWindow::Item(item)
} }
Some(item) => { Some(item) => {
let window = item.merge_toplevel(handle)?; let window = item.merge_toplevel(window);
ItemOrWindow::Window(window) ItemOrWindow::Window(window)
} }
} }
@@ -178,40 +185,20 @@ impl Module<gtk::Box> for LauncherModule {
send_update(LauncherUpdate::AddItem(item)).await send_update(LauncherUpdate::AddItem(item)).await
} }
ItemOrWindow::Window(window) => { ItemOrWindow::Window(window) => {
send_update(LauncherUpdate::AddWindow(info.app_id.clone(), window)) send_update(LauncherUpdate::AddWindow(app_id, window)).await
.await
} }
}?; }?;
} }
ToplevelEvent::Update(handle) => { ToplevelChange::Close => {
let Some(info) = handle.info() else { continue };
if let Some(item) = lock!(items).get_mut(&info.app_id) {
item.set_window_focused(info.id, info.focused);
item.set_window_name(info.id, info.title.clone());
}
send_update(LauncherUpdate::Focus(info.app_id.clone(), info.focused))
.await?;
send_update(LauncherUpdate::Title(
info.app_id.clone(),
info.id,
info.title.clone(),
))
.await?;
}
ToplevelEvent::Remove(handle) => {
let Some(info) = handle.info() else { continue };
let remove_item = { let remove_item = {
let mut items = lock!(items); let mut items = lock!(items);
let item = items.get_mut(&info.app_id); let item = items.get_mut(&app_id);
match item { match item {
Some(item) => { Some(item) => {
item.unmerge_toplevel(&handle); item.unmerge_toplevel(&window);
if item.windows.is_empty() { if item.windows.is_empty() {
items.remove(&info.app_id); items.remove(&app_id);
Some(ItemOrWindowId::Item) Some(ItemOrWindowId::Item)
} else { } else {
Some(ItemOrWindowId::Window) Some(ItemOrWindowId::Window)
@@ -223,28 +210,56 @@ impl Module<gtk::Box> for LauncherModule {
match remove_item { match remove_item {
Some(ItemOrWindowId::Item) => { Some(ItemOrWindowId::Item) => {
send_update(LauncherUpdate::RemoveItem(info.app_id.clone())) send_update(LauncherUpdate::RemoveItem(app_id)).await?;
.await?;
} }
Some(ItemOrWindowId::Window) => { Some(ItemOrWindowId::Window) => {
send_update(LauncherUpdate::RemoveWindow( send_update(LauncherUpdate::RemoveWindow(app_id, window.id))
info.app_id.clone(),
info.id,
))
.await?; .await?;
} }
None => {} None => {}
}; };
} }
ToplevelChange::Focus(focused) => {
let mut update_title = false;
if focused {
if let Some(item) = lock!(items).get_mut(&app_id) {
item.set_window_focused(window.id, true);
// might be switching focus between windows of same app
if item.windows.len() > 1 {
item.set_window_name(window.id, window.title.clone());
update_title = true;
}
} }
} }
Ok::<(), Report>(()) send_update(LauncherUpdate::Focus(app_id.clone(), focused)).await?;
if update_title {
send_update(LauncherUpdate::Title(app_id, window.id, window.title))
.await?;
}
}
ToplevelChange::Title(title) => {
if let Some(item) = lock!(items).get_mut(&app_id) {
item.set_window_name(window.id, title.clone());
}
send_update(LauncherUpdate::Title(app_id, window.id, title)).await?;
}
ToplevelChange::Fullscreen(_) => {}
}
}
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<LauncherUpdate>>>(())
}); });
// listen to ui events // listen to ui events
spawn(async move { spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
trace!("{:?}", event);
if let ItemEvent::OpenItem(app_id) = event { if let ItemEvent::OpenItem(app_id) = event {
find_desktop_file(&app_id).map_or_else( find_desktop_file(&app_id).map_or_else(
|| error!("Could not find desktop file for {}", app_id), || error!("Could not find desktop file for {}", app_id),
@@ -268,8 +283,6 @@ impl Module<gtk::Box> for LauncherModule {
}, },
); );
} else { } else {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
let wl = wayland::get_client().await; let wl = wayland::get_client().await;
let items = lock!(items); let items = lock!(items);
@@ -282,12 +295,11 @@ impl Module<gtk::Box> for LauncherModule {
}; };
if let Some(id) = id { if let Some(id) = id {
if let Some(window) = let toplevels = read_lock!(wl.toplevels);
items.iter().find_map(|(_, item)| item.windows.get(&id)) let seat = wl.seats.first().expect("Failed to get Wayland seat");
{ if let Some((_top, handle)) = toplevels.get(&id) {
let seat = wl.get_seats().pop().expect("Failed to get Wayland seat"); handle.activate(seat);
window.focus(&seat); };
}
} }
// roundtrip to immediately send activate event // roundtrip to immediately send activate event
@@ -342,7 +354,7 @@ impl Module<gtk::Box> for LauncherModule {
&controller_tx, &controller_tx,
); );
container.add(&button.button); container.append(&button.button);
buttons.insert(item.app_id, button); buttons.insert(item.app_id, button);
} }
} }
@@ -413,12 +425,15 @@ impl Module<gtk::Box> for LauncherModule {
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250; const MAX_WIDTH: i32 = 250;
let container = gtk::Box::new(Orientation::Vertical, 0); let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
.name("popup-launcher")
.build();
// we need some content to force the container to have a size // we need some content to force the container to have a size
let placeholder = Button::with_label("PLACEHOLDER"); let placeholder = Button::with_label("PLACEHOLDER");
placeholder.set_width_request(MAX_WIDTH); placeholder.set_width_request(MAX_WIDTH);
container.add(&placeholder); container.append(&placeholder);
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new(); let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
@@ -441,8 +456,12 @@ impl Module<gtk::Box> for LauncherModule {
{ {
let tx = controller_tx.clone(); let tx = controller_tx.clone();
button.connect_clicked(move |_| { button.connect_clicked(move |button| {
try_send!(tx, ItemEvent::FocusWindow(win.id)); try_send!(tx, ItemEvent::FocusWindow(win.id));
if let Some(win) = button.window() {
win.hide();
}
}); });
} }
@@ -506,10 +525,9 @@ impl Module<gtk::Box> for LauncherModule {
if let Some(buttons) = buttons.get(&app_id) { if let Some(buttons) = buttons.get(&app_id) {
for (_, button) in buttons { for (_, button) in buttons {
button.style_context().add_class("popup-item"); button.style_context().add_class("popup-item");
container.add(button); container.append(button);
} }
container.show_all();
container.set_width_request(MAX_WIDTH); container.set_width_request(MAX_WIDTH);
} }
} }
@@ -529,9 +547,6 @@ impl Module<gtk::Box> for LauncherModule {
/// This is a hacky number derived from /// This is a hacky number derived from
/// "what fits inside the 250px popup" /// "what fits inside the 250px popup"
/// and probably won't hold up with wide fonts. /// and probably won't hold up with wide fonts.
///
/// TODO: Migrate this to truncate system
///
fn clamp(str: &str) -> String { fn clamp(str: &str) -> String {
const MAX_CHARS: usize = 24; const MAX_CHARS: usize = 24;

View File

@@ -7,15 +7,14 @@ pub enum OpenState {
Open { focused: bool }, Open { focused: bool },
} }
impl From<&ToplevelInfo> for OpenState {
fn from(info: &ToplevelInfo) -> Self {
Self::Open {
focused: info.focused,
}
}
}
impl OpenState { impl OpenState {
/// Creates from `SwayNode`
pub const fn from_toplevel(toplevel: &ToplevelInfo) -> Self {
Self::Open {
focused: toplevel.active,
}
}
/// Creates open with focused /// Creates open with focused
pub const fn focused(focused: bool) -> Self { pub const fn focused(focused: bool) -> Self {
Self::Open { focused } Self::Open { focused }

View File

@@ -120,7 +120,7 @@ pub fn create_module<TModule, TWidget, TSend, TRec>(
id: usize, id: usize,
info: &ModuleInfo, info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>, popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleWidget<TWidget>> ) -> Result<TWidget>
where where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>, TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>, TWidget: IsA<Widget>,
@@ -145,21 +145,17 @@ where
let name = TModule::name(); let name = TModule::name();
let module_parts = module.into_widget(context, info)?; let module_parts = module.into_widget(context, info)?;
module_parts.widget.style_context().add_class(name); module_parts.widget.set_widget_name(name);
let mut has_popup = false; let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() { if let Some(popup_content) = module_parts.popup {
popup_content
.style_context()
.add_class(&format!("popup-{name}"));
register_popup_content(popup, id, popup_content); register_popup_content(popup, id, popup_content);
has_popup = true; has_popup = true;
} }
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup); setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
Ok(module_parts) Ok(module_parts.widget)
} }
/// Registers the popup content with the popup. /// Registers the popup content with the popup.
@@ -238,32 +234,6 @@ fn setup_receiver<TSend>(
}); });
} }
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleWidget<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup {
popup.set_widget_name(&format!("popup-{name}"));
}
}
if let Some(ref class) = common.class {
// gtk counts classes with spaces as the same class
for part in class.split(' ') {
widget_parts.widget.style_context().add_class(part);
}
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup.style_context().add_class(&format!("popup-{part}"));
}
}
}
}
/// Takes a widget and adds it into a new `gtk::EventBox`. /// Takes a widget and adds it into a new `gtk::EventBox`.
/// The event box container is returned. /// The event box container is returned.
pub fn wrap_widget<W: IsA<Widget>>( pub fn wrap_widget<W: IsA<Widget>>(
@@ -271,14 +241,14 @@ pub fn wrap_widget<W: IsA<Widget>>(
common: CommonConfig, common: CommonConfig,
orientation: Orientation, orientation: Orientation,
) -> EventBox { ) -> EventBox {
let transition_type = common let revealer = Revealer::builder()
.transition_type(
common
.transition_type .transition_type
.as_ref() .as_ref()
.unwrap_or(&TransitionType::SlideStart) .unwrap_or(&TransitionType::SlideStart)
.to_revealer_transition_type(orientation); .to_revealer_transition_type(orientation),
)
let revealer = Revealer::builder()
.transition_type(transition_type)
.transition_duration(common.transition_duration.unwrap_or(250)) .transition_duration(common.transition_duration.unwrap_or(250))
.build(); .build();
@@ -287,9 +257,9 @@ pub fn wrap_widget<W: IsA<Widget>>(
let container = EventBox::new(); let container = EventBox::new();
container.add_events(EventMask::SCROLL_MASK); container.add_events(EventMask::SCROLL_MASK);
container.add(&revealer); container.append(&revealer);
common.install_events(&container, &revealer); common.install(&container, &revealer);
container container
} }

View File

@@ -1,7 +1,6 @@
mod config; mod config;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track}; use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::gtk_helpers::add_class;
use crate::image::{new_icon_button, new_icon_label, ImageProvider}; use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup; use crate::popup::Popup;
@@ -14,6 +13,7 @@ use regex::Regex;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use glib::signal::Inhibit;
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error; use tracing::error;
@@ -136,7 +136,7 @@ impl Module<Button> for MusicModule {
PlayerCommand::Play => client.play(), PlayerCommand::Play => client.play(),
PlayerCommand::Pause => client.pause(), PlayerCommand::Pause => client.pause(),
PlayerCommand::Next => client.next(), PlayerCommand::Next => client.next(),
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), PlayerCommand::Volume(vol) => client.set_volume_percent(vol), // .unwrap_or_else(|_| error!("Failed to update player volume")),
}; };
if let Err(err) = res { if let Err(err) = res {
@@ -156,9 +156,7 @@ impl Module<Button> for MusicModule {
) -> Result<ModuleWidget<Button>> { ) -> Result<ModuleWidget<Button>> {
let button = Button::new(); let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5); let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&button_contents, "contents"); button.append(&button_contents);
button.add(&button_contents);
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size); let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size); let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
@@ -170,9 +168,9 @@ impl Module<Button> for MusicModule {
truncate.truncate_label(&label); truncate.truncate_label(&label);
} }
button_contents.add(&icon_pause); button_contents.append(&icon_pause);
button_contents.add(&icon_play); button_contents.append(&icon_play);
button_contents.add(&label); button_contents.append(&label);
let orientation = info.bar_position.get_orientation(); let orientation = info.bar_position.get_orientation();
@@ -241,13 +239,17 @@ impl Module<Button> for MusicModule {
) -> Option<gtk::Box> { ) -> Option<gtk::Box> {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(Orientation::Horizontal, 10); let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-music")
.build();
let album_image = gtk::Image::builder() let album_image = gtk::Image::builder()
.width_request(128) .width_request(128)
.height_request(128) .height_request(128)
.name("album-art")
.build(); .build();
add_class(&album_image, "album-art");
let icons = self.icons; let icons = self.icons;
@@ -256,52 +258,54 @@ impl Module<Button> for MusicModule {
let album_label = IconLabel::new(&icons.album, None, icon_theme); let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme); let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
add_class(&title_label.container, "title"); title_label.container.set_widget_name("title");
add_class(&album_label.container, "album"); album_label.container.set_widget_name("album");
add_class(&artist_label.container, "artist"); artist_label.container.set_widget_name("artist");
info_box.add(&title_label.container); info_box.append(&title_label.container);
info_box.add(&album_label.container); info_box.append(&album_label.container);
info_box.add(&artist_label.container); info_box.append(&artist_label.container);
let controls_box = gtk::Box::new(Orientation::Horizontal, 0); let controls_box = gtk::Box::builder().name("controls").build();
add_class(&controls_box, "controls");
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size); let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
add_class(&btn_prev, "btn-prev"); btn_prev.set_widget_name("btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size); let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
add_class(&btn_play, "btn-play"); btn_play.set_widget_name("btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size); let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
add_class(&btn_pause, "btn-pause"); btn_pause.set_widget_name("btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size); let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
add_class(&btn_next, "btn-next"); btn_next.set_widget_name("btn-next");
controls_box.add(&btn_prev); controls_box.append(&btn_prev);
controls_box.add(&btn_play); controls_box.append(&btn_play);
controls_box.add(&btn_pause); controls_box.append(&btn_pause);
controls_box.add(&btn_next); controls_box.append(&btn_next);
info_box.add(&controls_box); info_box.append(&controls_box);
let volume_box = gtk::Box::new(Orientation::Vertical, 5); let volume_box = gtk::Box::builder()
add_class(&volume_box, "volume"); .orientation(Orientation::Vertical)
.spacing(5)
.name("volume")
.build();
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0); let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true); volume_slider.set_inverted(true);
add_class(&volume_slider, "slider"); volume_slider.set_widget_name("slider");
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size); let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
add_class(&volume_icon, "icon"); volume_icon.style_context().add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0); volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0); volume_box.pack_end(&volume_icon, false, false, 0);
container.add(&album_image); container.append(&album_image);
container.add(&info_box); container.append(&info_box);
container.add(&volume_box); container.append(&volume_box);
let tx_prev = tx.clone(); let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| { btn_prev.connect_clicked(move |_| {
@@ -329,8 +333,6 @@ impl Module<Button> for MusicModule {
Inhibit(false) Inhibit(false)
}); });
container.show_all();
{ {
let icon_theme = icon_theme.clone(); let icon_theme = icon_theme.clone();
let image_size = self.cover_image_size; let image_size = self.cover_image_size;
@@ -458,11 +460,11 @@ impl IconLabel {
let icon = new_icon_label(icon_input, icon_theme, 24); let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label); let label = Label::new(label);
add_class(&icon, "icon"); icon.style_context().add_class("icon");
add_class(&label, "label"); label.style_context().add_class("label");
container.add(&icon); container.append(&icon);
container.add(&label); container.append(&label);
Self { label, container } Self { label, container }
} }

View File

@@ -1,5 +1,4 @@
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::send_async; use crate::send_async;
use color_eyre::Result; use color_eyre::Result;
@@ -194,12 +193,13 @@ impl Module<gtk::Box> for SysInfoModule {
let mut labels = Vec::new(); let mut labels = Vec::new();
for format in &self.format { for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build(); let label = Label::builder()
.label(format)
add_class(&label, "item"); .use_markup(true)
.name("item")
.build();
label.set_angle(info.bar_position.get_angle()); label.set_angle(info.bar_position.get_angle());
container.append(&label);
container.add(&label);
labels.push(label); labels.push(label);
} }

View File

@@ -27,20 +27,15 @@ pub struct TrayModule {
/// Attempts to get a GTK `Image` component /// Attempts to get a GTK `Image` component
/// for the status notifier item's icon. /// for the status notifier item's icon.
fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> { fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> {
let theme = item item.icon_theme_path.as_ref().and_then(|path| {
.icon_theme_path
.as_ref()
.map(|path| {
let theme = IconTheme::new(); let theme = IconTheme::new();
theme.append_search_path(path); theme.append_search_path(path);
theme
})
.unwrap_or_default();
item.icon_name.as_ref().and_then(|icon_name| { item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
}) })
})
} }
/// Attempts to get an image from the item pixmap. /// Attempts to get an image from the item pixmap.
@@ -96,7 +91,7 @@ fn get_menu_items(
let menu = Menu::new(); let menu = Menu::new();
get_menu_items(&item_info.submenu, &tx.clone(), id, path) get_menu_items(&item_info.submenu, &tx.clone(), id, path)
.iter() .iter()
.for_each(|item| menu.add(item)); .for_each(|item| menu.append(item));
builder = builder.submenu(&menu); builder = builder.submenu(&menu);
} }
@@ -202,11 +197,11 @@ impl Module<MenuBar> for TrayModule {
}, },
|image| { |image| {
image.set_widget_name(address.as_str()); image.set_widget_name(address.as_str());
menu_item.add(&image); menu_item.append(&image);
}, },
); );
container.add(&menu_item); container.append(&menu_item);
menu_item.show_all(); menu_item.show_all();
menu_item menu_item
}); });
@@ -221,7 +216,7 @@ impl Module<MenuBar> for TrayModule {
&menu_path, &menu_path,
) )
.iter() .iter()
.for_each(|item| menu.add(item)); .for_each(|item| menu.append(item));
menu_item.set_submenu(Some(&menu)); menu_item.set_submenu(Some(&menu));
} }
} }

View File

@@ -1,6 +1,5 @@
use crate::clients::upower::get_display_proxy; use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider; use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup; use crate::popup::Popup;
@@ -145,20 +144,20 @@ impl Module<gtk::Box> for UpowerModule {
info: &ModuleInfo, info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> { ) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = info.icon_theme.clone(); let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new(); let icon = gtk::Image::builder().name("icon").build();
add_class(&icon, "icon");
let label = Label::builder() let label = Label::builder()
.label(&self.format) .label(&self.format)
.use_markup(true) .use_markup(true)
.name("label")
.build(); .build();
add_class(&label, "label");
let container = gtk::Box::new(Orientation::Horizontal, 0); let container = gtk::Box::builder()
add_class(&container, "upower"); .orientation(Orientation::Horizontal)
.name("upower")
.build();
let button = Button::new(); let button = Button::builder().name("button").build();
add_class(&button, "button");
button.add(&label); button.add(&label);
container.add(&button); container.add(&button);
@@ -208,10 +207,11 @@ impl Module<gtk::Box> for UpowerModule {
{ {
let container = gtk::Box::builder() let container = gtk::Box::builder()
.orientation(Orientation::Horizontal) .orientation(Orientation::Horizontal)
.name("popup-upower")
.build(); .build();
let label = Label::new(None); let label = Label::builder().name("upower-details").build();
add_class(&label, "upower-details"); container.add(&label);
rx.attach(None, move |properties| { rx.attach(None, move |properties| {
let mut format = String::new(); let mut format = String::new();

View File

@@ -186,7 +186,7 @@ impl Module<gtk::Box> for WorkspacesModule {
icon_size, icon_size,
&context.controller_tx, &context.controller_tx,
); );
container.add(&item); container.append(&item);
button_map.insert(workspace.name, item); button_map.insert(workspace.name, item);
} }
@@ -196,7 +196,6 @@ impl Module<gtk::Box> for WorkspacesModule {
reorder_workspaces(&container); reorder_workspaces(&container);
} }
container.show_all();
has_initialized = true; has_initialized = true;
} }
} }
@@ -223,7 +222,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&context.controller_tx, &context.controller_tx,
); );
container.add(&item); container.append(&item);
if self.sort == SortOrder::Alphanumeric { if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container); reorder_workspaces(&container);
} }
@@ -248,7 +247,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&context.controller_tx, &context.controller_tx,
); );
container.add(&item); container.append(&item);
if self.sort == SortOrder::Alphanumeric { if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container); reorder_workspaces(&container);

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use glib::signal::Inhibit;
use crate::config::BarPosition; use crate::config::BarPosition;
use crate::modules::ModuleInfo; use crate::modules::ModuleInfo;
@@ -121,7 +122,7 @@ impl Popup {
if let Some(content) = self.cache.get(&key) { if let Some(content) = self.cache.get(&key) {
content.style_context().add_class("popup"); content.style_context().add_class("popup");
self.window.add(content); self.window.append(content);
} }
} }