1use std::io::Write;
7use std::process::{Command, Stdio};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Tool { PbCopyPaste, WlClipboard, Xclip, Xsel }
11
12pub fn detect() -> Option<Tool> {
14 const CANDIDATES: &[(&str, Tool)] = &[
18 ("pbpaste", Tool::PbCopyPaste),
19 ("wl-paste", Tool::WlClipboard),
20 ("xclip", Tool::Xclip),
21 ("xsel", Tool::Xsel),
22 ];
23 for (probe, tool) in CANDIDATES {
24 if which(probe) { return Some(*tool); }
25 }
26 None
27}
28
29fn which(bin: &str) -> bool {
32 Command::new("sh").arg("-c").arg(format!("command -v {bin}"))
33 .stdout(Stdio::null()).stderr(Stdio::null()).stdin(Stdio::null())
34 .status().map(|s| s.success()).unwrap_or(false)
35}
36
37fn read_cmd(tool: Tool) -> Command {
38 let mut c;
39 match tool {
40 Tool::PbCopyPaste => { c = Command::new("pbpaste"); }
41 Tool::WlClipboard => { c = Command::new("wl-paste"); c.arg("--no-newline"); }
42 Tool::Xclip => { c = Command::new("xclip"); c.args(["-selection","clipboard","-o"]); }
43 Tool::Xsel => { c = Command::new("xsel"); c.args(["--clipboard","--output"]); }
44 }
45 c
46}
47
48fn write_cmd(tool: Tool) -> Command {
49 let mut c;
50 match tool {
51 Tool::PbCopyPaste => { c = Command::new("pbcopy"); }
52 Tool::WlClipboard => { c = Command::new("wl-copy"); }
53 Tool::Xclip => { c = Command::new("xclip"); c.args(["-selection","clipboard"]); }
54 Tool::Xsel => { c = Command::new("xsel"); c.args(["--clipboard","--input"]); }
55 }
56 c
57}
58
59pub fn read() -> Result<Vec<u8>, String> {
61 let tool = detect().ok_or("no clipboard tool found (need pbpaste/wl-paste/xclip/xsel)")?;
62 let out = read_cmd(tool).stderr(Stdio::piped()).stdout(Stdio::piped()).stdin(Stdio::null())
63 .output()
64 .map_err(|e| format!("clipboard read failed: {e}"))?;
65 if !out.status.success() {
66 let err = String::from_utf8_lossy(&out.stderr);
67 let err = err.trim();
68 return Err(if err.is_empty() {
69 "clipboard read failed (tool exited with error)".to_string()
70 } else {
71 format!("clipboard read failed: {err}")
72 });
73 }
74 Ok(out.stdout)
75}
76
77pub fn write(bytes: &[u8]) -> Result<(), String> {
79 let tool = detect().ok_or("no clipboard tool found (need pbcopy/wl-copy/xclip/xsel)")?;
80 let mut child = write_cmd(tool).stdin(Stdio::piped())
81 .stdout(Stdio::null()).stderr(Stdio::piped()).spawn()
82 .map_err(|e| format!("clipboard write failed: {e}"))?;
83 let mut stdin = child.stdin.take().ok_or("clipboard stdin unavailable")?;
86 stdin.write_all(bytes).map_err(|e| format!("clipboard write failed: {e}"))?;
87 drop(stdin);
88 let out = child.wait_with_output().map_err(|e| format!("clipboard write failed: {e}"))?;
89 if !out.status.success() {
90 let err = String::from_utf8_lossy(&out.stderr);
91 let err = err.trim();
92 return Err(if err.is_empty() {
93 "clipboard write failed (tool exited with error)".to_string()
94 } else {
95 format!("clipboard write failed: {err}")
96 });
97 }
98 Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 #[test]
105 fn read_write_cmd_programs_distinct_per_tool() {
106 for t in [Tool::PbCopyPaste, Tool::WlClipboard, Tool::Xclip, Tool::Xsel] {
107 assert!(!read_cmd(t).get_program().is_empty());
108 assert!(!write_cmd(t).get_program().is_empty());
109 }
110 }
111 #[test]
112 fn xclip_read_uses_output_flag() {
113 let c = read_cmd(Tool::Xclip);
114 let args: Vec<_> = c.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
115 assert!(args.contains(&"-o".to_string()));
116 }
117}