diff --git a/Cargo.lock b/Cargo.lock
index a707628..99e8b5d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1360,6 +1360,7 @@ dependencies = [
"libcorn",
"mpd_client",
"mpris",
+ "nix 0.26.2",
"notify",
"regex",
"reqwest",
@@ -1658,6 +1659,20 @@ dependencies = [
"memoffset 0.6.5",
]
+[[package]]
+name = "nix"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+ "memoffset 0.7.1",
+ "pin-utils",
+ "static_assertions",
+]
+
[[package]]
name = "nom"
version = "7.1.1"
@@ -3274,45 +3289,45 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winreg"
diff --git a/Cargo.toml b/Cargo.toml
index 2330d71..4cd4d4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ description = "Customisable GTK Layer Shell wlroots/sway bar"
default = [
"http",
"config+all",
+ "clipboard",
"clock",
"music+all",
"sys_info",
@@ -24,6 +25,8 @@ http = ["dep:reqwest"]
"config+toml" = ["toml"]
"config+corn" = ["libcorn"]
+clipboard = ["nix"]
+
clock = ["chrono"]
music = ["regex"]
@@ -60,6 +63,7 @@ notify = { version = "5.0.0", default-features = false }
wayland-client = "0.29.5"
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
smithay-client-toolkit = { version = "0.16.0", default-features = false, features = ["calloop"] }
+
lazy_static = "1.4.0"
async_once = "0.2.6"
cfg-if = "1.0.0"
@@ -73,6 +77,9 @@ serde_yaml = { version = "0.9.4", optional = true }
toml = { version = "0.7.0", optional = true }
libcorn = { version = "0.6.1", optional = true }
+# clipboard
+nix = { version = "0.26.2", optional = true }
+
# clock
chrono = { version = "0.4.19", optional = true }
@@ -92,4 +99,4 @@ hyprland = { version = "0.3.0", optional = true }
futures-util = { version = "0.3.21", optional = true }
# shared
-regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
\ No newline at end of file
+regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
diff --git a/README.md b/README.md
index eb1a206..12ef692 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,23 @@ It uses GTK3 and gtk-layer-shell.
The bar can be styled to your liking using CSS and hot-loads style changes.
For information and examples on styling please see the [wiki](https://github.com/JakeStanger/ironbar/wiki).
-
+
+
+## Features
+
+- First-class support for Sway and Hyprland, but should (mostly) work on any wlroots compositor.
+- Fully themeable with CSS and hot-loaded styles.
+- Support for multiple configuration languages.
+- Popups used by widgets to show rich content and controls on click.
+- Out of the box widgets which can be used to create anything from a lightweight to a more traditional desktop experience.
+- Ability to create custom widgets (including popups), run scripts and inject dynamic content.
## Installation
### Cargo
+Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
+
```sh
cargo install ironbar
```
@@ -74,6 +85,8 @@ in case you don't want to compile Ironbar.
### Source
+Ensure you have the [build dependencies](https://github.com/JakeStanger/ironbar/wiki/compiling#Build-requirements) installed.
+
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
@@ -83,7 +96,7 @@ install target/release/ironbar ~/.local/bin/ironbar
```
By default, all features are enabled.
-See [here](https://github.com/JakeStanger/ironbar/wiki/compiling) for controlling which features are included.
+See [here](https://github.com/JakeStanger/ironbar/wiki/compiling#features) for controlling which features are included.
[repo](https://github.com/jakestanger/ironbar)
diff --git a/docs/Compiling.md b/docs/Compiling.md
index 23e065c..6e02668 100644
--- a/docs/Compiling.md
+++ b/docs/Compiling.md
@@ -9,6 +9,28 @@ cargo build --release
install target/release/ironbar ~/.local/bin/ironbar
```
+## Build requirements
+
+To build from source, you must have GTK (>= 3.22) and GTK Layer Shell installed.
+
+### Arch
+
+```shell
+pacman -S gtk3 gtk-layer-shell
+```
+
+### Ubuntu/Debian
+
+```shell
+apt install libgtk-3-dev libgtk-layer-shell-dev
+```
+
+### Fedora
+
+```shell
+dnf install gtk3 gtk-layer-shell
+```
+
## Features
By default, all features are enabled for convenience. This can result in a significant compile time.
@@ -39,6 +61,7 @@ cargo build --release --no-default-features \
| config+toml | Enables configuration support for TOML. |
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger.corn). |
| **Modules** | |
+| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| music+all | Enables the `music` module with support for all player types. |
| music+mpris | Enables the `music` module with MPRIS support. |
diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md
index 58f98eb..61a393f 100644
--- a/docs/_Sidebar.md
+++ b/docs/_Sidebar.md
@@ -1,10 +1,10 @@
# Guides
+- [Compiling from source](compiling)
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
-- [Examples](https://github.com/JakeStanger/ironbar/tree/master/examples)
# Examples
@@ -17,6 +17,7 @@
# Modules
+- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)
- [Focused](focused)
diff --git a/docs/modules/Clipboard.md b/docs/modules/Clipboard.md
new file mode 100644
index 0000000..24161dc
--- /dev/null
+++ b/docs/modules/Clipboard.md
@@ -0,0 +1,93 @@
+Shows recent clipboard items, allowing you to switch between them to re-copy previous values.
+Clicking the icon button opens the popup containing all functionality.
+
+Supports plain text and images.
+
+
+
+## Configuration
+
+> Type: `clipboard`
+
+| Name | Type | Default | Description |
+|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `icon` | `string/image` | `` | Icon to show on the widget button. |
+| `max_items` | `integer` | `10` | Maximum number of items to show on the bar. |
+| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
+| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
+| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
+| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
+
+See [here](images) for information on images.
+
+
+JSON
+
+```json
+{
+ "end": {
+ "type": "clipboard",
+ "max_items": 3,
+ "truncate": {
+ "mode": "end",
+ "length": 50
+ }
+ }
+}
+```
+
+
+
+TOML
+
+```toml
+[[end]]
+type = "clipboard"
+max_items = 3
+
+[[end.truncate]]
+mode = "end"
+length = 50
+```
+
+
+
+YAML
+
+```yaml
+end:
+ - type: 'clipboard'
+ max_items: 3
+ truncate:
+ mode: 'end'
+ length: 50
+```
+
+
+
+Corn
+
+```corn
+{
+ end = [ {
+ type = "clipboard"
+ max_items = 3
+ truncate.mode = "end"
+ truncate.length = 50
+ } ]
+}
+```
+
+
+## Styling
+
+| Selector | Description |
+|--------------------------------------|------------------------------------------------------|
+| `#clipboard` | Clipboard widget. |
+| `#clipboard .btn` | Clipboard widget button. |
+| `#popup-clipboard` | Clipboard popup box. |
+| `#popup-clipboard .item` | Clipboard row item inside the popup. |
+| `#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.image` | Clipboard row item radio button (image values only). |
+| `#popup-clipboard .item .btn-remove` | Clipboard row item remove button. |
\ No newline at end of file
diff --git a/docs/modules/Clock.md b/docs/modules/Clock.md
index 72ed9fe..99a944b 100644
--- a/docs/modules/Clock.md
+++ b/docs/modules/Clock.md
@@ -69,9 +69,9 @@ end:
## Styling
-| Selector | Description |
-|-------------------------------|------------------------------------------------------------------------------------|
-| `#clock` | Clock widget button |
-| `#popup-clock` | Clock popup box |
+| Selector | Description |
+|--------------------------------|------------------------------------------------------------------------------------|
+| `#clock` | Clock widget button |
+| `#popup-clock` | Clock popup box |
| `#popup-clock #calendar-clock` | Clock inside the popup |
| `#popup-clock #calendar` | Calendar widget inside the popup. GTK provides some OOTB styling options for this. |
\ No newline at end of file
diff --git a/docs/modules/Focused.md b/docs/modules/Focused.md
index 52469d3..514cbcf 100644
--- a/docs/modules/Focused.md
+++ b/docs/modules/Focused.md
@@ -7,14 +7,15 @@ Displays the title and/or icon of the currently focused window.
> Type: `focused`
-| Name | Type | Default | Description |
-|-------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
-| `show_title` | `boolean` | `true` | Whether to show the app's title |
-| `icon_size` | `integer` | `32` | Size of icon in pixels |
-| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
-| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
-| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
+| Name | Type | Default | Description |
+|-----------------------|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
+| `show_title` | `boolean` | `true` | Whether to show the app's title |
+| `icon_size` | `integer` | `32` | Size of icon in pixels |
+| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
+| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
+| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
+| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
JSON
diff --git a/docs/modules/Music.md b/docs/modules/Music.md
index 46608c7..044c816 100644
--- a/docs/modules/Music.md
+++ b/docs/modules/Music.md
@@ -11,23 +11,24 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
> Type: `music`
-| | Type | Default | Description |
-|-------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
-| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
-| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
-| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
-| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
-| `icons.play` | `string/image` | `` | Icon to show when playing. |
-| `icons.pause` | `string/image` | `` | Icon to show when paused. |
-| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
-| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
-| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
-| `icons.track` | `string/image` | `` | Icon to show next to track title. |
-| `icons.album` | `string/image` | `` | Icon to show next to album name. |
-| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
-| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
-| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
+| | Type | Default | Description |
+|-----------------------|---------------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `player_type` | `mpris` or `mpd` | `mpris` | Whether to connect to MPRIS players or an MPD server. |
+| `format` | `string` | `{title} / {artist}` | Format string for the widget. More info below. |
+| `truncate` | `start` or `middle` or `end` or `Map` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. |
+| `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. |
+| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
+| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
+| `icons.play` | `string/image` | `` | Icon to show when playing. |
+| `icons.pause` | `string/image` | `` | Icon to show when paused. |
+| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
+| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
+| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
+| `icons.track` | `string/image` | `` | Icon to show next to track title. |
+| `icons.album` | `string/image` | `` | Icon to show next to album name. |
+| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
+| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
+| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
See [here](images) for information on images.
diff --git a/examples/config.corn b/examples/config.corn
index 2f96167..9fc572c 100644
--- a/examples/config.corn
+++ b/examples/config.corn
@@ -20,8 +20,18 @@ let {
show_icons = true
}
- $mpd_local = { type = "mpd" music_dir = "/home/jake/Music" }
- $mpd_server = { type = "mpd" host = "chloe:6600" }
+ $mpris = {
+ type = "music"
+ player_type = "mpris"
+
+ on_click_middle = "playerctl play-pause"
+ on_scroll_up = "playerctl volume +5"
+ on_scroll_down = "playerctl volume -5"
+
+ }
+
+ $mpd_local = { type = "music" player_type = "mpd" music_dir = "/home/jake/Music" truncate.mode = "end" truncate.max_length = 100 }
+ $mpd_server = { type = "music" player_type = "mpd" host = "chloe:6600" truncate = "end" }
$sys_info = {
type = "sys_info"
@@ -55,6 +65,8 @@ let {
show_if.interval = 500
}
+ $clipboard = { type = "clipboard" max_items = 3 truncate.mode = "end" truncate.length = 50 }
+
// -- begin custom --
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
@@ -86,10 +98,13 @@ let {
// -- end custom --
$left = [ $workspaces $launcher ]
- $right = [ $mpd_local $mpd_server $phone_battery $sys_info $power_menu $clock ]
+ $right = [ $mpd_local $mpd_server $phone_battery $sys_info $clipboard $power_menu $clock ]
}
in {
anchor_to_edges = true
- position = "top"
- start = $left end = $right
+ position = "bottom"
+ icon_theme = "Paper"
+
+ start = $left
+ end = $right
}
diff --git a/examples/config.json b/examples/config.json
index 84f6c30..05f3e04 100644
--- a/examples/config.json
+++ b/examples/config.json
@@ -5,7 +5,7 @@
"music_dir": "/home/jake/Music",
"player_type": "mpd",
"truncate": {
- "length": 100,
+ "max_length": 100,
"mode": "end"
},
"type": "music"
@@ -43,6 +43,14 @@
},
"type": "sys_info"
},
+ {
+ "max_items": 3,
+ "truncate": {
+ "length": 50,
+ "mode": "end"
+ },
+ "type": "clipboard"
+ },
{
"bar": [
{
@@ -98,16 +106,6 @@
"icon_theme": "Paper",
"position": "bottom",
"start": [
- {
- "bar": [
- {
- "size": 32,
- "src": "file:///path/to/image.jpg",
- "type": "image"
- }
- ],
- "type": "custom"
- },
{
"all_monitors": false,
"name_map": {
diff --git a/examples/config.toml b/examples/config.toml
index 181bb8b..a511b59 100644
--- a/examples/config.toml
+++ b/examples/config.toml
@@ -8,7 +8,7 @@ player_type = 'mpd'
type = 'music'
[end.truncate]
-length = 100
+max_length = 100
mode = 'end'
[[end]]
@@ -44,6 +44,14 @@ memory = 30
networks = 3
temps = 5
+[[end]]
+max_items = 3
+type = 'clipboard'
+
+[end.truncate]
+length = 50
+mode = 'end'
+
[[end]]
class = 'power-menu'
tooltip = '''Up: {{30000:uptime -p | cut -d ' ' -f2-}}'''
@@ -87,14 +95,6 @@ type = 'label'
[[end]]
type = 'clock'
-[[start]]
-type = 'custom'
-
-[[start.bar]]
-size = 32
-src = 'file:///path/to/image.jpg'
-type = 'image'
-
[[start]]
all_monitors = false
type = 'workspaces'
diff --git a/examples/config.yaml b/examples/config.yaml
index debb1cd..59ee18e 100644
--- a/examples/config.yaml
+++ b/examples/config.yaml
@@ -1,50 +1,20 @@
anchor_to_edges: true
-icon_theme: Paper
-position: bottom
-
-start:
- - bar:
- - size: 32
- src: file:///path/to/image.jpg
- type: image
- type: custom
-
- - all_monitors: false
- name_map:
- '1': ﭮ
- '2': icon:firefox
- '3':
- Code:
- Games: icon:steam
- type: workspaces
-
- - favorites:
- - firefox
- - discord
- - Steam
- show_icons: true
- show_names: false
- type: launcher
-
end:
- music_dir: /home/jake/Music
player_type: mpd
truncate:
- length: 100
+ max_length: 100
mode: end
type: music
-
- host: chloe:6600
player_type: mpd
truncate: end
type: music
-
- cmd: /home/jake/bin/phone-battery
show_if:
cmd: /home/jake/bin/phone-connected
interval: 500
type: script
-
- format:
- {cpu_percent}% | {temp_c:k10temp_Tccd1}°C
- {memory_used} / {memory_total} GB ({memory_percent}%)
@@ -60,7 +30,11 @@ end:
networks: 3
temps: 5
type: sys_info
-
+ - max_items: 3
+ truncate:
+ length: 50
+ mode: end
+ type: clipboard
- bar:
- label:
name: power-btn
@@ -89,9 +63,23 @@ end:
type: label
tooltip: 'Up: {{30000:uptime -p | cut -d '' '' -f2-}}'
type: custom
-
- type: clock
-
-
-
+icon_theme: Paper
+position: bottom
+start:
+ - all_monitors: false
+ name_map:
+ '1': ﭮ
+ '2': icon:firefox
+ '3':
+ Code:
+ Games: icon:steam
+ type: workspaces
+ - favorites:
+ - firefox
+ - discord
+ - Steam
+ show_icons: true
+ show_names: false
+ type: launcher
diff --git a/src/bar.rs b/src/bar.rs
index 0c508f1..b18978b 100644
--- a/src/bar.rs
+++ b/src/bar.rs
@@ -193,7 +193,7 @@ fn add_modules(content: >k::Box, modules: Vec, info: &ModuleInfo
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist");
- let widget = create_module($module, $id, &info, &Arc::clone(&popup))?;
+ let widget = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
let container = wrap_widget(&widget);
content.add(&container);
@@ -203,6 +203,8 @@ fn add_modules(content: >k::Box, modules: Vec, info: &ModuleInfo
for (id, config) in modules.into_iter().enumerate() {
match config {
+ #[cfg(feature = "clipboard")]
+ ModuleConfig::Clipboard(mut module) => add_module!(module, id),
#[cfg(feature = "clock")]
ModuleConfig::Clock(mut module) => add_module!(module, id),
ModuleConfig::Custom(mut module) => add_module!(module, id),
@@ -289,6 +291,10 @@ fn setup_receiver(
) where
TSend: Clone + Send + 'static,
{
+ // some rare cases can cause the popup to incorrectly calculate its size on first open.
+ // we can fix that by just force re-rendering it on its first open.
+ let mut has_popup_opened = false;
+
channel.recv(move |ev| {
match ev {
ModuleUpdateEvent::Update(update) => {
@@ -306,6 +312,12 @@ fn setup_receiver(
} else {
popup.show_content(id);
popup.show(geometry);
+
+ if !has_popup_opened {
+ popup.show_content(id);
+ popup.show(geometry);
+ has_popup_opened = true;
+ }
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
@@ -315,6 +327,12 @@ fn setup_receiver(
popup.hide();
popup.show_content(id);
popup.show(geometry);
+
+ if !has_popup_opened {
+ popup.show_content(id);
+ popup.show(geometry);
+ has_popup_opened = true;
+ }
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
@@ -394,8 +412,6 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
let scroll_down_script = common.on_scroll_down.map(Script::new_polling);
container.connect_scroll_event(move |_, event| {
- println!("{:?}", event.direction());
-
let script = match event.direction() {
ScrollDirection::Up => scroll_up_script.as_ref(),
ScrollDirection::Down => scroll_down_script.as_ref(),
diff --git a/src/clients/clipboard.rs b/src/clients/clipboard.rs
new file mode 100644
index 0000000..4301b2d
--- /dev/null
+++ b/src/clients/clipboard.rs
@@ -0,0 +1,245 @@
+use super::wayland::{self, ClipboardItem};
+use crate::{lock, try_send};
+use indexmap::map::Iter;
+use indexmap::IndexMap;
+use lazy_static::lazy_static;
+use std::sync::{Arc, Mutex};
+use tokio::spawn;
+use tokio::sync::mpsc;
+use tracing::debug;
+
+#[derive(Debug)]
+pub enum ClipboardEvent {
+ Add(Arc),
+ Remove(usize),
+ Activate(usize),
+}
+
+type EventSender = mpsc::Sender;
+
+/// Clipboard client singleton,
+/// to ensure bars don't duplicate requests to the compositor.
+pub struct ClipboardClient {
+ senders: Arc>>,
+ cache: Arc>,
+}
+
+impl ClipboardClient {
+ fn new() -> Self {
+ let senders = Arc::new(Mutex::new(Vec::<(EventSender, usize)>::new()));
+
+ let cache = Arc::new(Mutex::new(ClipboardCache::new()));
+
+ {
+ let senders = senders.clone();
+ let cache = cache.clone();
+
+ spawn(async move {
+ let mut rx = {
+ let wl = wayland::get_client().await;
+ wl.subscribe_clipboard()
+ };
+
+ while let Ok(item) = rx.recv().await {
+ debug!("Received clipboard item (ID: {})", item.id);
+
+ let (existing_id, cache_size) = {
+ let cache = lock!(cache);
+ (cache.contains(&item), cache.len())
+ };
+
+ existing_id.map_or_else(
+ || {
+ {
+ let mut cache = lock!(cache);
+ let senders = lock!(senders);
+ cache.insert(item.clone(), senders.len());
+ }
+ let senders = lock!(senders);
+ let iter = senders.iter();
+ for (tx, sender_cache_size) in iter {
+ if cache_size == *sender_cache_size {
+ let mut cache = lock!(cache);
+ let removed_id = cache
+ .remove_ref_first()
+ .expect("Clipboard cache unexpectedly empty");
+ try_send!(tx, ClipboardEvent::Remove(removed_id));
+ }
+ try_send!(tx, ClipboardEvent::Add(item.clone()));
+ }
+ },
+ |existing_id| {
+ let senders = lock!(senders);
+ let iter = senders.iter();
+ for (tx, _) in iter {
+ try_send!(tx, ClipboardEvent::Activate(existing_id));
+ }
+ },
+ );
+ }
+ });
+ }
+
+ Self { senders, cache }
+ }
+
+ pub async fn subscribe(&self, cache_size: usize) -> mpsc::Receiver {
+ let (tx, rx) = mpsc::channel(16);
+
+ let wl = wayland::get_client().await;
+ wl.roundtrip();
+
+ {
+ let mut cache = lock!(self.cache);
+
+ if let Some(item) = wl.get_clipboard() {
+ cache.insert_or_inc_ref(item);
+ }
+
+ let iter = cache.iter();
+ for (id, (item, _)) in iter {
+ println!("Initialising value with id {id}");
+ try_send!(tx, ClipboardEvent::Add(item.clone()));
+ }
+ }
+
+ {
+ let mut senders = lock!(self.senders);
+ senders.push((tx, cache_size));
+ }
+
+ rx
+ }
+
+ pub async fn copy(&self, id: usize) {
+ debug!("Copying item with id {id}");
+
+ let item = {
+ let cache = lock!(self.cache);
+ cache.get(id)
+ };
+
+ if let Some(item) = item {
+ let wl = wayland::get_client().await;
+ wl.copy_to_clipboard(item);
+ }
+
+ let senders = lock!(self.senders);
+ let iter = senders.iter();
+ for (tx, _) in iter {
+ try_send!(tx, ClipboardEvent::Activate(id));
+ }
+ }
+
+ pub fn remove(&self, id: usize) {
+ let mut cache = lock!(self.cache);
+ cache.remove(id);
+
+ let senders = lock!(self.senders);
+ let iter = senders.iter();
+ for (tx, _) in iter {
+ try_send!(tx, ClipboardEvent::Remove(id));
+ }
+ }
+}
+
+/// Shared clipboard item cache.
+///
+/// Items are stored with a number of references,
+/// allowing different consumers to 'remove' cached items
+/// at different times.
+#[derive(Debug)]
+struct ClipboardCache {
+ cache: IndexMap, usize)>,
+}
+
+impl ClipboardCache {
+ /// Creates a new empty cache.
+ fn new() -> Self {
+ Self {
+ cache: IndexMap::new(),
+ }
+ }
+
+ /// Gets the entry with key `id` from the cache.
+ fn get(&self, id: usize) -> Option> {
+ self.cache.get(&id).map(|(item, _)| item).cloned()
+ }
+
+ /// Inserts an entry with `ref_count` initial references.
+ fn insert(&mut self, item: Arc, ref_count: usize) -> Option> {
+ self.cache
+ .insert(item.id, (item, ref_count))
+ .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) {
+ let mut item = self.cache.entry(item.id).or_insert((item, 0));
+ item.1 += 1;
+ }
+
+ /// Removes the entry with key `id`.
+ /// This ignores references.
+ fn remove(&mut self, id: usize) -> Option> {
+ self.cache.shift_remove(&id).map(|(item, _)| item)
+ }
+
+ /// Removes a reference to the entry with key `id`.
+ ///
+ /// If the reference count reaches zero, the entry
+ /// is removed from the cache.
+ fn remove_ref(&mut self, id: usize) {
+ if let Some(entry) = self.cache.get_mut(&id) {
+ entry.1 -= 1;
+
+ if entry.1 == 0 {
+ self.cache.shift_remove(&id);
+ }
+ }
+ }
+
+ /// Removes a reference to the first entry.
+ ///
+ /// If the reference count reaches zero, the entry
+ /// is removed from the cache.
+ fn remove_ref_first(&mut self) -> Option {
+ if let Some((id, _)) = self.cache.first() {
+ let id = *id;
+ self.remove_ref(id);
+ Some(id)
+ } else {
+ None
+ }
+ }
+
+ /// Checks if an item with matching mime type and value
+ /// already exists in the cache.
+ fn contains(&self, item: &ClipboardItem) -> Option {
+ self.cache.values().find_map(|(it, _)| {
+ if it.mime_type == item.mime_type && it.value == item.value {
+ Some(it.id)
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Gets the current number of items in the cache.
+ fn len(&self) -> usize {
+ self.cache.len()
+ }
+
+ fn iter(&self) -> Iter<'_, usize, (Arc, usize)> {
+ self.cache.iter()
+ }
+}
+
+lazy_static! {
+ static ref CLIENT: ClipboardClient = ClipboardClient::new();
+}
+
+pub fn get_client() -> &'static ClipboardClient {
+ &CLIENT
+}
diff --git a/src/clients/mod.rs b/src/clients/mod.rs
index 31d72ba..d14fd0a 100644
--- a/src/clients/mod.rs
+++ b/src/clients/mod.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "clipboard")]
+pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "music")]
diff --git a/src/clients/wayland/client.rs b/src/clients/wayland/client.rs
index b0eb716..11adc02 100644
--- a/src/clients/wayland/client.rs
+++ b/src/clients/wayland/client.rs
@@ -1,31 +1,61 @@
-use super::toplevel::{ToplevelEvent, ToplevelInfo};
-use super::toplevel_manager::listen_for_toplevels;
-use super::ToplevelChange;
-use super::{Env, ToplevelHandler};
-use crate::{error as err, send, write_lock};
+use super::wlr_foreign_toplevel::{
+ handle::{ToplevelEvent, ToplevelInfo},
+ manager::listen_for_toplevels,
+};
+use super::{DData, Env, ToplevelHandler};
+use crate::{error as err, send};
+use cfg_if::cfg_if;
use color_eyre::Report;
use indexmap::IndexMap;
use smithay_client_toolkit::environment::Environment;
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
-use smithay_client_toolkit::reexports::calloop;
-use smithay_client_toolkit::{new_default_environment, WaylandSource};
+use smithay_client_toolkit::reexports::calloop::channel::{channel, Event, Sender};
+use smithay_client_toolkit::reexports::calloop::EventLoop;
+use smithay_client_toolkit::WaylandSource;
+use std::collections::HashMap;
use std::sync::{Arc, RwLock};
-use std::time::Duration;
use tokio::sync::{broadcast, oneshot};
use tokio::task::spawn_blocking;
-use tracing::{error, trace};
+use tracing::{debug, error};
use wayland_client::protocol::wl_seat::WlSeat;
+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! {
+ if #[cfg(feature = "clipboard")] {
+ use super::{ClipboardItem};
+ use super::wlr_data_control::manager::{listen_to_devices, DataControlDeviceHandler};
+ use crate::{read_lock, write_lock};
+ use tokio::spawn;
+ }
+}
+
+#[derive(Debug)]
+pub enum Request {
+ /// Copies the value to the clipboard
+ #[cfg(feature = "clipboard")]
+ CopyToClipboard(Arc),
+ /// Forces a dispatch, flushing any currently queued events
+ Refresh,
+}
+
pub struct WaylandClient {
pub outputs: Vec,
pub seats: Vec,
+
pub toplevels: Arc>>,
toplevel_tx: broadcast::Sender,
_toplevel_rx: broadcast::Receiver,
+
+ #[cfg(feature = "clipboard")]
+ clipboard_tx: broadcast::Sender>,
+ #[cfg(feature = "clipboard")]
+ clipboard: Arc>>>,
+
+ request_tx: Sender,
}
impl WaylandClient {
@@ -35,21 +65,44 @@ impl WaylandClient {
let (toplevel_tx, toplevel_rx) = broadcast::channel(32);
- let toplevel_tx2 = toplevel_tx.clone();
-
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
let toplevels2 = toplevels.clone();
- // `queue` is not send so we need to handle everything inside the task
+ let toplevel_tx2 = toplevel_tx.clone();
+
+ cfg_if! {
+ if #[cfg(feature = "clipboard")] {
+ let (clipboard_tx, mut clipboard_rx) = broadcast::channel(32);
+ let clipboard = Arc::new(RwLock::new(None));
+ let clipboard_tx2 = clipboard_tx.clone();
+ }
+ }
+
+ let (ev_tx, ev_rx) = channel::();
+
+ // `queue` is not `Send` so we need to handle everything inside the task
spawn_blocking(move || {
+ let toplevels = toplevels2;
+ let toplevel_tx = toplevel_tx2;
+
let (env, _display, queue) =
- new_default_environment!(Env, fields = [toplevel: ToplevelHandler::init()])
- .expect("Failed to connect to Wayland compositor");
+ Self::new_environment().expect("Failed to connect to Wayland compositor");
+
+ let mut event_loop =
+ EventLoop::::try_new().expect("Failed to create new event loop");
+ WaylandSource::new(queue)
+ .quick_insert(event_loop.handle())
+ .expect("Failed to insert Wayland event queue into event loop");
let outputs = Self::get_outputs(&env);
send!(output_tx, outputs);
let seats = env.get_all_seats();
+
+ // TODO: Actually handle seats properly
+ #[cfg(feature = "clipboard")]
+ let default_seat = seats[0].detach();
+
send!(
seat_tx,
seats
@@ -58,30 +111,56 @@ impl WaylandClient {
.collect::>()
);
+ let handle = event_loop.handle();
+ handle
+ .insert_source(ev_rx, move |event, _metadata, ddata| {
+ // let env = &ddata.env;
+ match event {
+ Event::Msg(Request::Refresh) => debug!("Received refresh event"),
+ #[cfg(feature = "clipboard")]
+ Event::Msg(Request::CopyToClipboard(value)) => {
+ super::wlr_data_control::copy_to_clipboard(
+ &ddata.env,
+ &default_seat,
+ &value,
+ )
+ .expect("Failed to copy to clipboard");
+ }
+ Event::Closed => panic!("Channel unexpectedly closed"),
+ }
+ })
+ .expect("Failed to insert channel into event queue");
+
let _toplevel_manager = env.require_global::();
- let _listener = listen_for_toplevels(env, move |handle, event, _ddata| {
- trace!("Received toplevel event: {:?}", event);
-
- if event.change == ToplevelChange::Close {
- write_lock!(toplevels2).remove(&event.toplevel.id);
- } else {
- write_lock!(toplevels2)
- .insert(event.toplevel.id, (event.toplevel.clone(), handle));
- }
-
- send!(toplevel_tx2, event);
+ let _toplevel_listener = listen_for_toplevels(&env, move |handle, event, _ddata| {
+ super::wlr_foreign_toplevel::update_toplevels(
+ &toplevels,
+ handle,
+ event,
+ &toplevel_tx,
+ );
});
- let mut event_loop =
- calloop::EventLoop::<()>::try_new().expect("Failed to create new event loop");
- WaylandSource::new(queue)
- .quick_insert(event_loop.handle())
- .expect("Failed to insert event loop into wayland event queue");
+ 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 {
- // TODO: Avoid need for duration here - can we force some event when sending requests?
- if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
+ if let Err(err) = event_loop.dispatch(None, &mut data) {
error!(
"{:?}",
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
@@ -90,6 +169,18 @@ 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);
@@ -97,9 +188,14 @@ impl WaylandClient {
Self {
outputs,
seats,
+ #[cfg(feature = "clipboard")]
+ clipboard,
toplevels,
toplevel_tx,
_toplevel_rx: toplevel_rx,
+ #[cfg(feature = "clipboard")]
+ clipboard_tx,
+ request_tx: ev_tx,
}
}
@@ -107,6 +203,26 @@ impl WaylandClient {
self.toplevel_tx.subscribe()
}
+ #[cfg(feature = "clipboard")]
+ pub fn subscribe_clipboard(&self) -> broadcast::Receiver> {
+ self.clipboard_tx.subscribe()
+ }
+
+ pub fn roundtrip(&self) {
+ send!(self.request_tx, Request::Refresh);
+ }
+
+ #[cfg(feature = "clipboard")]
+ pub fn get_clipboard(&self) -> Option> {
+ let clipboard = read_lock!(self.clipboard);
+ clipboard.as_ref().cloned()
+ }
+
+ #[cfg(feature = "clipboard")]
+ pub fn copy_to_clipboard(&self, item: Arc) {
+ send!(self.request_tx, Request::CopyToClipboard(item));
+ }
+
fn get_outputs(env: &Environment) -> Vec {
let outputs = env.get_all_outputs();
@@ -115,4 +231,57 @@ impl WaylandClient {
.filter_map(|output| with_output_info(output, Clone::clone))
.collect()
}
+
+ fn new_environment() -> Result<(Environment, 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}");
+ },
+ ),
+ }
+ })
+ }
}
diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs
index 54f4fc5..0f567d9 100644
--- a/src/clients/wayland/mod.rs
+++ b/src/clients/wayland/mod.rs
@@ -1,21 +1,32 @@
mod client;
-mod toplevel;
-mod toplevel_manager;
-extern crate smithay_client_toolkit as sctk;
+mod wlr_foreign_toplevel;
+use std::collections::HashMap;
use async_once::AsyncOnce;
use lazy_static::lazy_static;
-pub use toplevel::{ToplevelChange, ToplevelEvent, ToplevelInfo};
-use toplevel_manager::{ToplevelHandler, ToplevelHandling, ToplevelStatusListener};
-use wayland_client::{Attached, DispatchData, Interface};
-use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
- zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
- zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
-};
+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 client::WaylandClient;
+cfg_if! {
+ if #[cfg(feature = "clipboard")] {
+ mod wlr_data_control;
+
+ use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
+ use wlr_data_control::manager::DataControlDeviceHandler;
+ pub use wlr_data_control::{ClipboardItem, ClipboardValue};
+ }
+}
+
/// A utility for lazy-loading globals.
/// Taken from `smithay_client_toolkit` where it's not exposed
#[derive(Debug)]
@@ -25,21 +36,32 @@ enum LazyGlobal {
Bound(Attached),
}
-sctk::default_environment!(Env,
- fields = [
- toplevel: ToplevelHandler
- ],
- singles = [
- ZwlrForeignToplevelManagerV1 => toplevel
- ],
-);
+pub struct DData {
+ env: Environment,
+ offer_tokens: HashMap,
+}
-impl ToplevelHandling for Env {
- fn listen(&mut self, f: F) -> ToplevelStatusListener
- where
- F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
- {
- self.toplevel.listen(f)
+cfg_if! {
+ if #[cfg(feature = "clipboard")] {
+ default_environment!(Env,
+ fields = [
+ toplevel: ToplevelHandler,
+ data_control_device: DataControlDeviceHandler
+ ],
+ singles = [
+ ZwlrForeignToplevelManagerV1 => toplevel,
+ ZwlrDataControlManagerV1 => data_control_device
+ ],
+ );
+ } else {
+ default_environment!(Env,
+ fields = [
+ toplevel: ToplevelHandler,
+ ],
+ singles = [
+ ZwlrForeignToplevelManagerV1 => toplevel,
+ ],
+ );
}
}
diff --git a/src/clients/wayland/wlr_data_control/device.rs b/src/clients/wayland/wlr_data_control/device.rs
new file mode 100644
index 0000000..493eec6
--- /dev/null
+++ b/src/clients/wayland/wlr_data_control/device.rs
@@ -0,0 +1,88 @@
+use super::offer::DataControlOffer;
+use super::source::DataControlSource;
+use crate::lock;
+use std::sync::{Arc, Mutex};
+use wayland_client::protocol::wl_seat::WlSeat;
+use wayland_client::{Attached, DispatchData, Main};
+use wayland_protocols::wlr::unstable::data_control::v1::client::{
+ zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
+ zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
+ zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
+};
+
+#[derive(Debug)]
+struct Inner {
+ offer: Option>,
+}
+
+impl Inner {
+ fn new_offer(&mut self, offer: &Main) {
+ self.offer.replace(Arc::new(DataControlOffer::new(offer)));
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DataControlDeviceEvent(pub Arc);
+
+fn data_control_device_implem(
+ event: Event,
+ inner: &mut Inner,
+ implem: &mut F,
+ ddata: DispatchData,
+) where
+ F: FnMut(DataControlDeviceEvent, DispatchData),
+{
+ match event {
+ Event::DataOffer { id } => {
+ inner.new_offer(&id);
+ }
+ Event::Selection { id: Some(offer) } => {
+ let inner_offer = inner
+ .offer
+ .clone()
+ .expect("Offer should exist at this stage");
+ if offer == inner_offer.offer {
+ implem(DataControlDeviceEvent(inner_offer), ddata);
+ }
+ }
+ _ => {}
+ }
+}
+
+pub struct DataControlDevice {
+ device: ZwlrDataControlDeviceV1,
+ _inner: Arc>,
+}
+
+impl DataControlDevice {
+ pub fn init_for_seat(
+ manager: &Attached,
+ 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) {
+ self.device
+ .set_selection(source.as_ref().map(|s| &s.source));
+ }
+}
diff --git a/src/clients/wayland/wlr_data_control/manager.rs b/src/clients/wayland/wlr_data_control/manager.rs
new file mode 100644
index 0000000..fcd239a
--- /dev/null
+++ b/src/clients/wayland/wlr_data_control/manager.rs
@@ -0,0 +1,253 @@
+use super::device::{DataControlDevice, DataControlDeviceEvent};
+use super::source::DataControlSource;
+use smithay_client_toolkit::data_device::WritePipe;
+use smithay_client_toolkit::environment::{Environment, GlobalHandler};
+use smithay_client_toolkit::seat::{SeatHandling, SeatListener};
+use smithay_client_toolkit::MissingGlobal;
+use std::cell::RefCell;
+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::{Attached, DispatchData};
+use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
+
+enum DataControlDeviceHandlerInner {
+ Ready {
+ manager: Attached,
+ devices: Vec<(WlSeat, DataControlDevice)>,
+ status_listeners: Rc>>>>,
+ },
+ Pending {
+ seats: Vec,
+ status_listeners: Rc>>>>,
+ },
+}
+
+impl DataControlDeviceHandlerInner {
+ fn init_manager(&mut self, manager: Attached) {
+ let (seats, status_listeners) = if let Self::Pending {
+ seats,
+ status_listeners,
+ } = self
+ {
+ (std::mem::take(seats), status_listeners.clone())
+ } else {
+ 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,
+ devices,
+ status_listeners,
+ };
+ }
+
+ fn get_manager(&self) -> Option> {
+ 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(&self, mime_types: Vec, callback: F) -> Option
+ 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(&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),
+ }
+ }
+}
+
+pub struct DataControlDeviceHandler {
+ inner: Rc>,
+ status_listeners: Rc>>>>,
+ _seat_listener: SeatListener,
+}
+
+impl DataControlDeviceHandler {
+ pub fn init(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 GlobalHandler for DataControlDeviceHandler {
+ fn created(
+ &mut self,
+ registry: Attached,
+ id: u32,
+ version: u32,
+ _ddata: DispatchData,
+ ) {
+ // data control manager is supported until version 2
+ let version = std::cmp::min(version, 2);
+
+ let manager = registry.bind::(version, id);
+ self.inner.borrow_mut().init_manager((*manager).clone());
+ }
+
+ fn get(&self) -> Option> {
+ 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>>>,
+) {
+ 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>,
+}
+
+pub trait DataControlDeviceHandling {
+ fn listen(&mut self, f: F) -> DataControlDeviceStatusListener
+ where
+ F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static;
+
+ fn with_data_control_device(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
+ where
+ F: FnOnce(&DataControlDevice);
+
+ fn create_source(&self, mime_types: Vec, callback: F) -> Option
+ where
+ F: FnMut(String, WritePipe, DispatchData) + 'static;
+}
+
+impl DataControlDeviceHandling for DataControlDeviceHandler {
+ fn listen(&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(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
+ where
+ F: FnOnce(&DataControlDevice),
+ {
+ RefCell::borrow(&self.inner).with_device(seat, f)
+ }
+
+ fn create_source(&self, mime_types: Vec, callback: F) -> Option
+ where
+ F: FnMut(String, WritePipe, DispatchData) + 'static,
+ {
+ RefCell::borrow(&self.inner).create_source(mime_types, callback)
+ }
+}
+
+pub fn listen_to_devices(env: &Environment, f: F) -> DataControlDeviceStatusListener
+where
+ E: DataControlDeviceHandling,
+ F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
+{
+ env.with_inner(move |inner| DataControlDeviceHandling::listen(inner, f))
+}
diff --git a/src/clients/wayland/wlr_data_control/mod.rs b/src/clients/wayland/wlr_data_control/mod.rs
new file mode 100644
index 0000000..2a5f99a
--- /dev/null
+++ b/src/clients/wayland/wlr_data_control/mod.rs
@@ -0,0 +1,259 @@
+pub mod device;
+pub mod manager;
+pub mod offer;
+pub mod source;
+
+use super::Env;
+use crate::clients::wayland::DData;
+use crate::send;
+use color_eyre::Report;
+use device::{DataControlDevice, DataControlDeviceEvent};
+use glib::Bytes;
+use manager::{DataControlDeviceHandling, DataControlDeviceStatusListener};
+use smithay_client_toolkit::data_device::WritePipe;
+use smithay_client_toolkit::environment::Environment;
+use smithay_client_toolkit::reexports::calloop::LoopHandle;
+use smithay_client_toolkit::MissingGlobal;
+use source::DataControlSource;
+use std::fs::File;
+use std::io;
+use std::io::{Read, Write};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Arc;
+use std::time::UNIX_EPOCH;
+use tokio::sync::broadcast;
+use tracing::{debug, error, trace};
+use wayland_client::protocol::wl_seat::WlSeat;
+use wayland_client::DispatchData;
+
+static COUNTER: AtomicUsize = AtomicUsize::new(1);
+
+const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
+
+fn get_id() -> usize {
+ COUNTER.fetch_add(1, Ordering::Relaxed)
+}
+
+#[derive(Debug, Clone, Eq)]
+pub struct ClipboardItem {
+ pub id: usize,
+ pub value: ClipboardValue,
+ pub mime_type: String,
+}
+
+impl PartialEq for ClipboardItem {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ClipboardValue {
+ Text(String),
+ Image(Bytes),
+ Other,
+}
+
+impl DataControlDeviceHandling for Env {
+ fn listen(&mut self, f: F) -> DataControlDeviceStatusListener
+ where
+ F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
+ {
+ self.data_control_device.listen(f)
+ }
+
+ fn with_data_control_device(&self, seat: &WlSeat, f: F) -> Result<(), MissingGlobal>
+ where
+ F: FnOnce(&DataControlDevice),
+ {
+ self.data_control_device.with_data_control_device(seat, f)
+ }
+
+ fn create_source(&self, mime_types: Vec, callback: F) -> Option
+ where
+ F: FnMut(String, WritePipe, DispatchData) + 'static,
+ {
+ self.data_control_device.create_source(mime_types, callback)
+ }
+}
+
+pub fn copy_to_clipboard(
+ env: &Environment,
+ 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)]
+struct MimeType {
+ value: String,
+ category: MimeTypeCategory,
+}
+
+#[derive(Debug)]
+enum MimeTypeCategory {
+ Text,
+ Image,
+}
+
+impl MimeType {
+ fn parse(mime_types: &[String]) -> Option {
+ mime_types
+ .iter()
+ .map(|s| s.to_lowercase())
+ .find_map(|mime_type| match mime_type.as_str() {
+ "text"
+ | "string"
+ | "utf8_string"
+ | "text/plain"
+ | "text/plain;charset=utf-8"
+ | "text/plain;charset=iso-8859-1"
+ | "text/plain;charset=us-ascii"
+ | "text/plain;charset=unicode" => Some(Self {
+ value: mime_type,
+ category: MimeTypeCategory::Text,
+ }),
+ "image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
+ | "image/x-bmp" | "image/icon" => Some(Self {
+ value: mime_type,
+ category: MimeTypeCategory::Image,
+ }),
+ _ => None,
+ })
+ }
+}
+
+pub fn receive_offer(
+ event: DataControlDeviceEvent,
+ handle: &LoopHandle,
+ tx: broadcast::Sender>,
+ 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();
+
+ let offer = event.0;
+
+ let ddata = ddata
+ .get::()
+ .expect("Expected dispatch data to exist");
+
+ 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(×tamp) {
+ 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
+ })
+ );
+ }
+ }
+
+ Ok::<(), Report>(())
+ });
+
+ if let Err(err) = res {
+ error!("{err:?}");
+ }
+}
+
+fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result {
+ let value = match mime_type.category {
+ MimeTypeCategory::Text => {
+ let mut txt = String::new();
+ file.read_to_string(&mut txt)?;
+
+ ClipboardValue::Text(txt)
+ }
+ MimeTypeCategory::Image => {
+ let mut bytes = vec![];
+ file.read_to_end(&mut bytes)?;
+ let bytes = Bytes::from(&bytes);
+
+ println!("Num bytes: {}", bytes.len());
+ ClipboardValue::Image(bytes)
+ }
+ };
+
+ Ok(ClipboardItem {
+ id: get_id(),
+ value,
+ mime_type: mime_type.value.clone(),
+ })
+}
diff --git a/src/clients/wayland/wlr_data_control/offer.rs b/src/clients/wayland/wlr_data_control/offer.rs
new file mode 100644
index 0000000..ba9dd2b
--- /dev/null
+++ b/src/clients/wayland/wlr_data_control/offer.rs
@@ -0,0 +1,74 @@
+use crate::lock;
+use nix::fcntl::OFlag;
+use nix::unistd::{close, pipe2};
+use smithay_client_toolkit::data_device::ReadPipe;
+use std::io;
+use std::os::fd::FromRawFd;
+use std::sync::{Arc, Mutex};
+use tracing::warn;
+use wayland_client::Main;
+use wayland_protocols::wlr::unstable::data_control::v1::client::zwlr_data_control_offer_v1::{
+ Event, ZwlrDataControlOfferV1,
+};
+
+#[derive(Debug, Clone)]
+struct Inner {
+ mime_types: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub struct DataControlOffer {
+ inner: Arc>,
+ pub(crate) offer: ZwlrDataControlOfferV1,
+}
+
+impl DataControlOffer {
+ pub(crate) fn new(offer: &Main) -> Self {
+ let inner = Arc::new(Mutex::new(Inner {
+ mime_types: Vec::new(),
+ }));
+
+ {
+ let inner = inner.clone();
+
+ offer.quick_assign(move |_, event, _| {
+ let mut inner = lock!(inner);
+ if let Event::Offer { mime_type } = event {
+ inner.mime_types.push(mime_type);
+ }
+ });
+ }
+
+ Self {
+ offer: offer.detach(),
+ inner,
+ }
+ }
+
+ pub fn with_mime_types(&self, f: F) -> T
+ where
+ F: FnOnce(&[String]) -> T,
+ {
+ let inner = lock!(self.inner);
+ f(&inner.mime_types)
+ }
+
+ pub fn receive(&self, mime_type: String) -> io::Result {
+ // create a pipe
+ let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
+
+ self.offer.receive(mime_type, writefd);
+
+ if let Err(err) = close(writefd) {
+ warn!("Failed to close write pipe: {}", err);
+ }
+
+ Ok(unsafe { FromRawFd::from_raw_fd(readfd) })
+ }
+}
+
+impl Drop for DataControlOffer {
+ fn drop(&mut self) {
+ self.offer.destroy();
+ }
+}
diff --git a/src/clients/wayland/wlr_data_control/source.rs b/src/clients/wayland/wlr_data_control/source.rs
new file mode 100644
index 0000000..305b46a
--- /dev/null
+++ b/src/clients/wayland/wlr_data_control/source.rs
@@ -0,0 +1,54 @@
+use smithay_client_toolkit::data_device::WritePipe;
+use std::os::fd::FromRawFd;
+use wayland_client::{Attached, DispatchData};
+use wayland_protocols::wlr::unstable::data_control::v1::client::{
+ zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
+ zwlr_data_control_source_v1::{Event, ZwlrDataControlSourceV1},
+};
+
+fn data_control_source_impl(
+ source: &ZwlrDataControlSourceV1,
+ event: Event,
+ implem: &mut F,
+ ddata: DispatchData,
+) where
+ F: FnMut(String, WritePipe, DispatchData),
+{
+ match event {
+ Event::Send { mime_type, fd } => {
+ let pipe = unsafe { FromRawFd::from_raw_fd(fd) };
+ implem(mime_type, pipe, ddata);
+ }
+ Event::Cancelled => source.destroy(),
+ _ => unreachable!(),
+ }
+}
+
+pub struct DataControlSource {
+ pub(crate) source: ZwlrDataControlSourceV1,
+}
+
+impl DataControlSource {
+ pub fn new(
+ manager: &Attached,
+ mime_types: Vec,
+ 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);
+ }
+
+ Self {
+ source: source.detach(),
+ }
+ }
+}
diff --git a/src/clients/wayland/toplevel.rs b/src/clients/wayland/wlr_foreign_toplevel/handle.rs
similarity index 100%
rename from src/clients/wayland/toplevel.rs
rename to src/clients/wayland/wlr_foreign_toplevel/handle.rs
diff --git a/src/clients/wayland/toplevel_manager.rs b/src/clients/wayland/wlr_foreign_toplevel/manager.rs
similarity index 96%
rename from src/clients/wayland/toplevel_manager.rs
rename to src/clients/wayland/wlr_foreign_toplevel/manager.rs
index 4eafb17..af136fa 100644
--- a/src/clients/wayland/toplevel_manager.rs
+++ b/src/clients/wayland/wlr_foreign_toplevel/manager.rs
@@ -1,9 +1,8 @@
-use super::toplevel::{Toplevel, ToplevelEvent};
-use super::LazyGlobal;
+use super::handle::{Toplevel, ToplevelEvent};
+use crate::wayland::LazyGlobal;
use smithay_client_toolkit::environment::{Environment, GlobalHandler};
use std::cell::RefCell;
-use std::rc;
-use std::rc::Rc;
+use std::rc::{self, Rc};
use tracing::warn;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::{Attached, DispatchData};
@@ -155,7 +154,7 @@ impl ToplevelHandling for ToplevelHandler {
}
}
-pub fn listen_for_toplevels(env: Environment, f: F) -> ToplevelStatusListener
+pub fn listen_for_toplevels(env: &Environment, f: F) -> ToplevelStatusListener
where
E: ToplevelHandling,
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
diff --git a/src/clients/wayland/wlr_foreign_toplevel/mod.rs b/src/clients/wayland/wlr_foreign_toplevel/mod.rs
new file mode 100644
index 0000000..edb9691
--- /dev/null
+++ b/src/clients/wayland/wlr_foreign_toplevel/mod.rs
@@ -0,0 +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 manager;
+
+impl ToplevelHandling for Env {
+ fn listen(&mut self, f: F) -> ToplevelStatusListener
+ where
+ F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
+ {
+ self.toplevel.listen(f)
+ }
+}
+
+pub fn update_toplevels(
+ toplevels: &RwLock>,
+ handle: ZwlrForeignToplevelHandleV1,
+ event: ToplevelEvent,
+ tx: &Sender,
+) {
+ trace!("Received toplevel event: {:?}", event);
+
+ if event.change == ToplevelChange::Close {
+ write_lock!(toplevels).remove(&event.toplevel.id);
+ } else {
+ write_lock!(toplevels).insert(event.toplevel.id, (event.toplevel.clone(), handle));
+ }
+
+ send!(tx, event);
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index a742fd0..12373eb 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,6 +1,8 @@
mod r#impl;
mod truncate;
+#[cfg(feature = "clipboard")]
+use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
use crate::modules::clock::ClockModule;
use crate::modules::custom::CustomModule;
@@ -38,19 +40,21 @@ pub struct CommonConfig {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "clock")]
- Clock(ClockModule),
- Custom(CustomModule),
- Focused(FocusedModule),
- Launcher(LauncherModule),
+ Clipboard(Box),
+ #[cfg(feature = "clock")]
+ Clock(Box),
+ Custom(Box),
+ Focused(Box),
+ Launcher(Box),
#[cfg(feature = "music")]
- Music(MusicModule),
- Script(ScriptModule),
+ Music(Box),
+ Script(Box),
#[cfg(feature = "sys_info")]
- SysInfo(SysInfoModule),
+ SysInfo(Box),
#[cfg(feature = "tray")]
- Tray(TrayModule),
+ Tray(Box),
#[cfg(feature = "workspaces")]
- Workspaces(WorkspacesModule),
+ Workspaces(Box),
}
#[derive(Debug, Clone)]
@@ -74,7 +78,7 @@ impl Default for BarPosition {
}
}
-#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
pub struct MarginConfig {
#[serde(default)]
pub bottom: i32,
@@ -86,17 +90,6 @@ pub struct MarginConfig {
pub top: i32,
}
-impl Default for MarginConfig {
- fn default() -> Self {
- MarginConfig {
- bottom: 0,
- left: 0,
- right: 0,
- top: 0,
- }
- }
-}
-
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
diff --git a/src/config/truncate.rs b/src/config/truncate.rs
index ebf8a8c..fed4ffc 100644
--- a/src/config/truncate.rs
+++ b/src/config/truncate.rs
@@ -24,31 +24,43 @@ impl From for GtkEllipsizeMode {
#[serde(untagged)]
pub enum TruncateMode {
Auto(EllipsizeMode),
- MaxLength {
+ Length {
mode: EllipsizeMode,
length: Option,
+ max_length: Option,
},
}
impl TruncateMode {
const fn mode(&self) -> EllipsizeMode {
match self {
- Self::MaxLength { mode, .. } | Self::Auto(mode) => *mode,
+ Self::Length { mode, .. } | Self::Auto(mode) => *mode,
}
}
const fn length(&self) -> Option {
match self {
Self::Auto(_) => None,
- Self::MaxLength { length, .. } => *length,
+ Self::Length { length, .. } => *length,
+ }
+ }
+
+ const fn max_length(&self) -> Option {
+ match self {
+ Self::Auto(_) => None,
+ Self::Length { max_length, .. } => *max_length,
}
}
pub fn truncate_label(&self, label: >k::Label) {
label.set_ellipsize(self.mode().into());
- if let Some(max_length) = self.length() {
- label.set_max_width_chars(max_length);
+ if let Some(length) = self.length() {
+ label.set_width_chars(length);
+ }
+
+ if let Some(length) = self.max_length() {
+ label.set_max_width_chars(length);
}
}
}
diff --git a/src/image/gtk.rs b/src/image/gtk.rs
index 678cb0b..06c62ed 100644
--- a/src/image/gtk.rs
+++ b/src/image/gtk.rs
@@ -3,7 +3,7 @@ use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error;
-#[cfg(any(feature = "music", feature = "workspaces"))]
+#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
let button = Button::new();
diff --git a/src/image/mod.rs b/src/image/mod.rs
index c7e41aa..b7dac37 100644
--- a/src/image/mod.rs
+++ b/src/image/mod.rs
@@ -1,4 +1,4 @@
-#[cfg(any(feature = "music", feature = "workspaces"))]
+#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
mod gtk;
mod provider;
diff --git a/src/image/provider.rs b/src/image/provider.rs
index e70cd65..52be37c 100644
--- a/src/image/provider.rs
+++ b/src/image/provider.rs
@@ -132,16 +132,16 @@ impl<'a> ImageProvider<'a> {
});
}
} else {
- self.load_into_image_sync(image)?;
+ self.load_into_image_sync(&image)?;
};
#[cfg(not(feature = "http"))]
- self.load_into_image_sync(image)?;
+ self.load_into_image_sync(&image)?;
Ok(())
}
- fn load_into_image_sync(&self, image: gtk::Image) -> Result<()> {
+ fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
ImageLocation::Local(path) => self.get_from_file(path),
diff --git a/src/modules/clipboard.rs b/src/modules/clipboard.rs
new file mode 100644
index 0000000..0357228
--- /dev/null
+++ b/src/modules/clipboard.rs
@@ -0,0 +1,315 @@
+use crate::clients::clipboard::{self, ClipboardEvent};
+use crate::clients::wayland::{ClipboardItem, ClipboardValue};
+use crate::config::{CommonConfig, TruncateMode};
+use crate::image::new_icon_button;
+use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
+use crate::popup::Popup;
+use crate::try_send;
+use gtk::gdk_pixbuf::Pixbuf;
+use gtk::gio::{Cancellable, MemoryInputStream};
+use gtk::prelude::*;
+use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::sync::Arc;
+use tokio::spawn;
+use tokio::sync::mpsc::{Receiver, Sender};
+use tracing::{debug, error};
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct ClipboardModule {
+ #[serde(default = "default_icon")]
+ icon: String,
+
+ #[serde(default = "default_max_items")]
+ max_items: usize,
+
+ // -- Common --
+ truncate: Option,
+
+ #[serde(flatten)]
+ pub common: Option,
+}
+
+fn default_icon() -> String {
+ String::from("")
+}
+
+const fn default_max_items() -> usize {
+ 10
+}
+
+#[derive(Debug, Clone)]
+pub enum ControllerEvent {
+ Add(usize, Arc),
+ Remove(usize),
+ Activate(usize),
+ Deactivate,
+}
+
+#[derive(Debug, Clone)]
+pub enum UIEvent {
+ Copy(usize),
+ Remove(usize),
+}
+
+impl Module