mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +02:00
feat: new clipboard manager module
This commit is contained in:
parent
5bbe64bb86
commit
575d6cc30f
26 changed files with 1809 additions and 149 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
regex = { version = "1.6.0", default-features = false, features = ["std"], optional = true } # music, sys_info
|
||||
|
|
|
@ -61,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. |
|
||||
|
|
93
docs/modules/Clipboard.md
Normal file
93
docs/modules/Clipboard.md
Normal file
|
@ -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.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"end": {
|
||||
"type": "clipboard",
|
||||
"max_items": 3,
|
||||
"truncate": {
|
||||
"mode": "end",
|
||||
"length": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TOML</summary>
|
||||
|
||||
```toml
|
||||
[[end]]
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
|
||||
[[end.truncate]]
|
||||
mode = "end"
|
||||
length = 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>YAML</summary>
|
||||
|
||||
```yaml
|
||||
end:
|
||||
- type: 'clipboard'
|
||||
max_items: 3
|
||||
truncate:
|
||||
mode: 'end'
|
||||
length: 50
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Corn</summary>
|
||||
|
||||
```corn
|
||||
{
|
||||
end = [ {
|
||||
type = "clipboard"
|
||||
max_items = 3
|
||||
truncate.mode = "end"
|
||||
truncate.length = 50
|
||||
} ]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## 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. |
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
20
src/bar.rs
20
src/bar.rs
|
@ -193,7 +193,7 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, 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<ModuleConfig>, 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<TSend>(
|
|||
) 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<TSend>(
|
|||
} 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<TSend>(
|
|||
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);
|
||||
|
|
245
src/clients/clipboard.rs
Normal file
245
src/clients/clipboard.rs
Normal file
|
@ -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<ClipboardItem>),
|
||||
Remove(usize),
|
||||
Activate(usize),
|
||||
}
|
||||
|
||||
type EventSender = mpsc::Sender<ClipboardEvent>;
|
||||
|
||||
/// Clipboard client singleton,
|
||||
/// to ensure bars don't duplicate requests to the compositor.
|
||||
pub struct ClipboardClient {
|
||||
senders: Arc<Mutex<Vec<(EventSender, usize)>>>,
|
||||
cache: Arc<Mutex<ClipboardCache>>,
|
||||
}
|
||||
|
||||
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<ClipboardEvent> {
|
||||
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, (Arc<ClipboardItem>, 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<Arc<ClipboardItem>> {
|
||||
self.cache.get(&id).map(|(item, _)| item).cloned()
|
||||
}
|
||||
|
||||
/// Inserts an entry with `ref_count` initial references.
|
||||
fn insert(&mut self, item: Arc<ClipboardItem>, ref_count: usize) -> Option<Arc<ClipboardItem>> {
|
||||
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<ClipboardItem>) {
|
||||
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<Arc<ClipboardItem>> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<ClipboardItem>, usize)> {
|
||||
self.cache.iter()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: ClipboardClient = ClipboardClient::new();
|
||||
}
|
||||
|
||||
pub fn get_client() -> &'static ClipboardClient {
|
||||
&CLIENT
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "clipboard")]
|
||||
pub mod clipboard;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod compositor;
|
||||
#[cfg(feature = "music")]
|
||||
|
|
|
@ -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<ClipboardItem>),
|
||||
/// Forces a dispatch, flushing any currently queued events
|
||||
Refresh,
|
||||
}
|
||||
|
||||
pub struct WaylandClient {
|
||||
pub outputs: Vec<OutputInfo>,
|
||||
pub seats: Vec<WlSeat>,
|
||||
|
||||
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard_tx: broadcast::Sender<Arc<ClipboardItem>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Arc<RwLock<Option<Arc<ClipboardItem>>>>,
|
||||
|
||||
request_tx: Sender<Request>,
|
||||
}
|
||||
|
||||
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::<Request>();
|
||||
|
||||
// `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::<DData>::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::<Vec<WlSeat>>()
|
||||
);
|
||||
|
||||
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::<ZwlrForeignToplevelManagerV1>();
|
||||
|
||||
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<Arc<ClipboardItem>> {
|
||||
self.clipboard_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn roundtrip(&self) {
|
||||
send!(self.request_tx, Request::Refresh);
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn get_clipboard(&self) -> Option<Arc<ClipboardItem>> {
|
||||
let clipboard = read_lock!(self.clipboard);
|
||||
clipboard.as_ref().cloned()
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
pub fn copy_to_clipboard(&self, item: Arc<ClipboardItem>) {
|
||||
send!(self.request_tx, Request::CopyToClipboard(item));
|
||||
}
|
||||
|
||||
fn get_outputs(env: &Environment<Env>) -> Vec<OutputInfo> {
|
||||
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<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}");
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<I: Interface> {
|
|||
Bound(Attached<I>),
|
||||
}
|
||||
|
||||
sctk::default_environment!(Env,
|
||||
fields = [
|
||||
toplevel: ToplevelHandler
|
||||
],
|
||||
singles = [
|
||||
ZwlrForeignToplevelManagerV1 => toplevel
|
||||
],
|
||||
);
|
||||
pub struct DData {
|
||||
env: Environment<Env>,
|
||||
offer_tokens: HashMap<u128, RegistrationToken>,
|
||||
}
|
||||
|
||||
impl ToplevelHandling for Env {
|
||||
fn listen<F>(&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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
88
src/clients/wayland/wlr_data_control/device.rs
Normal file
88
src/clients/wayland/wlr_data_control/device.rs
Normal file
|
@ -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<Arc<DataControlOffer>>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn new_offer(&mut self, offer: &Main<ZwlrDataControlOfferV1>) {
|
||||
self.offer.replace(Arc::new(DataControlOffer::new(offer)));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataControlDeviceEvent(pub Arc<DataControlOffer>);
|
||||
|
||||
fn data_control_device_implem<F>(
|
||||
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<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));
|
||||
}
|
||||
}
|
253
src/clients/wayland/wlr_data_control/manager.rs
Normal file
253
src/clients/wayland/wlr_data_control/manager.rs
Normal file
|
@ -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<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 DataControlDeviceHandlerInner {
|
||||
fn init_manager(&mut self, manager: Attached<ZwlrDataControlManagerV1>) {
|
||||
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<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DataControlDeviceHandler {
|
||||
inner: Rc<RefCell<DataControlDeviceHandlerInner>>,
|
||||
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 GlobalHandler<ZwlrDataControlManagerV1> for DataControlDeviceHandler {
|
||||
fn created(
|
||||
&mut self,
|
||||
registry: Attached<WlRegistry>,
|
||||
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::<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))
|
||||
}
|
259
src/clients/wayland/wlr_data_control/mod.rs
Normal file
259
src/clients/wayland/wlr_data_control/mod.rs
Normal file
|
@ -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<Self> 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<F>(&mut self, f: F) -> DataControlDeviceStatusListener
|
||||
where
|
||||
F: FnMut(WlSeat, DataControlDeviceEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.data_control_device.listen(f)
|
||||
}
|
||||
|
||||
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)]
|
||||
struct MimeType {
|
||||
value: String,
|
||||
category: MimeTypeCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MimeTypeCategory {
|
||||
Text,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
fn parse(mime_types: &[String]) -> Option<Self> {
|
||||
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<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();
|
||||
|
||||
let offer = event.0;
|
||||
|
||||
let ddata = ddata
|
||||
.get::<DData>()
|
||||
.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<ClipboardItem> {
|
||||
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(),
|
||||
})
|
||||
}
|
74
src/clients/wayland/wlr_data_control/offer.rs
Normal file
74
src/clients/wayland/wlr_data_control/offer.rs
Normal file
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataControlOffer {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
pub(crate) offer: ZwlrDataControlOfferV1,
|
||||
}
|
||||
|
||||
impl DataControlOffer {
|
||||
pub(crate) fn new(offer: &Main<ZwlrDataControlOfferV1>) -> 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<F, T>(&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<ReadPipe> {
|
||||
// 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();
|
||||
}
|
||||
}
|
54
src/clients/wayland/wlr_data_control/source.rs
Normal file
54
src/clients/wayland/wlr_data_control/source.rs
Normal file
|
@ -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<F>(
|
||||
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<F>(
|
||||
manager: &Attached<ZwlrDataControlManagerV1>,
|
||||
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);
|
||||
}
|
||||
|
||||
Self {
|
||||
source: source.detach(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<E, F>(env: Environment<E>, f: F) -> ToplevelStatusListener
|
||||
pub fn listen_for_toplevels<E, F>(env: &Environment<E>, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
E: ToplevelHandling,
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
39
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
39
src/clients/wayland/wlr_foreign_toplevel/mod.rs
Normal file
|
@ -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<F>(&mut self, f: F) -> ToplevelStatusListener
|
||||
where
|
||||
F: FnMut(ZwlrForeignToplevelHandleV1, ToplevelEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.toplevel.listen(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_toplevels(
|
||||
toplevels: &RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>,
|
||||
handle: ZwlrForeignToplevelHandleV1,
|
||||
event: ToplevelEvent,
|
||||
tx: &Sender<ToplevelEvent>,
|
||||
) {
|
||||
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);
|
||||
}
|
|
@ -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<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
Custom(Box<CustomModule>),
|
||||
Focused(Box<FocusedModule>),
|
||||
Launcher(Box<LauncherModule>),
|
||||
#[cfg(feature = "music")]
|
||||
Music(MusicModule),
|
||||
Script(ScriptModule),
|
||||
Music(Box<MusicModule>),
|
||||
Script(Box<ScriptModule>),
|
||||
#[cfg(feature = "sys_info")]
|
||||
SysInfo(SysInfoModule),
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
#[cfg(feature = "tray")]
|
||||
Tray(TrayModule),
|
||||
Tray(Box<TrayModule>),
|
||||
#[cfg(feature = "workspaces")]
|
||||
Workspaces(WorkspacesModule),
|
||||
Workspaces(Box<WorkspacesModule>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
mod gtk;
|
||||
mod provider;
|
||||
|
||||
|
|
315
src/modules/clipboard.rs
Normal file
315
src/modules/clipboard.rs
Normal file
|
@ -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<TruncateMode>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
fn default_icon() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
const fn default_max_items() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ControllerEvent {
|
||||
Add(usize, Arc<ClipboardItem>),
|
||||
Remove(usize),
|
||||
Activate(usize),
|
||||
Deactivate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UIEvent {
|
||||
Copy(usize),
|
||||
Remove(usize),
|
||||
}
|
||||
|
||||
impl Module<Button> for ClipboardModule {
|
||||
type SendMessage = ControllerEvent;
|
||||
type ReceiveMessage = UIEvent;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"clipboard"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> color_eyre::Result<()> {
|
||||
let max_items = self.max_items;
|
||||
|
||||
// listen to clipboard events
|
||||
spawn(async move {
|
||||
let mut rx = {
|
||||
let client = clipboard::get_client();
|
||||
client.subscribe(max_items).await
|
||||
};
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
ClipboardEvent::Add(item) => {
|
||||
let msg = match &item.value {
|
||||
ClipboardValue::Other => {
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
|
||||
}
|
||||
_ => ModuleUpdateEvent::Update(ControllerEvent::Add(item.id, item)),
|
||||
};
|
||||
try_send!(tx, msg);
|
||||
}
|
||||
ClipboardEvent::Remove(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Remove(id)));
|
||||
}
|
||||
ClipboardEvent::Activate(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Activate(id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("Clipboard client unexpectedly closed");
|
||||
});
|
||||
|
||||
// listen to ui events
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let client = clipboard::get_client();
|
||||
match event {
|
||||
UIEvent::Copy(id) => client.copy(id).await,
|
||||
UIEvent::Remove(id) => client.remove(id),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> color_eyre::Result<ModuleWidget<Button>> {
|
||||
let position = info.bar_position;
|
||||
|
||||
let button = new_icon_button(&self.icon, info.icon_theme, 32);
|
||||
button.style_context().add_class("btn");
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
let pos = Popup::button_pos(button, position.get_orientation());
|
||||
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
||||
});
|
||||
|
||||
// we need to bind to the receiver as the channel does not open
|
||||
// until the popup is first opened.
|
||||
context.widget_rx.attach(None, |_| Continue(true));
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: button,
|
||||
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
rx: glib::Receiver<Self::SendMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(10)
|
||||
.name("popup-clipboard")
|
||||
.build();
|
||||
|
||||
let entries = gtk::Box::new(Orientation::Vertical, 5);
|
||||
container.add(&entries);
|
||||
|
||||
let hidden_option = RadioButton::new();
|
||||
entries.add(&hidden_option);
|
||||
|
||||
let mut items = HashMap::new();
|
||||
|
||||
{
|
||||
let hidden_option = hidden_option.clone();
|
||||
rx.attach(None, move |event| {
|
||||
match event {
|
||||
ControllerEvent::Add(id, item) => {
|
||||
debug!("Adding new value with ID {}", id);
|
||||
|
||||
let row = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
row.style_context().add_class("item");
|
||||
|
||||
let button = match &item.value {
|
||||
ClipboardValue::Text(value) => {
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
|
||||
let label = Label::new(Some(value));
|
||||
button.add(&label);
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
}
|
||||
|
||||
button.style_context().add_class("text");
|
||||
button
|
||||
}
|
||||
ClipboardValue::Image(bytes) => {
|
||||
let stream = MemoryInputStream::from_bytes(bytes);
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
128,
|
||||
64,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
)
|
||||
.expect("Failed to read Pixbuf from stream");
|
||||
let image = Image::from_pixbuf(Some(&pixbuf));
|
||||
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
button.style_context().add_class("image");
|
||||
|
||||
button
|
||||
}
|
||||
ClipboardValue::Other => unreachable!(),
|
||||
};
|
||||
|
||||
button.style_context().add_class("btn");
|
||||
button.set_active(true); // if just added, should be on clipboard
|
||||
|
||||
let button_wrapper = EventBox::new();
|
||||
button_wrapper.add(&button);
|
||||
|
||||
button_wrapper.set_widget_name(&format!("copy-{id}"));
|
||||
button_wrapper.set_above_child(true);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
button_wrapper.connect_button_press_event(
|
||||
move |button_wrapper, event| {
|
||||
// left click
|
||||
if event.button() == 1 {
|
||||
let id = get_button_id(button_wrapper)
|
||||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Copying item with id: {id}");
|
||||
try_send!(tx, UIEvent::Copy(id));
|
||||
}
|
||||
|
||||
Inhibit(true)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let remove_button = Button::with_label("x");
|
||||
remove_button.set_widget_name(&format!("remove-{id}"));
|
||||
remove_button.style_context().add_class("btn-remove");
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let entries = entries.clone();
|
||||
let row = row.clone();
|
||||
|
||||
remove_button.connect_clicked(move |button| {
|
||||
let id = get_button_id(button)
|
||||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Removing item with id: {id}");
|
||||
try_send!(tx, UIEvent::Remove(id));
|
||||
|
||||
entries.remove(&row);
|
||||
});
|
||||
}
|
||||
|
||||
row.add(&button_wrapper);
|
||||
row.pack_end(&remove_button, false, false, 0);
|
||||
|
||||
entries.add(&row);
|
||||
entries.reorder_child(&row, 0);
|
||||
row.show_all();
|
||||
|
||||
items.insert(id, (row, button));
|
||||
}
|
||||
ControllerEvent::Remove(id) => {
|
||||
debug!("Removing option with ID {id}");
|
||||
let row = items.remove(&id);
|
||||
if let Some((row, button)) = row {
|
||||
if button.is_active() {
|
||||
hidden_option.set_active(true);
|
||||
}
|
||||
|
||||
entries.remove(&row);
|
||||
}
|
||||
}
|
||||
ControllerEvent::Activate(id) => {
|
||||
debug!("Activating option with ID {id}");
|
||||
|
||||
hidden_option.set_active(false);
|
||||
let row = items.get(&id);
|
||||
if let Some((_, button)) = row {
|
||||
button.set_active(true);
|
||||
}
|
||||
}
|
||||
ControllerEvent::Deactivate => {
|
||||
debug!("Deactivating current option");
|
||||
hidden_option.set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
hidden_option.hide();
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the ID from a widget's name.
|
||||
///
|
||||
/// This expects the button name to be
|
||||
/// in the format `<purpose>-<id>`.
|
||||
fn get_button_id<W>(button_wrapper: &W) -> Option<usize>
|
||||
where
|
||||
W: IsA<Widget>,
|
||||
{
|
||||
button_wrapper
|
||||
.widget_name()
|
||||
.split_once('-')
|
||||
.and_then(|(_, id)| id.parse().ok())
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "clipboard")]
|
||||
pub mod clipboard;
|
||||
/// Displays the current date and time.
|
||||
///
|
||||
/// A custom date/time format string can be set in the config.
|
||||
|
|
Loading…
Add table
Reference in a new issue