/* * 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 . */ use std::{ io::{Read, Write}, os::unix::net::UnixStream, path::PathBuf, process::Stdio, }; use crate::{Clipboard, ClipboardOperationOptions, ClipboardOptions}; use anyhow::Result; use log::{error, warn}; 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 { // 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") { let wayland_display = if let Ok(display) = std::env::var("WAYLAND_DISPLAY") { display } else { warn!("Could not determine wayland display from WAYLAND_DISPLAY env variable, falling back to 'wayland-0'"); warn!("Note that this might not work on some systems."); "wayland-0".to_string() }; PathBuf::from(runtime_dir).join(wayland_display) } 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, _: &ClipboardOperationOptions) -> Option { 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, _: &ClipboardOperationOptions) -> 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, _: &ClipboardOperationOptions, ) -> 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>, _: &ClipboardOperationOptions, ) -> 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), }