aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src/clipboard.rs
diff options
context:
space:
mode:
authorNathan Vegdahl2021-06-20 23:09:14 +0000
committerNathan Vegdahl2021-06-20 23:09:14 +0000
commite686c3e4626fdafbcc2dab9d381eba83a5f6f974 (patch)
treea598e3fedc1f2ae78ebc6f132c81b37cedf5415d /helix-view/src/clipboard.rs
parent4efd6713c5b30b33c497a1f85b77a7b0a7fd17e0 (diff)
parent985625763addd839a101263ae90cfb2f205830fc (diff)
Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection
Rebasing was making me manually fix conflicts on every commit, so merging instead.
Diffstat (limited to 'helix-view/src/clipboard.rs')
-rw-r--r--helix-view/src/clipboard.rs193
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(|_| ())
+ }
+ }
+}