on 2158366 help sign in
helix/helix-view/src/clipboard.rs
320 lines 11 kB history raw
1// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
3use anyhow::Result;
4use std::borrow::Cow;
6pub enum ClipboardType {
7 Clipboard,
8 Selection,
9}
11pub trait ClipboardProvider: std::fmt::Debug {
12 fn name(&self) -> Cow<str>;
13 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>;
14 fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>;
17macro_rules! command_provider {
18 (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
19 Box::new(provider::CommandProvider {
20 get_cmd: provider::CommandConfig {
21 prg: $get_prg,
22 args: &[ $( $get_arg ),* ],
23 },
24 set_cmd: provider::CommandConfig {
25 prg: $set_prg,
26 args: &[ $( $set_arg ),* ],
27 },
28 get_primary_cmd: None,
29 set_primary_cmd: None,
30 })
31 }};
33 (paste => $get_prg:literal $( , $get_arg:literal )* ;
34 copy => $set_prg:literal $( , $set_arg:literal )* ;
35 primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ;
36 primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ;
37 ) => {{
38 Box::new(provider::CommandProvider {
39 get_cmd: provider::CommandConfig {
40 prg: $get_prg,
41 args: &[ $( $get_arg ),* ],
42 },
43 set_cmd: provider::CommandConfig {
44 prg: $set_prg,
45 args: &[ $( $set_arg ),* ],
46 },
47 get_primary_cmd: Some(provider::CommandConfig {
48 prg: $pr_get_prg,
49 args: &[ $( $pr_get_arg ),* ],
50 }),
51 set_primary_cmd: Some(provider::CommandConfig {
52 prg: $pr_set_prg,
53 args: &[ $( $pr_set_arg ),* ],
54 }),
55 })
56 }};
59pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
60 // TODO: support for user-defined provider, probably when we have plugin support by setting a
61 // variable?
63 if exists("pbcopy") && exists("pbpaste") {
64 command_provider! {
65 paste => "pbpaste";
66 copy => "pbcopy";
67 }
68 } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
69 command_provider! {
70 paste => "wl-paste", "--no-newline";
71 copy => "wl-copy", "--type", "text/plain";
72 primary_paste => "wl-paste", "-p", "--no-newline";
73 primary_copy => "wl-copy", "-p", "--type", "text/plain";
74 }
75 } else if env_var_is_set("DISPLAY") && exists("xclip") {
76 command_provider! {
77 paste => "xclip", "-o", "-selection", "clipboard";
78 copy => "xclip", "-i", "-selection", "clipboard";
79 primary_paste => "xclip", "-o";
80 primary_copy => "xclip", "-i";
81 }
82 } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
83 {
84 // FIXME: check performance of is_exit_success
85 command_provider! {
86 paste => "xsel", "-o", "-b";
87 copy => "xsel", "--nodetach", "-i", "-b";
88 primary_paste => "xsel", "-o";
89 primary_copy => "xsel", "-i";
90 }
91 } else if exists("lemonade") {
92 command_provider! {
93 paste => "lemonade", "paste";
94 copy => "lemonade", "copy";
95 }
96 } else if exists("doitclient") {
97 command_provider! {
98 paste => "doitclient", "wclip", "-r";
99 copy => "doitclient", "wclip";
100 }
101 } else if exists("win32yank.exe") {
102 // FIXME: does it work within WSL?
103 command_provider! {
104 paste => "win32yank.exe", "-o", "--lf";
105 copy => "win32yank.exe", "-i", "--crlf";
106 }
107 } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
108 command_provider! {
109 paste => "termux-clipboard-get";
110 copy => "termux-clipboard-set";
111 }
112 } else if env_var_is_set("TMUX") && exists("tmux") {
113 command_provider! {
114 paste => "tmux", "save-buffer", "-";
115 copy => "tmux", "load-buffer", "-";
116 }
117 } else {
118 #[cfg(target_os = "windows")]
119 return Box::new(provider::WindowsProvider::new());
121 #[cfg(not(target_os = "windows"))]
122 return Box::new(provider::NopProvider::new());
123 }
126fn exists(executable_name: &str) -> bool {
127 which::which(executable_name).is_ok()
130fn env_var_is_set(env_var_name: &str) -> bool {
131 std::env::var_os(env_var_name).is_some()
134fn is_exit_success(program: &str, args: &[&str]) -> bool {
135 std::process::Command::new(program)
136 .args(args)
137 .output()
138 .ok()
139 .and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
140 .is_some()
143mod provider {
144 use super::{ClipboardProvider, ClipboardType};
145 use anyhow::{bail, Context as _, Result};
146 use std::borrow::Cow;
148 #[derive(Debug)]
149 pub struct NopProvider {
150 buf: String,
151 primary_buf: String,
152 }
154 impl NopProvider {
155 #[allow(dead_code)]
156 // Only dead_code on Windows.
157 pub fn new() -> Self {
158 Self {
159 buf: String::new(),
160 primary_buf: String::new(),
161 }
162 }
163 }
165 impl ClipboardProvider for NopProvider {
166 fn name(&self) -> Cow<str> {
167 Cow::Borrowed("none")
168 }
170 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
171 let value = match clipboard_type {
172 ClipboardType::Clipboard => self.buf.clone(),
173 ClipboardType::Selection => self.primary_buf.clone(),
174 };
176 Ok(value)
177 }
179 fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
180 match clipboard_type {
181 ClipboardType::Clipboard => self.buf = content,
182 ClipboardType::Selection => self.primary_buf = content,
183 }
184 Ok(())
185 }
186 }
188 #[cfg(target_os = "windows")]
189 #[derive(Debug)]
190 pub struct WindowsProvider {
191 selection_buf: String,
192 }
194 #[cfg(target_os = "windows")]
195 impl WindowsProvider {
196 pub fn new() -> Self {
197 Self {
198 selection_buf: String::new(),
199 }
200 }
201 }
203 #[cfg(target_os = "windows")]
204 impl ClipboardProvider for WindowsProvider {
205 fn name(&self) -> Cow<str> {
206 Cow::Borrowed("clipboard-win")
207 }
209 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
210 match clipboard_type {
211 ClipboardType::Clipboard => {
212 let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
213 Ok(contents)
214 }
215 ClipboardType::Selection => Ok(String::new()),
216 }
217 }
219 fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
220 match clipboard_type {
221 ClipboardType::Clipboard => {
222 clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?;
223 }
224 ClipboardType::Selection => {}
225 };
226 Ok(())
227 }
228 }
230 #[derive(Debug)]
231 pub struct CommandConfig {
232 pub prg: &'static str,
233 pub args: &'static [&'static str],
234 }
236 impl CommandConfig {
237 fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
238 use std::io::Write;
239 use std::process::{Command, Stdio};
241 let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
242 let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
244 let mut child = Command::new(self.prg)
245 .args(self.args)
246 .stdin(stdin)
247 .stdout(stdout)
248 .stderr(Stdio::null())
249 .spawn()?;
251 if let Some(input) = input {
252 let mut stdin = child.stdin.take().context("stdin is missing")?;
253 stdin
254 .write_all(input.as_bytes())
255 .context("couldn't write in stdin")?;
256 }
258 // TODO: add timer?
259 let output = child.wait_with_output()?;
261 if !output.status.success() {
262 bail!("clipboard provider {} failed", self.prg);
263 }
265 if pipe_output {
266 Ok(Some(String::from_utf8(output.stdout)?))
267 } else {
268 Ok(None)
269 }
270 }
271 }
273 #[derive(Debug)]
274 pub struct CommandProvider {
275 pub get_cmd: CommandConfig,
276 pub set_cmd: CommandConfig,
277 pub get_primary_cmd: Option<CommandConfig>,
278 pub set_primary_cmd: Option<CommandConfig>,
279 }
281 impl ClipboardProvider for CommandProvider {
282 fn name(&self) -> Cow<str> {
283 if self.get_cmd.prg != self.set_cmd.prg {
284 Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
285 } else {
286 Cow::Borrowed(self.get_cmd.prg)
287 }
288 }
290 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
291 match clipboard_type {
292 ClipboardType::Clipboard => Ok(self
293 .get_cmd
294 .execute(None, true)?
295 .context("output is missing")?),
296 ClipboardType::Selection => {
297 if let Some(cmd) = &self.get_primary_cmd {
298 return cmd.execute(None, true)?.context("output is missing");
299 }
301 Ok(String::new())
302 }
303 }
304 }
306 fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> {
307 let cmd = match clipboard_type {
308 ClipboardType::Clipboard => &self.set_cmd,
309 ClipboardType::Selection => {
310 if let Some(cmd) = &self.set_primary_cmd {
311 cmd
312 } else {
313 return Ok(());
314 }
315 }
316 };
317 cmd.execute(Some(&value), false).map(|_| ())
318 }
319 }