feat(clipboard): implement wayland clipboard manager

This commit is contained in:
Federico Terzi 2021-03-16 19:56:00 +01:00 committed by Federico
parent aa64f11950
commit 4038dd0cf3
10 changed files with 294 additions and 21 deletions

13
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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)?))
}

View 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

View 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),
}

View 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;

View File

@ -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;

View File

@ -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)
} }

View File

@ -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]

View File

@ -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();
} }
} }
} }