diff options
Diffstat (limited to 'helix-view/src/clipboard.rs')
-rw-r--r-- | helix-view/src/clipboard.rs | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs new file mode 100644 index 00000000..dcc44340 --- /dev/null +++ b/helix-view/src/clipboard.rs @@ -0,0 +1,193 @@ +// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152 + +use anyhow::Result; +use std::borrow::Cow; + +pub trait ClipboardProvider: std::fmt::Debug { + fn name(&self) -> Cow<str>; + fn get_contents(&self) -> Result<String>; + fn set_contents(&self, contents: String) -> Result<()>; +} + +macro_rules! command_provider { + (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{ + Box::new(provider::CommandProvider { + get_cmd: provider::CommandConfig { + prg: $get_prg, + args: &[ $( $get_arg ),* ], + }, + set_cmd: provider::CommandConfig { + prg: $set_prg, + args: &[ $( $set_arg ),* ], + }, + }) + }}; +} + +pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { + // TODO: support for user-defined provider, probably when we have plugin support by setting a + // variable? + + if exists("pbcopy") && exists("pbpaste") { + command_provider! { + paste => "pbpaste"; + copy => "pbcopy"; + } + } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") { + command_provider! { + paste => "wl-paste", "--no-newline"; + copy => "wl-copy", "--foreground", "--type", "text/plain"; + } + } else if env_var_is_set("DISPLAY") && exists("xclip") { + command_provider! { + paste => "xclip", "-o", "-selection", "clipboard"; + copy => "xclip", "-i", "-selection", "clipboard"; + } + } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"]) + { + // FIXME: check performance of is_exit_success + command_provider! { + paste => "xsel", "-o", "-b"; + copy => "xsel", "--nodetach", "-i", "-b"; + } + } else if exists("lemonade") { + command_provider! { + paste => "lemonade", "paste"; + copy => "lemonade", "copy"; + } + } else if exists("doitclient") { + command_provider! { + paste => "doitclient", "wclip", "-r"; + copy => "doitclient", "wclip"; + } + } else if exists("win32yank.exe") { + // FIXME: does it work within WSL? + command_provider! { + paste => "win32yank.exe", "-o", "--lf"; + copy => "win32yank.exe", "-i", "--crlf"; + } + } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") { + command_provider! { + paste => "termux-clipboard-get"; + copy => "termux-clipboard-set"; + } + } else if env_var_is_set("TMUX") && exists("tmux") { + command_provider! { + paste => "tmux", "save-buffer", "-"; + copy => "tmux", "load-buffer", "-"; + } + } else { + Box::new(provider::NopProvider) + } +} + +fn exists(executable_name: &str) -> bool { + which::which(executable_name).is_ok() +} + +fn env_var_is_set(env_var_name: &str) -> bool { + std::env::var_os(env_var_name).is_some() +} + +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(|| ())) // TODO: use then_some when stabilized + .is_some() +} + +mod provider { + use super::ClipboardProvider; + use anyhow::{bail, Context as _, Result}; + use std::borrow::Cow; + + #[derive(Debug)] + pub struct NopProvider; + + impl ClipboardProvider for NopProvider { + fn name(&self) -> Cow<str> { + Cow::Borrowed("none") + } + + fn get_contents(&self) -> Result<String> { + Ok(String::new()) + } + + fn set_contents(&self, _: String) -> Result<()> { + Ok(()) + } + } + + #[derive(Debug)] + pub struct CommandConfig { + pub prg: &'static str, + pub args: &'static [&'static str], + } + + impl CommandConfig { + 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 child = Command::new(self.prg) + .args(self.args) + .stdin(stdin) + .stdout(stdout) + .stderr(Stdio::null()) + .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)?)) + } else { + Ok(None) + } + } + } + + #[derive(Debug)] + pub struct CommandProvider { + pub get_cmd: CommandConfig, + pub set_cmd: CommandConfig, + } + + impl ClipboardProvider for CommandProvider { + 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) + } + } + + fn get_contents(&self) -> Result<String> { + let output = self + .get_cmd + .execute(None, true)? + .context("output is missing")?; + Ok(output) + } + + fn set_contents(&self, value: String) -> Result<()> { + self.set_cmd.execute(Some(&value), false).map(|_| ()) + } + } +} |