use std::io::Write;
use std::process::{Command, Stdio};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tool { PbCopyPaste, WlClipboard, Xclip, Xsel }
pub fn detect() -> Option<Tool> {
const CANDIDATES: &[(&str, Tool)] = &[
("pbpaste", Tool::PbCopyPaste),
("wl-paste", Tool::WlClipboard),
("xclip", Tool::Xclip),
("xsel", Tool::Xsel),
];
for (probe, tool) in CANDIDATES {
if which(probe) { return Some(*tool); }
}
None
}
fn which(bin: &str) -> bool {
Command::new("sh").arg("-c").arg(format!("command -v {bin}"))
.stdout(Stdio::null()).stderr(Stdio::null()).stdin(Stdio::null())
.status().map(|s| s.success()).unwrap_or(false)
}
fn read_cmd(tool: Tool) -> Command {
let mut c;
match tool {
Tool::PbCopyPaste => { c = Command::new("pbpaste"); }
Tool::WlClipboard => { c = Command::new("wl-paste"); c.arg("--no-newline"); }
Tool::Xclip => { c = Command::new("xclip"); c.args(["-selection","clipboard","-o"]); }
Tool::Xsel => { c = Command::new("xsel"); c.args(["--clipboard","--output"]); }
}
c
}
fn write_cmd(tool: Tool) -> Command {
let mut c;
match tool {
Tool::PbCopyPaste => { c = Command::new("pbcopy"); }
Tool::WlClipboard => { c = Command::new("wl-copy"); }
Tool::Xclip => { c = Command::new("xclip"); c.args(["-selection","clipboard"]); }
Tool::Xsel => { c = Command::new("xsel"); c.args(["--clipboard","--input"]); }
}
c
}
pub fn read() -> Result<Vec<u8>, String> {
let tool = detect().ok_or("no clipboard tool found (need pbpaste/wl-paste/xclip/xsel)")?;
let out = read_cmd(tool).stderr(Stdio::piped()).stdout(Stdio::piped()).stdin(Stdio::null())
.output()
.map_err(|e| format!("clipboard read failed: {e}"))?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
let err = err.trim();
return Err(if err.is_empty() {
"clipboard read failed (tool exited with error)".to_string()
} else {
format!("clipboard read failed: {err}")
});
}
Ok(out.stdout)
}
pub fn write(bytes: &[u8]) -> Result<(), String> {
let tool = detect().ok_or("no clipboard tool found (need pbcopy/wl-copy/xclip/xsel)")?;
let mut child = write_cmd(tool).stdin(Stdio::piped())
.stdout(Stdio::null()).stderr(Stdio::piped()).spawn()
.map_err(|e| format!("clipboard write failed: {e}"))?;
let mut stdin = child.stdin.take().ok_or("clipboard stdin unavailable")?;
stdin.write_all(bytes).map_err(|e| format!("clipboard write failed: {e}"))?;
drop(stdin);
let out = child.wait_with_output().map_err(|e| format!("clipboard write failed: {e}"))?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
let err = err.trim();
return Err(if err.is_empty() {
"clipboard write failed (tool exited with error)".to_string()
} else {
format!("clipboard write failed: {err}")
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_write_cmd_programs_distinct_per_tool() {
for t in [Tool::PbCopyPaste, Tool::WlClipboard, Tool::Xclip, Tool::Xsel] {
assert!(!read_cmd(t).get_program().is_empty());
assert!(!write_cmd(t).get_program().is_empty());
}
}
#[test]
fn xclip_read_uses_output_flag() {
let c = read_cmd(Tool::Xclip);
let args: Vec<_> = c.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
assert!(args.contains(&"-o".to_string()));
}
}