use std::io;
pub const CLIPBOARD_CMDS: &[(&str, &[&str])] = &[
("pbcopy", &[]),
("/usr/bin/pbcopy", &[]),
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
("clip.exe", &[]),
("termux-clipboard-set", &[]),
];
pub fn copy_to_clipboard(text: &str) -> Result<String, String> {
let mut last_err = String::new();
for (cmd, args) in CLIPBOARD_CMDS {
match run_clipboard_cmd(cmd, args, text) {
Ok(()) => return Ok((*cmd).to_string()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => last_err = format!("{}: {}", cmd, e),
}
}
match copy_via_osc52(text) {
Ok(()) => Ok("osc52".to_string()),
Err(e) if last_err.is_empty() => Err(format!("no clipboard helper found; osc52: {}", e)),
Err(_) => Err(last_err),
}
}
pub fn run_clipboard_cmd(cmd: &str, args: &[&str], text: &str) -> io::Result<()> {
use std::io::Write;
let mut child = std::process::Command::new(cmd)
.args(args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| io::Error::other("clipboard helper has no stdin"))?;
let written = stdin.write_all(text.as_bytes());
drop(stdin);
let status = child.wait()?;
written?;
if status.success() {
Ok(())
} else {
Err(io::Error::other(format!("exited with {}", status)))
}
}
pub fn osc52_sequence(text: &str, tmux: bool) -> String {
let payload = format!("\x1b]52;c;{}\x07", base64_encode(text.as_bytes()));
if tmux {
format!("\x1bPtmux;{}\x1b\\", payload.replace('\x1b', "\x1b\x1b"))
} else {
payload
}
}
pub fn copy_via_osc52(text: &str) -> io::Result<()> {
use std::io::Write;
let seq = osc52_sequence(text, std::env::var_os("TMUX").is_some());
let mut out = io::stdout();
out.write_all(seq.as_bytes())?;
out.flush()
}
#[must_use]
pub fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = u32::from(chunk[0]);
let b1 = chunk.get(1).map_or(0, |b| u32::from(*b));
let b2 = chunk.get(2).map_or(0, |b| u32::from(*b));
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(n >> 18) as usize & 0x3f] as char);
out.push(ALPHABET[(n >> 12) as usize & 0x3f] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(n >> 6) as usize & 0x3f] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[n as usize & 0x3f] as char
} else {
'='
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_encode_matches_rfc4648_vectors() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_encode(b"f"), "Zg==");
assert_eq!(base64_encode(b"fo"), "Zm8=");
assert_eq!(base64_encode(b"foo"), "Zm9v");
assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
assert_eq!(base64_encode(&[0xff, 0xfe, 0xfd]), "//79");
}
#[test]
fn missing_clipboard_helper_reports_not_found() {
let err = run_clipboard_cmd("iftoprs-no-such-clipboard-helper", &[], "x")
.expect_err("nonexistent helper must fail");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn clipboard_helper_exit_status_is_checked() {
assert!(run_clipboard_cmd("false", &[], "payload").is_err());
assert!(run_clipboard_cmd("true", &[], "payload").is_ok());
}
#[test]
fn osc52_sequence_encodes_and_wraps_for_tmux() {
assert_eq!(osc52_sequence("foobar", false), "\x1b]52;c;Zm9vYmFy\x07");
assert_eq!(
osc52_sequence("foobar", true),
"\x1bPtmux;\x1b\x1b]52;c;Zm9vYmFy\x07\x1b\\"
);
}
}