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 = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"libc",
|
||||
"log",
|
||||
"scopeguard",
|
||||
"thiserror",
|
||||
"wait-timeout",
|
||||
"widestring",
|
||||
]
|
||||
|
||||
|
@ -857,6 +855,15 @@ version = "0.9.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
|
|
|
@ -8,7 +8,7 @@ build="build.rs"
|
|||
[features]
|
||||
# If the wayland feature is enabled, all X11 dependencies will be dropped
|
||||
# and wayland support will be enabled
|
||||
wayland = []
|
||||
wayland = ["wait-timeout"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.14"
|
||||
|
@ -20,10 +20,8 @@ lazy_static = "1.4.0"
|
|||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "0.4.3"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
libc = "0.2.85"
|
||||
scopeguard = "1.1.0"
|
||||
itertools = "0.10.0"
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
wait-timeout = { version = "0.2.0", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.66"
|
|
@ -29,9 +29,9 @@ mod win32;
|
|||
#[cfg(not(feature = "wayland"))]
|
||||
mod x11;
|
||||
|
||||
//#[cfg(target_os = "linux")]
|
||||
//#[cfg(feature = "wayland")]
|
||||
//mod wayland;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(feature = "wayland")]
|
||||
mod wayland;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
@ -45,11 +45,17 @@ pub trait Clipboard {
|
|||
|
||||
#[allow(dead_code)]
|
||||
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 {
|
||||
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(feature = "wayland")]
|
||||
pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
|
||||
info!("using EVDEVInjector");
|
||||
Ok(Box::new(evdev::EVDEVInjector::new(options)?))
|
||||
}
|
||||
pub fn get_clipboard(options: ClipboardOptions) -> Result<Box<dyn Clipboard>> {
|
||||
// TODO: 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
|
||||
//
|
||||
// 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/>.
|
||||
*/
|
||||
|
||||
pub mod native;
|
||||
pub(crate) mod native;
|
|
@ -73,18 +73,22 @@ impl HotKey {
|
|||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn has_ctrl(&self) -> bool {
|
||||
self.modifiers.contains(&ShortcutKey::Control)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn has_meta(&self) -> bool {
|
||||
self.modifiers.contains(&ShortcutKey::Meta)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn has_alt(&self) -> bool {
|
||||
self.modifiers.contains(&ShortcutKey::Alt)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn has_shift(&self) -> bool {
|
||||
self.modifiers.contains(&ShortcutKey::Shift)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ edition = "2018"
|
|||
|
||||
[features]
|
||||
# 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"]
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -12,6 +12,7 @@ use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
|||
|
||||
fn main() {
|
||||
println!("Hello, world!z");
|
||||
|
||||
CombinedLogger::init(vec![
|
||||
TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
|
||||
// WriteLogger::new(
|
||||
|
@ -22,6 +23,9 @@ fn main() {
|
|||
])
|
||||
.unwrap();
|
||||
|
||||
let clipboard = espanso_clipboard::get_clipboard(Default::default()).unwrap();
|
||||
println!("clipboard: {:?}", clipboard.get_text());
|
||||
|
||||
// let icon_paths = vec![
|
||||
// (
|
||||
// espanso_ui::icons::TrayIcon::Normal,
|
||||
|
@ -68,7 +72,8 @@ fn main() {
|
|||
let mut source = get_source(SourceCreationOptions {
|
||||
//use_evdev: true,
|
||||
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(),
|
||||
],
|
||||
..Default::default()
|
||||
|
@ -96,9 +101,9 @@ fn main() {
|
|||
if hotkey.hotkey_id == 2 {
|
||||
println!("clip {:?}", clipboard.get_text());
|
||||
} 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_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