Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/clipboard.rs')
| -rw-r--r-- | helix-view/src/clipboard.rs | 754 |
1 files changed, 420 insertions, 334 deletions
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index 379accc7..234b91fe 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -1,356 +1,224 @@ // Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152 -use anyhow::Result; +use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use thiserror::Error; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy)] pub enum ClipboardType { Clipboard, Selection, } -pub trait ClipboardProvider: std::fmt::Debug { - fn name(&self) -> Cow<str>; - fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>; - fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>; +#[derive(Debug, Error)] +pub enum ClipboardError { + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error("could not convert terminal output to UTF-8: {0}")] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[cfg(windows)] + #[error("Windows API error: {0}")] + WinAPI(#[from] clipboard_win::ErrorCode), + #[error("clipboard provider command failed")] + CommandFailed, + #[error("failed to write to clipboard provider's stdin")] + StdinWriteFailed, + #[error("clipboard provider did not return any contents")] + MissingStdout, + #[error("This clipboard provider does not support reading")] + ReadingNotSupported, } -#[cfg(not(windows))] -macro_rules! command_provider { - (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{ - log::debug!( - "Using {} to interact with the system clipboard", - if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() } - ); - Box::new(provider::command::Provider { - get_cmd: provider::command::Config { - prg: $get_prg, - args: &[ $( $get_arg ),* ], - }, - set_cmd: provider::command::Config { - prg: $set_prg, - args: &[ $( $set_arg ),* ], - }, - get_primary_cmd: None, - set_primary_cmd: None, - }) - }}; - - (paste => $get_prg:literal $( , $get_arg:literal )* ; - copy => $set_prg:literal $( , $set_arg:literal )* ; - primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ; - primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ; - ) => {{ - log::debug!( - "Using {} to interact with the system and selection (primary) clipboard", - if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() } - ); - Box::new(provider::command::Provider { - get_cmd: provider::command::Config { - prg: $get_prg, - args: &[ $( $get_arg ),* ], - }, - set_cmd: provider::command::Config { - prg: $set_prg, - args: &[ $( $set_arg ),* ], - }, - get_primary_cmd: Some(provider::command::Config { - prg: $pr_get_prg, - args: &[ $( $pr_get_arg ),* ], - }), - set_primary_cmd: Some(provider::command::Config { - prg: $pr_set_prg, - args: &[ $( $pr_set_arg ),* ], - }), - }) - }}; -} - -#[cfg(windows)] -pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { - Box::<provider::WindowsProvider>::default() -} - -#[cfg(target_os = "macos")] -pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { - use helix_stdx::env::{binary_exists, env_var_is_set}; - - if env_var_is_set("TMUX") && binary_exists("tmux") { - command_provider! { - paste => "tmux", "save-buffer", "-"; - copy => "tmux", "load-buffer", "-w", "-"; - } - } else if binary_exists("pbcopy") && binary_exists("pbpaste") { - command_provider! { - paste => "pbpaste"; - copy => "pbcopy"; - } - } else { - Box::new(provider::FallbackProvider::new()) - } -} +type Result<T> = std::result::Result<T, ClipboardError>; +#[cfg(not(target_arch = "wasm32"))] +pub use external::ClipboardProvider; #[cfg(target_arch = "wasm32")] -pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { - // TODO: - Box::new(provider::FallbackProvider::new()) -} +pub use noop::ClipboardProvider; -#[cfg(not(any(windows, target_arch = "wasm32", target_os = "macos")))] -pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { - use helix_stdx::env::{binary_exists, env_var_is_set}; - use provider::command::is_exit_success; - // TODO: support for user-defined provider, probably when we have plugin support by setting a - // variable? - - if env_var_is_set("WAYLAND_DISPLAY") && binary_exists("wl-copy") && binary_exists("wl-paste") { - command_provider! { - paste => "wl-paste", "--no-newline"; - copy => "wl-copy", "--type", "text/plain"; - primary_paste => "wl-paste", "-p", "--no-newline"; - primary_copy => "wl-copy", "-p", "--type", "text/plain"; - } - } else if env_var_is_set("DISPLAY") && binary_exists("xclip") { - command_provider! { - paste => "xclip", "-o", "-selection", "clipboard"; - copy => "xclip", "-i", "-selection", "clipboard"; - primary_paste => "xclip", "-o"; - primary_copy => "xclip", "-i"; - } - } else if env_var_is_set("DISPLAY") - && binary_exists("xsel") - && is_exit_success("xsel", &["-o", "-b"]) - { - // FIXME: check performance of is_exit_success - command_provider! { - paste => "xsel", "-o", "-b"; - copy => "xsel", "-i", "-b"; - primary_paste => "xsel", "-o"; - primary_copy => "xsel", "-i"; - } - } else if binary_exists("win32yank.exe") { - command_provider! { - paste => "win32yank.exe", "-o", "--lf"; - copy => "win32yank.exe", "-i", "--crlf"; - } - } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") { - command_provider! { - paste => "termux-clipboard-get"; - copy => "termux-clipboard-set"; - } - } else if env_var_is_set("TMUX") && binary_exists("tmux") { - command_provider! { - paste => "tmux", "save-buffer", "-"; - copy => "tmux", "load-buffer", "-w", "-"; - } - } else { - Box::new(provider::FallbackProvider::new()) - } -} +// Clipboard not supported for wasm +#[cfg(target_arch = "wasm32")] +mod noop { + use super::*; -#[cfg(not(target_os = "windows"))] -pub mod provider { - use super::{ClipboardProvider, ClipboardType}; - use anyhow::Result; - use std::borrow::Cow; + #[derive(Debug, Clone)] + pub enum ClipboardProvider {} - #[cfg(feature = "term")] - mod osc52 { - use {super::ClipboardType, crate::base64}; + impl ClipboardProvider { + pub fn detect() -> Self { + Self + } - #[derive(Debug)] - pub struct SetClipboardCommand { - encoded_content: String, - clipboard_type: ClipboardType, + pub fn name(&self) -> Cow<str> { + "none".into() } - impl SetClipboardCommand { - pub fn new(content: &str, clipboard_type: ClipboardType) -> Self { - Self { - encoded_content: base64::encode(content.as_bytes()), - clipboard_type, - } - } + pub fn get_contents(&self, _clipboard_type: ClipboardType) -> Result<String> { + Err(ClipboardError::ReadingNotSupported) } - impl crossterm::Command for SetClipboardCommand { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - let kind = match &self.clipboard_type { - ClipboardType::Clipboard => "c", - ClipboardType::Selection => "p", - }; - // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/ - write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content) - } + pub fn set_contents(&self, _content: &str, _clipboard_type: ClipboardType) -> Result<()> { + Ok(()) } } +} - #[derive(Debug)] - pub struct FallbackProvider { - buf: String, - primary_buf: String, - } +#[cfg(not(target_arch = "wasm32"))] +mod external { + use super::*; - impl FallbackProvider { - pub fn new() -> Self { - #[cfg(feature = "term")] - log::debug!( - "No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix" - ); - #[cfg(not(feature = "term"))] - log::warn!( - "No native clipboard provider found! Yanking and pasting will be internal to Helix" - ); - Self { - buf: String::new(), - primary_buf: String::new(), - } - } + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct Command { + command: Cow<'static, str>, + #[serde(default)] + args: Cow<'static, [Cow<'static, str>]>, } - impl Default for FallbackProvider { - fn default() -> Self { - Self::new() - } + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + pub struct CommandProvider { + yank: Command, + paste: Command, + yank_primary: Option<Command>, + paste_primary: Option<Command>, } - impl ClipboardProvider for FallbackProvider { + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "kebab-case")] + #[allow(clippy::large_enum_variant)] + pub enum ClipboardProvider { + Pasteboard, + Wayland, + XClip, + XSel, + Win32Yank, + Tmux, + #[cfg(windows)] + Windows, + Termux, #[cfg(feature = "term")] - fn name(&self) -> Cow<str> { - Cow::Borrowed("termcode") - } - - #[cfg(not(feature = "term"))] - fn name(&self) -> Cow<str> { - Cow::Borrowed("none") - } + Termcode, + Custom(CommandProvider), + None, + } - fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> { - // This is the same noop if term is enabled or not. - // We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole, - // and it would require this to be async to listen for the response - let value = match clipboard_type { - ClipboardType::Clipboard => self.buf.clone(), - ClipboardType::Selection => self.primary_buf.clone(), - }; + impl Default for ClipboardProvider { + #[cfg(windows)] + fn default() -> Self { + use helix_stdx::env::binary_exists; - Ok(value) + if binary_exists("win32yank.exe") { + Self::Win32Yank + } else { + Self::Windows + } } - fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> { - #[cfg(feature = "term")] - crossterm::execute!( - std::io::stdout(), - osc52::SetClipboardCommand::new(&content, clipboard_type) - )?; - // Set our internal variables to use in get_content regardless of using OSC 52 - match clipboard_type { - ClipboardType::Clipboard => self.buf = content, - ClipboardType::Selection => self.primary_buf = content, + #[cfg(target_os = "macos")] + fn default() -> Self { + use helix_stdx::env::{binary_exists, env_var_is_set}; + + if env_var_is_set("TMUX") && binary_exists("tmux") { + Self::Tmux + } else if binary_exists("pbcopy") && binary_exists("pbpaste") { + Self::Pasteboard + } else if cfg!(feature = "term") { + Self::Termcode + } else { + Self::None } - Ok(()) } - } - - #[cfg(not(target_arch = "wasm32"))] - pub mod command { - use super::*; - use anyhow::{bail, Context as _}; #[cfg(not(any(windows, target_os = "macos")))] - pub fn is_exit_success(program: &str, args: &[&str]) -> bool { - std::process::Command::new(program) - .args(args) - .output() - .ok() - .and_then(|out| out.status.success().then_some(())) - .is_some() - } + fn default() -> Self { + use helix_stdx::env::{binary_exists, env_var_is_set}; + + fn is_exit_success(program: &str, args: &[&str]) -> bool { + std::process::Command::new(program) + .args(args) + .output() + .ok() + .and_then(|out| out.status.success().then_some(())) + .is_some() + } - #[derive(Debug)] - pub struct Config { - pub prg: &'static str, - pub args: &'static [&'static str], + if env_var_is_set("WAYLAND_DISPLAY") + && binary_exists("wl-copy") + && binary_exists("wl-paste") + { + Self::Wayland + } else if env_var_is_set("DISPLAY") && binary_exists("xclip") { + Self::XClip + } else if env_var_is_set("DISPLAY") + && binary_exists("xsel") + // FIXME: check performance of is_exit_success + && is_exit_success("xsel", &["-o", "-b"]) + { + Self::XSel + } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") + { + Self::Termux + } else if env_var_is_set("TMUX") && binary_exists("tmux") { + Self::Tmux + } else if binary_exists("win32yank.exe") { + Self::Win32Yank + } else if cfg!(feature = "term") { + Self::Termcode + } else { + Self::None + } } + } - impl Config { - fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> { - use std::io::Write; - use std::process::{Command, Stdio}; - - let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null); - let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null); - - let mut command: Command = Command::new(self.prg); - - let mut command_mut: &mut Command = command - .args(self.args) - .stdin(stdin) - .stdout(stdout) - .stderr(Stdio::null()); - - // Fix for https://github.com/helix-editor/helix/issues/5424 - if cfg!(unix) { - use std::os::unix::process::CommandExt; - - unsafe { - command_mut = command_mut.pre_exec(|| match libc::setsid() { - -1 => Err(std::io::Error::last_os_error()), - _ => Ok(()), - }); - } - } - - let mut child = command_mut.spawn()?; - - if let Some(input) = input { - let mut stdin = child.stdin.take().context("stdin is missing")?; - stdin - .write_all(input.as_bytes()) - .context("couldn't write in stdin")?; - } - - // TODO: add timer? - let output = child.wait_with_output()?; - - if !output.status.success() { - bail!("clipboard provider {} failed", self.prg); - } - - if pipe_output { - Ok(Some(String::from_utf8(output.stdout)?)) + impl ClipboardProvider { + pub fn name(&self) -> Cow<'_, str> { + fn builtin_name<'a>( + name: &'static str, + provider: &'static CommandProvider, + ) -> Cow<'a, str> { + if provider.yank.command != provider.paste.command { + Cow::Owned(format!( + "{} ({}+{})", + name, provider.yank.command, provider.paste.command + )) } else { - Ok(None) + Cow::Owned(format!("{} ({})", name, provider.yank.command)) } } - } - - #[derive(Debug)] - pub struct Provider { - pub get_cmd: Config, - pub set_cmd: Config, - pub get_primary_cmd: Option<Config>, - pub set_primary_cmd: Option<Config>, - } - impl ClipboardProvider for Provider { - fn name(&self) -> Cow<str> { - if self.get_cmd.prg != self.set_cmd.prg { - Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg)) - } else { - Cow::Borrowed(self.get_cmd.prg) - } + match self { + // These names should match the config option names from Serde + Self::Pasteboard => builtin_name("pasteboard", &PASTEBOARD), + Self::Wayland => builtin_name("wayland", &WL_CLIPBOARD), + Self::XClip => builtin_name("x-clip", &WL_CLIPBOARD), + Self::XSel => builtin_name("x-sel", &WL_CLIPBOARD), + Self::Win32Yank => builtin_name("win-32-yank", &WL_CLIPBOARD), + Self::Tmux => builtin_name("tmux", &TMUX), + Self::Termux => builtin_name("termux", &TERMUX), + #[cfg(windows)] + Self::Windows => "windows".into(), + #[cfg(feature = "term")] + Self::Termcode => "termcode".into(), + Self::Custom(command_provider) => Cow::Owned(format!( + "custom ({}+{})", + command_provider.yank.command, command_provider.paste.command + )), + Self::None => "none".into(), } + } - fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> { + pub fn get_contents(&self, clipboard_type: &ClipboardType) -> Result<String> { + fn yank_from_builtin( + provider: CommandProvider, + clipboard_type: &ClipboardType, + ) -> Result<String> { match clipboard_type { - ClipboardType::Clipboard => Ok(self - .get_cmd - .execute(None, true)? - .context("output is missing")?), + ClipboardType::Clipboard => execute_command(&provider.yank, None, true)? + .ok_or(ClipboardError::MissingStdout), ClipboardType::Selection => { - if let Some(cmd) = &self.get_primary_cmd { - return cmd.execute(None, true)?.context("output is missing"); + if let Some(cmd) = provider.yank_primary.as_ref() { + return execute_command(cmd, None, true)? + .ok_or(ClipboardError::MissingStdout); } Ok(String::new()) @@ -358,56 +226,274 @@ pub mod provider { } } - fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> { + match self { + Self::Pasteboard => yank_from_builtin(PASTEBOARD, clipboard_type), + Self::Wayland => yank_from_builtin(WL_CLIPBOARD, clipboard_type), + Self::XClip => yank_from_builtin(XCLIP, clipboard_type), + Self::XSel => yank_from_builtin(XSEL, clipboard_type), + Self::Win32Yank => yank_from_builtin(WIN32, clipboard_type), + Self::Tmux => yank_from_builtin(TMUX, clipboard_type), + Self::Termux => yank_from_builtin(TERMUX, clipboard_type), + #[cfg(target_os = "windows")] + Self::Windows => match clipboard_type { + ClipboardType::Clipboard => { + let contents = + clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; + Ok(contents) + } + ClipboardType::Selection => Ok(String::new()), + }, + #[cfg(feature = "term")] + Self::Termcode => Err(ClipboardError::ReadingNotSupported), + Self::Custom(command_provider) => { + execute_command(&command_provider.yank, None, true)? + .ok_or(ClipboardError::MissingStdout) + } + Self::None => Err(ClipboardError::ReadingNotSupported), + } + } + + pub fn set_contents(&self, content: &str, clipboard_type: ClipboardType) -> Result<()> { + fn paste_to_builtin( + provider: CommandProvider, + content: &str, + clipboard_type: ClipboardType, + ) -> Result<()> { let cmd = match clipboard_type { - ClipboardType::Clipboard => &self.set_cmd, + ClipboardType::Clipboard => &provider.paste, ClipboardType::Selection => { - if let Some(cmd) = &self.set_primary_cmd { + if let Some(cmd) = provider.paste_primary.as_ref() { cmd } else { return Ok(()); } } }; - cmd.execute(Some(&value), false).map(|_| ()) + + execute_command(cmd, Some(content), false).map(|_| ()) + } + + match self { + Self::Pasteboard => paste_to_builtin(PASTEBOARD, content, clipboard_type), + Self::Wayland => paste_to_builtin(WL_CLIPBOARD, content, clipboard_type), + Self::XClip => paste_to_builtin(XCLIP, content, clipboard_type), + Self::XSel => paste_to_builtin(XSEL, content, clipboard_type), + Self::Win32Yank => paste_to_builtin(WIN32, content, clipboard_type), + Self::Tmux => paste_to_builtin(TMUX, content, clipboard_type), + Self::Termux => paste_to_builtin(TERMUX, content, clipboard_type), + #[cfg(target_os = "windows")] + Self::Windows => match clipboard_type { + ClipboardType::Clipboard => { + clipboard_win::set_clipboard(clipboard_win::formats::Unicode, content)?; + Ok(()) + } + ClipboardType::Selection => Ok(()), + }, + #[cfg(feature = "term")] + Self::Termcode => { + crossterm::queue!( + std::io::stdout(), + osc52::SetClipboardCommand::new(content, clipboard_type) + )?; + Ok(()) + } + Self::Custom(command_provider) => match clipboard_type { + ClipboardType::Clipboard => { + execute_command(&command_provider.paste, Some(content), false).map(|_| ()) + } + ClipboardType::Selection => { + if let Some(cmd) = &command_provider.paste_primary { + execute_command(cmd, Some(content), false).map(|_| ()) + } else { + Ok(()) + } + } + }, + Self::None => Ok(()), } } } -} -#[cfg(target_os = "windows")] -mod provider { - use super::{ClipboardProvider, ClipboardType}; - use anyhow::Result; - use std::borrow::Cow; + macro_rules! command_provider { + ($name:ident, + yank => $yank_cmd:literal $( , $yank_arg:literal )* ; + paste => $paste_cmd:literal $( , $paste_arg:literal )* ; ) => { + const $name: CommandProvider = CommandProvider { + yank: Command { + command: Cow::Borrowed($yank_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ]) + }, + paste: Command { + command: Cow::Borrowed($paste_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ]) + }, + yank_primary: None, + paste_primary: None, + }; + }; + ($name:ident, + yank => $yank_cmd:literal $( , $yank_arg:literal )* ; + paste => $paste_cmd:literal $( , $paste_arg:literal )* ; + yank_primary => $yank_primary_cmd:literal $( , $yank_primary_arg:literal )* ; + paste_primary => $paste_primary_cmd:literal $( , $paste_primary_arg:literal )* ; ) => { + const $name: CommandProvider = CommandProvider { + yank: Command { + command: Cow::Borrowed($yank_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ]) + }, + paste: Command { + command: Cow::Borrowed($paste_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ]) + }, + yank_primary: Some(Command { + command: Cow::Borrowed($yank_primary_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_primary_arg) ),* ]) + }), + paste_primary: Some(Command { + command: Cow::Borrowed($paste_primary_cmd), + args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_primary_arg) ),* ]) + }), + }; + }; + } - #[derive(Default, Debug)] - pub struct WindowsProvider; + command_provider! { + TMUX, + yank => "tmux", "load-buffer", "-w", "-"; + paste => "tmux", "save-buffer", "-"; + } + command_provider! { + PASTEBOARD, + yank => "pbcopy"; + paste => "pbpaste"; + } + command_provider! { + WL_CLIPBOARD, + yank => "wl-copy", "--type", "text/plain"; + paste => "wl-paste", "--no-newline"; + yank_primary => "wl-copy", "-p", "--type", "text/plain"; + paste_primary => "wl-paste", "-p", "--no-newline"; + } + command_provider! { + XCLIP, + yank => "xclip", "-i", "-selection", "clipboard"; + paste => "xclip", "-o", "-selection", "clipboard"; + yank_primary => "xclip", "-i"; + paste_primary => "xclip", "-o"; + } + command_provider! { + XSEL, + yank => "xsel", "-i", "-b"; + paste => "xsel", "-o", "-b"; + yank_primary => "xsel", "-i"; + paste_primary => "xsel", "-o"; + } + command_provider! { + WIN32, + yank => "win32yank.exe", "-i", "--crlf"; + paste => "win32yank.exe", "-o", "--lf"; + } + command_provider! { + TERMUX, + yank => "termux-clipboard-set"; + paste => "termux-clipboard-get"; + } - impl ClipboardProvider for WindowsProvider { - fn name(&self) -> Cow<str> { - log::debug!("Using clipboard-win to interact with the system clipboard"); - Cow::Borrowed("clipboard-win") + #[cfg(feature = "term")] + mod osc52 { + use {super::ClipboardType, crate::base64}; + + pub struct SetClipboardCommand { + encoded_content: String, + clipboard_type: ClipboardType, } - fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> { - match clipboard_type { - ClipboardType::Clipboard => { - let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; - Ok(contents) + impl SetClipboardCommand { + pub fn new(content: &str, clipboard_type: ClipboardType) -> Self { + Self { + encoded_content: base64::encode(content.as_bytes()), + clipboard_type, } - ClipboardType::Selection => Ok(String::new()), } } - fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> { - match clipboard_type { - ClipboardType::Clipboard => { - clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?; - } - ClipboardType::Selection => {} - }; - Ok(()) + impl crossterm::Command for SetClipboardCommand { + fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + let kind = match &self.clipboard_type { + ClipboardType::Clipboard => "c", + ClipboardType::Selection => "p", + }; + // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/ + write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content) + } + #[cfg(windows)] + fn execute_winapi(&self) -> std::result::Result<(), std::io::Error> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "OSC clipboard codes not supported by winapi.", + )) + } + } + } + + fn execute_command( + cmd: &Command, + input: Option<&str>, + pipe_output: bool, + ) -> Result<Option<String>> { + use std::io::Write; + use std::process::{Command, Stdio}; + + let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null); + let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null); + + let mut command: Command = Command::new(cmd.command.as_ref()); + + #[allow(unused_mut)] + let mut command_mut: &mut Command = command + .args(cmd.args.iter().map(AsRef::as_ref)) + .stdin(stdin) + .stdout(stdout) + .stderr(Stdio::null()); + + // Fix for https://github.com/helix-editor/helix/issues/5424 + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + + unsafe { + command_mut = command_mut.pre_exec(|| match libc::setsid() { + -1 => Err(std::io::Error::last_os_error()), + _ => Ok(()), + }); + } + } + + let mut child = command_mut.spawn()?; + + if let Some(input) = input { + let mut stdin = child.stdin.take().ok_or(ClipboardError::StdinWriteFailed)?; + stdin + .write_all(input.as_bytes()) + .map_err(|_| ClipboardError::StdinWriteFailed)?; + } + + // TODO: add timer? + let output = child.wait_with_output()?; + + if !output.status.success() { + log::error!( + "clipboard provider {} failed with stderr: \"{}\"", + cmd.command, + String::from_utf8_lossy(&output.stderr) + ); + return Err(ClipboardError::CommandFailed); + } + + if pipe_output { + Ok(Some(String::from_utf8(output.stdout)?)) + } else { + Ok(None) } } } |