@@ -0,0 +1,193 @@
1+// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
2+
3+use anyhow::Result;
4+use std::borrow::Cow;
5+
6+pub trait ClipboardProvider: std::fmt::Debug {
7+ fn name(&self) -> Cow<str>;
8+ fn get_contents(&self) -> Result<String>;
9+ fn set_contents(&self, contents: String) -> Result<()>;
10+}
11+
12+macro_rules! command_provider {
13+ (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
14+ Box::new(provider::CommandProvider {
15+ get_cmd: provider::CommandConfig {
16+ prg: $get_prg,
17+ args: &[ $( $get_arg ),* ],
18+ },
19+ set_cmd: provider::CommandConfig {
20+ prg: $set_prg,
21+ args: &[ $( $set_arg ),* ],
22+ },
23+ })
24+ }};
25+}
26+
27+pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
28+ // TODO: support for user-defined provider, probably when we have plugin support by setting a
29+ // variable?
30+
31+ if exists("pbcopy") && exists("pbpaste") {
32+ command_provider! {
33+ paste => "pbpaste";
34+ copy => "pbcopy";
35+ }
36+ } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
37+ command_provider! {
38+ paste => "wl-paste", "--no-newline";
39+ copy => "wl-copy", "--foreground", "--type", "text/plain";
40+ }
41+ } else if env_var_is_set("DISPLAY") && exists("xclip") {
42+ command_provider! {
43+ paste => "xclip", "-o", "-selection", "clipboard";
44+ copy => "xclip", "-i", "-selection", "clipboard";
45+ }
46+ } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
47+ {
48+ // FIXME: check performance of is_exit_success
49+ command_provider! {
50+ paste => "xsel", "-o", "-b";
51+ copy => "xsel", "--nodetach", "-i", "-b";
52+ }
53+ } else if exists("lemonade") {
54+ command_provider! {
55+ paste => "lemonade", "paste";
56+ copy => "lemonade", "copy";
57+ }
58+ } else if exists("doitclient") {
59+ command_provider! {
60+ paste => "doitclient", "wclip", "-r";
61+ copy => "doitclient", "wclip";
62+ }
63+ } else if exists("win32yank.exe") {
64+ // FIXME: does it work within WSL?
65+ command_provider! {
66+ paste => "win32yank.exe", "-o", "--lf";
67+ copy => "win32yank.exe", "-i", "--crlf";
68+ }
69+ } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
70+ command_provider! {
71+ paste => "termux-clipboard-get";
72+ copy => "termux-clipboard-set";
73+ }
74+ } else if env_var_is_set("TMUX") && exists("tmux") {
75+ command_provider! {
76+ paste => "tmux", "save-buffer", "-";
77+ copy => "tmux", "load-buffer", "-";
78+ }
79+ } else {
80+ Box::new(provider::NopProvider)
81+ }
82+}
83+
84+fn exists(executable_name: &str) -> bool {
85+ which::which(executable_name).is_ok()
86+}
87+
88+fn env_var_is_set(env_var_name: &str) -> bool {
89+ std::env::var_os(env_var_name).is_some()
90+}
91+
92+fn is_exit_success(program: &str, args: &[&str]) -> bool {
93+ std::process::Command::new(program)
94+ .args(args)
95+ .output()
96+ .ok()
97+ .and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
98+ .is_some()
99+}
100+
101+mod provider {
102+ use super::ClipboardProvider;
103+ use anyhow::{bail, Context as _, Result};
104+ use std::borrow::Cow;
105+
106+ #[derive(Debug)]
107+ pub struct NopProvider;
108+
109+ impl ClipboardProvider for NopProvider {
110+ fn name(&self) -> Cow<str> {
111+ Cow::Borrowed("none")
112+ }
113+
114+ fn get_contents(&self) -> Result<String> {
115+ Ok(String::new())
116+ }
117+
118+ fn set_contents(&self, _: String) -> Result<()> {
119+ Ok(())
120+ }
121+ }
122+
123+ #[derive(Debug)]
124+ pub struct CommandConfig {
125+ pub prg: &'static str,
126+ pub args: &'static [&'static str],
127+ }
128+
129+ impl CommandConfig {
130+ fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
131+ use std::io::Write;
132+ use std::process::{Command, Stdio};
133+
134+ let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
135+ let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
136+
137+ let mut child = Command::new(self.prg)
138+ .args(self.args)
139+ .stdin(stdin)
140+ .stdout(stdout)
141+ .stderr(Stdio::null())
142+ .spawn()?;
143+
144+ if let Some(input) = input {
145+ let mut stdin = child.stdin.take().context("stdin is missing")?;
146+ stdin
147+ .write_all(input.as_bytes())
148+ .context("couldn't write in stdin")?;
149+ }
150+
151+ // TODO: add timer?
152+ let output = child.wait_with_output()?;
153+
154+ if !output.status.success() {
155+ bail!("clipboard provider {} failed", self.prg);
156+ }
157+
158+ if pipe_output {
159+ Ok(Some(String::from_utf8(output.stdout)?))
160+ } else {
161+ Ok(None)
162+ }
163+ }
164+ }
165+
166+ #[derive(Debug)]
167+ pub struct CommandProvider {
168+ pub get_cmd: CommandConfig,
169+ pub set_cmd: CommandConfig,
170+ }
171+
172+ impl ClipboardProvider for CommandProvider {
173+ fn name(&self) -> Cow<str> {
174+ if self.get_cmd.prg != self.set_cmd.prg {
175+ Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
176+ } else {
177+ Cow::Borrowed(self.get_cmd.prg)
178+ }
179+ }
180+
181+ fn get_contents(&self) -> Result<String> {
182+ let output = self
183+ .get_cmd
184+ .execute(None, true)?
185+ .context("output is missing")?;
186+ Ok(output)
187+ }
188+
189+ fn set_contents(&self, value: String) -> Result<()> {
190+ self.set_cmd.execute(Some(&value), false).map(|_| ())
191+ }
192+ }
193+}