use std::io::Write;
use std::process::{Command, Stdio};
#[cfg(target_os = "macos")]
const WRITERS: &[(&str, &[&str])] = &[("pbcopy", &[])];
#[cfg(target_os = "macos")]
const READERS: &[(&str, &[&str])] = &[("pbpaste", &[])];
#[cfg(target_os = "linux")]
const WRITERS: &[(&str, &[&str])] = &[
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
];
#[cfg(target_os = "linux")]
const READERS: &[(&str, &[&str])] = &[
("wl-paste", &["--no-newline"]),
("xclip", &["-selection", "clipboard", "-o"]),
("xsel", &["--clipboard", "--output"]),
];
#[cfg(target_os = "windows")]
const WRITERS: &[(&str, &[&str])] = &[("clip", &[])];
#[cfg(target_os = "windows")]
const READERS: &[(&str, &[&str])] = &[("powershell", &["-NoProfile", "-Command", "Get-Clipboard"])];
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
const WRITERS: &[(&str, &[&str])] = &[];
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
const READERS: &[(&str, &[&str])] = &[];
pub fn copy(text: &str) -> bool {
let via_tool = WRITERS
.iter()
.any(|&(program, args)| write_to(program, args, text));
if !via_tool || is_ssh() {
emit_osc52(text);
}
via_tool
}
fn is_ssh() -> bool {
std::env::var_os("SSH_CONNECTION").is_some() || std::env::var_os("SSH_TTY").is_some()
}
fn emit_osc52(text: &str) {
let mut out = std::io::stdout();
let _ = out.write_all(osc52_sequence(text).as_bytes());
let _ = out.flush();
}
fn osc52_sequence(text: &str) -> String {
format!("\x1b]52;c;{}\x07", base64(text.as_bytes()))
}
fn base64(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(n >> 18 & 63) as usize] as char);
out.push(ALPHABET[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(n >> 6 & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[(n & 63) as usize] as char
} else {
'='
});
}
out
}
pub fn paste() -> Option<String> {
READERS.iter().find_map(|&(program, args)| {
let output = Command::new(program).args(args).output().ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
Some(text.trim_end_matches(['\n', '\r']).to_string())
})
}
fn write_to(program: &str, args: &[&str], text: &str) -> bool {
let Ok(mut child) = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return false;
};
if let Some(mut stdin) = child.stdin.take() {
if stdin.write_all(text.as_bytes()).is_err() {
return false;
}
}
child.wait().map(|status| status.success()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_matches_known_vectors() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(b"foob"), "Zm9vYg==");
assert_eq!(base64(b"fooba"), "Zm9vYmE=");
assert_eq!(base64(b"foobar"), "Zm9vYmFy");
}
#[test]
fn osc52_sequence_wraps_the_base64_payload() {
assert_eq!(osc52_sequence("foo"), "\x1b]52;c;Zm9v\x07");
}
#[test]
#[ignore = "touches the real system clipboard"]
fn round_trips_the_clipboard() {
let saved = paste();
assert!(
copy("ikigai clipboard test ✓"),
"no clipboard writer available"
);
assert_eq!(paste().as_deref(), Some("ikigai clipboard test ✓"));
if let Some(previous) = saved {
copy(&previous); }
}
}