feat(clipboard): implement wayland clipboard manager
This commit is contained in:
parent
aa64f11950
commit
4038dd0cf3
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -240,13 +240,11 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cc",
|
"cc",
|
||||||
"itertools",
|
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"libc",
|
|
||||||
"log",
|
"log",
|
||||||
"scopeguard",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"wait-timeout",
|
||||||
"widestring",
|
"widestring",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -857,6 +855,15 @@ version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wait-timeout"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.9.0+wasi-snapshot-preview1"
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
|
|
|
@ -8,7 +8,7 @@ build="build.rs"
|
||||||
[features]
|
[features]
|
||||||
# If the wayland feature is enabled, all X11 dependencies will be dropped
|
# If the wayland feature is enabled, all X11 dependencies will be dropped
|
||||||
# and wayland support will be enabled
|
# and wayland support will be enabled
|
||||||
wayland = []
|
wayland = ["wait-timeout"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
@ -20,10 +20,8 @@ lazy_static = "1.4.0"
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
widestring = "0.4.3"
|
widestring = "0.4.3"
|
||||||
|
|
||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libc = "0.2.85"
|
wait-timeout = { version = "0.2.0", optional = true }
|
||||||
scopeguard = "1.1.0"
|
|
||||||
itertools = "0.10.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0.66"
|
cc = "1.0.66"
|
|
@ -29,9 +29,9 @@ mod win32;
|
||||||
#[cfg(not(feature = "wayland"))]
|
#[cfg(not(feature = "wayland"))]
|
||||||
mod x11;
|
mod x11;
|
||||||
|
|
||||||
//#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
//#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
//mod wayland;
|
mod wayland;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod mac;
|
mod mac;
|
||||||
|
@ -45,11 +45,17 @@ pub trait Clipboard {
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct ClipboardOptions {
|
pub struct ClipboardOptions {
|
||||||
|
// Wayland-only
|
||||||
|
// The number of milliseconds the wl-clipboard commands are allowed
|
||||||
|
// to run before triggering a time-out event.
|
||||||
|
wayland_command_timeout_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ClipboardOptions {
|
impl Default for ClipboardOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {}
|
Self {
|
||||||
|
wayland_command_timeout_ms: 2000,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +80,16 @@ pub fn get_clipboard(_: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
|
pub fn get_clipboard(options: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
|
||||||
info!("using EVDEVInjector");
|
// TODO: On some Wayland compositors (currently sway), the "wlr-data-control" protocol
|
||||||
Ok(Box::new(evdev::EVDEVInjector::new(options)?))
|
// could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
|
||||||
|
// Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8
|
||||||
|
//
|
||||||
|
// We could even decide the correct implementation at runtime by checking if the
|
||||||
|
// required protocol is available, if so use the efficient implementation
|
||||||
|
// instead of the fallback one, which calls the wl-copy and wl-paste binaries, and is thus
|
||||||
|
// less efficient
|
||||||
|
|
||||||
|
info!("using WaylandFallbackClipboard");
|
||||||
|
Ok(Box::new(wayland::fallback::WaylandFallbackClipboard::new(options)?))
|
||||||
}
|
}
|
33
espanso-clipboard/src/wayland/README.md
Normal file
33
espanso-clipboard/src/wayland/README.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Notes on Wayland and clipboard support
|
||||||
|
|
||||||
|
### Running espanso as another user
|
||||||
|
|
||||||
|
When running espanso as another user, we need to set up a couple of permissions
|
||||||
|
in order to enable the clipboard tools to correctly connect to the Wayland desktop.
|
||||||
|
|
||||||
|
In particular, we need to add the `espanso` user to the same group as the current user
|
||||||
|
so that it can access the `/run/user/X` directory (with X depending on the user).
|
||||||
|
|
||||||
|
```
|
||||||
|
# Find the current user wayland dir with
|
||||||
|
echo $XDG_RUNTIME_DIR # in my case output: /run/user/1000
|
||||||
|
|
||||||
|
ls -la /run/user/1000
|
||||||
|
|
||||||
|
# Now add the `espanso` user to the current user group
|
||||||
|
sudo usermod -a -G freddy espanso
|
||||||
|
|
||||||
|
# Give permissions to the group
|
||||||
|
chmod g+rwx /run/user/1000
|
||||||
|
|
||||||
|
# Give write permission to the wayland socket
|
||||||
|
chmod g+w /run/user/1000/wayland-0
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the clipboard should work as expected
|
||||||
|
|
||||||
|
## Better implementation
|
||||||
|
|
||||||
|
On some Wayland compositors (currently sway), the "wlr-data-control" protocol could enable the use of a much more efficient implementation relying on the "wl-clipboard-rs" crate.
|
||||||
|
|
||||||
|
Useful links: https://github.com/YaLTeR/wl-clipboard-rs/issues/8
|
191
espanso-clipboard/src/wayland/fallback/mod.rs
Normal file
191
espanso-clipboard/src/wayland/fallback/mod.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
os::unix::net::UnixStream,
|
||||||
|
path::PathBuf,
|
||||||
|
process::Stdio,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Clipboard, ClipboardOptions};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::error;
|
||||||
|
use std::process::Command;
|
||||||
|
use thiserror::Error;
|
||||||
|
use wait_timeout::ChildExt;
|
||||||
|
|
||||||
|
pub(crate) struct WaylandFallbackClipboard {
|
||||||
|
command_timeout: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandFallbackClipboard {
|
||||||
|
pub fn new(options: ClipboardOptions) -> Result<Self> {
|
||||||
|
// Make sure wl-paste and wl-copy are available
|
||||||
|
if Command::new("wl-paste").arg("--version").output().is_err() {
|
||||||
|
error!("unable to call 'wl-paste' binary, please install the wl-clipboard package.");
|
||||||
|
return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
|
||||||
|
}
|
||||||
|
if Command::new("wl-copy").arg("--version").output().is_err() {
|
||||||
|
error!("unable to call 'wl-copy' binary, please install the wl-clipboard package.");
|
||||||
|
return Err(WaylandFallbackClipboardError::MissingWLClipboard().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to connect to the wayland display
|
||||||
|
let wayland_socket = if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||||
|
PathBuf::from(runtime_dir).join("wayland-0")
|
||||||
|
} else {
|
||||||
|
error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard");
|
||||||
|
return Err(WaylandFallbackClipboardError::MissingEnvVariable().into());
|
||||||
|
};
|
||||||
|
if UnixStream::connect(wayland_socket).is_err() {
|
||||||
|
error!("failed to connect to Wayland display");
|
||||||
|
return Err(WaylandFallbackClipboardError::ConnectionFailed().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
command_timeout: options.wayland_command_timeout_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clipboard for WaylandFallbackClipboard {
|
||||||
|
fn get_text(&self) -> Option<String> {
|
||||||
|
let timeout = std::time::Duration::from_millis(self.command_timeout);
|
||||||
|
match Command::new("wl-paste")
|
||||||
|
.arg("--no-newline")
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(mut child) => match child.wait_timeout(timeout) {
|
||||||
|
Ok(status_code) => {
|
||||||
|
if let Some(status) = status_code {
|
||||||
|
if status.success() {
|
||||||
|
if let Some(mut io) = child.stdout {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
io.read_to_end(&mut output).ok()?;
|
||||||
|
Some(String::from_utf8_lossy(&output).to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("error, wl-paste exited with non-zero exit code");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("error, wl-paste has timed-out, killing the process");
|
||||||
|
if child.kill().is_err() {
|
||||||
|
error!("unable to kill wl-paste");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("error while executing 'wl-paste': {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("could not invoke 'wl-paste': {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_text(&self, text: &str) -> anyhow::Result<()> {
|
||||||
|
self.invoke_command_with_timeout(&mut Command::new("wl-copy"), text.as_bytes(), "wl-copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> {
|
||||||
|
if !image_path.exists() || !image_path.is_file() {
|
||||||
|
return Err(WaylandFallbackClipboardError::ImageNotFound(image_path.to_path_buf()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the image data
|
||||||
|
let mut file = std::fs::File::open(image_path)?;
|
||||||
|
let mut data = Vec::new();
|
||||||
|
file.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
self.invoke_command_with_timeout(&mut Command::new("wl-copy").arg("--type").arg("image/png"), &data, "wl-copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_html(&self, html: &str, _fallback_text: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
self.invoke_command_with_timeout(&mut Command::new("wl-copy").arg("--type").arg("text/html"), html.as_bytes(), "wl-copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandFallbackClipboard {
|
||||||
|
fn invoke_command_with_timeout(&self, command: &mut Command, data: &[u8], name: &str) -> Result<()> {
|
||||||
|
let timeout = std::time::Duration::from_millis(self.command_timeout);
|
||||||
|
match command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(mut child) => {
|
||||||
|
if let Some(stdin) = child.stdin.as_mut() {
|
||||||
|
stdin.write_all(data)?;
|
||||||
|
}
|
||||||
|
match child.wait_timeout(timeout) {
|
||||||
|
Ok(status_code) => {
|
||||||
|
if let Some(status) = status_code {
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
error!("error, {} exited with non-zero exit code", name);
|
||||||
|
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("error, {} has timed-out, killing the process", name);
|
||||||
|
if child.kill().is_err() {
|
||||||
|
error!("unable to kill {}", name);
|
||||||
|
}
|
||||||
|
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("error while executing '{}': {}", name, err);
|
||||||
|
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("could not invoke '{}': {}", name, err);
|
||||||
|
Err(WaylandFallbackClipboardError::SetOperationFailed().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum WaylandFallbackClipboardError {
|
||||||
|
#[error("wl-clipboard binaries are missing")]
|
||||||
|
MissingWLClipboard(),
|
||||||
|
|
||||||
|
#[error("missing XDG_RUNTIME_DIR env variable")]
|
||||||
|
MissingEnvVariable(),
|
||||||
|
|
||||||
|
#[error("can't connect to Wayland display")]
|
||||||
|
ConnectionFailed(),
|
||||||
|
|
||||||
|
#[error("clipboard set operation failed")]
|
||||||
|
SetOperationFailed(),
|
||||||
|
|
||||||
|
#[error("image not found: `{0}`")]
|
||||||
|
ImageNotFound(PathBuf),
|
||||||
|
}
|
20
espanso-clipboard/src/wayland/mod.rs
Normal file
20
espanso-clipboard/src/wayland/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub(crate) mod fallback;
|
|
@ -17,4 +17,4 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub mod native;
|
pub(crate) mod native;
|
|
@ -73,18 +73,22 @@ impl HotKey {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn has_ctrl(&self) -> bool {
|
pub(crate) fn has_ctrl(&self) -> bool {
|
||||||
self.modifiers.contains(&ShortcutKey::Control)
|
self.modifiers.contains(&ShortcutKey::Control)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn has_meta(&self) -> bool {
|
pub(crate) fn has_meta(&self) -> bool {
|
||||||
self.modifiers.contains(&ShortcutKey::Meta)
|
self.modifiers.contains(&ShortcutKey::Meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn has_alt(&self) -> bool {
|
pub(crate) fn has_alt(&self) -> bool {
|
||||||
self.modifiers.contains(&ShortcutKey::Alt)
|
self.modifiers.contains(&ShortcutKey::Alt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn has_shift(&self) -> bool {
|
pub(crate) fn has_shift(&self) -> bool {
|
||||||
self.modifiers.contains(&ShortcutKey::Shift)
|
self.modifiers.contains(&ShortcutKey::Shift)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ edition = "2018"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# If the wayland feature is enabled, all X11 dependencies will be dropped
|
# If the wayland feature is enabled, all X11 dependencies will be dropped
|
||||||
# and only EVDEV-based methods will be supported.
|
# and only methods suitable for Wayland will be used
|
||||||
wayland = ["espanso-detect/wayland", "espanso-inject/wayland", "espanso-clipboard/wayland"]
|
wayland = ["espanso-detect/wayland", "espanso-inject/wayland", "espanso-clipboard/wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -12,6 +12,7 @@ use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!z");
|
println!("Hello, world!z");
|
||||||
|
|
||||||
CombinedLogger::init(vec![
|
CombinedLogger::init(vec![
|
||||||
TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
|
TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
|
||||||
// WriteLogger::new(
|
// WriteLogger::new(
|
||||||
|
@ -22,6 +23,9 @@ fn main() {
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let clipboard = espanso_clipboard::get_clipboard(Default::default()).unwrap();
|
||||||
|
println!("clipboard: {:?}", clipboard.get_text());
|
||||||
|
|
||||||
// let icon_paths = vec![
|
// let icon_paths = vec![
|
||||||
// (
|
// (
|
||||||
// espanso_ui::icons::TrayIcon::Normal,
|
// espanso_ui::icons::TrayIcon::Normal,
|
||||||
|
@ -68,7 +72,8 @@ fn main() {
|
||||||
let mut source = get_source(SourceCreationOptions {
|
let mut source = get_source(SourceCreationOptions {
|
||||||
//use_evdev: true,
|
//use_evdev: true,
|
||||||
hotkeys: vec![
|
hotkeys: vec![
|
||||||
HotKey::new(1, "OPTION+SPACE").unwrap(),
|
HotKey::new(1, "CTRL+SPACE").unwrap(),
|
||||||
|
//HotKey::new(1, "OPTION+SPACE").unwrap(),
|
||||||
HotKey::new(2, "CTRL+OPTION+3").unwrap(),
|
HotKey::new(2, "CTRL+OPTION+3").unwrap(),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -96,9 +101,9 @@ fn main() {
|
||||||
if hotkey.hotkey_id == 2 {
|
if hotkey.hotkey_id == 2 {
|
||||||
println!("clip {:?}", clipboard.get_text());
|
println!("clip {:?}", clipboard.get_text());
|
||||||
} else if hotkey.hotkey_id == 1 {
|
} else if hotkey.hotkey_id == 1 {
|
||||||
//clipboard.set_text("test text").unwrap();
|
clipboard.set_text("test text").unwrap();
|
||||||
//clipboard.set_html("<i>test text</i>", Some("test text fallback")).unwrap();
|
//clipboard.set_html("<i>test text</i>", Some("test text fallback")).unwrap();
|
||||||
clipboard.set_image(&PathBuf::from("/home/freddy/insync/Development/Espanso/Images/icongreen.png")).unwrap();
|
//clipboard.set_image(&PathBuf::from("/home/freddy/insync/Development/Espanso/Images/icongreen.png")).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user