use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::thread;
use std::time::Duration;
use crate::error::{Error, Result};
type Backend = Option<&'static (dyn Fn(&str) -> Result<()> + Sync)>;
fn backend() -> Backend {
static BACKEND: OnceLock<Backend> = OnceLock::new();
*BACKEND.get_or_init(detect_backend_impl)
}
fn detect_backend_impl() -> Backend {
#[cfg(target_os = "macos")]
{
if probe(|t| run_pipe(&["pbcopy"], t)).is_ok() {
return Some(&|t: &str| run_pipe(&["pbcopy"], t));
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))]
{
if std::env::var_os("WAYLAND_DISPLAY").is_some()
&& probe(|t| run_pipe(&["wl-copy"], t)).is_ok()
{
return Some(&|t: &str| run_pipe(&["wl-copy"], t));
}
if std::env::var_os("DISPLAY").is_some()
&& probe(|t| run_pipe(&["xclip", "-selection", "clipboard"], t)).is_ok()
{
return Some(&|t: &str| run_pipe(&["xclip", "-selection", "clipboard"], t));
}
if std::env::var_os("DISPLAY").is_some()
&& probe(|t| run_pipe(&["xsel", "-bi"], t)).is_ok()
{
return Some(&|t: &str| run_pipe(&["xsel", "-bi"], t));
}
}
#[cfg(target_os = "windows")]
{
const PS_CMD: &[&str] = &[
"powershell.exe",
"-NoProfile",
"-Command",
"[Console]::InputEncoding=[Text.Encoding]::UTF8;Set-Clipboard([Console]::In.ReadToEnd())",
];
if probe(|t| run_pipe(PS_CMD, t)).is_ok() {
return Some(&|t: &str| run_pipe(PS_CMD, t));
}
}
None
}
fn probe<F>(f: F) -> Result<()>
where
F: Fn(&str) -> Result<()>,
{
f("")
}
fn run_pipe(cmd: &[&str], text: &str) -> Result<()> {
use std::io::Write;
let mut child = Command::new(cmd[0])
.args(&cmd[1..])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| Error::Other(format!("spawn {} failed: {e}", cmd[0])))?;
{
let stdin = child.stdin.as_mut().ok_or_else(|| {
Error::Other(format!("could not open stdin for {}", cmd[0]))
})?;
stdin
.write_all(text.as_bytes())
.map_err(|e| Error::Other(format!("write to {} failed: {e}", cmd[0])))?;
}
let status = child
.wait()
.map_err(|e| Error::Other(format!("wait {} failed: {e}", cmd[0])))?;
if !status.success() {
return Err(Error::Other(format!(
"{} exited with {status}",
cmd[0]
)));
}
Ok(())
}
pub fn copy(text: &str) -> Result<()> {
match backend() {
Some(backend) => (backend)(text),
None => Err(Error::Other(
"no clipboard backend available (pbcopy/wl-copy/xclip/xsel)".into(),
)),
}
}
pub fn copy_and_clear_after(text: &str, secs: u64) -> Result<()> {
copy(text)?;
thread::spawn(move || {
thread::sleep(Duration::from_secs(secs));
let _ = copy("");
});
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn copy_returns_err_when_no_backend() {
let has_env = std::env::var_os("DISPLAY").is_some()
|| std::env::var_os("WAYLAND_DISPLAY").is_some()
|| cfg!(target_os = "macos");
if has_env {
return;
}
let res = copy("hello");
assert!(res.is_err(), "expected Err when no clipboard backend, got {res:?}");
}
#[test]
#[ignore]
fn copy_and_clear_manual() {
let res = copy_and_clear_after("zkv-test-token", 1);
match res {
Ok(()) => println!("copied; clipboard should clear in ~1s"),
Err(e) => println!("backend unavailable in this env: {e}"),
}
}
}