use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use std::io::Write;
const OSC52_MAX_RAW_BYTES: usize = 100_000;
pub(crate) fn copy_to_clipboard(text: &str) -> Result<String, String> {
if !is_ssh_connection() {
try_arboard(text); }
if is_tmux() {
tmux_load_buffer(text); }
osc52_write(text)
}
fn is_ssh_connection() -> bool {
std::env::var("SSH_CONNECTION").is_ok()
}
fn is_tmux() -> bool {
std::env::var("TMUX").is_ok()
}
fn try_arboard(text: &str) {
if let Ok(mut cb) = arboard::Clipboard::new() {
let _ = cb.set_text(text);
}
}
fn tmux_load_buffer(text: &str) {
use std::io::Write as _;
use std::process::{Command, Stdio};
let Ok(mut child) = Command::new("tmux")
.args(["load-buffer", "-w", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return;
};
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
}
fn osc52_write(text: &str) -> Result<String, String> {
let raw = text.as_bytes();
if raw.len() > OSC52_MAX_RAW_BYTES {
return Err(format!(
"payload too large for OSC 52 ({} bytes, max {OSC52_MAX_RAW_BYTES})",
raw.len()
));
}
let encoded = B64.encode(raw);
let inner = format!("\x1b]52;c;{encoded}\x07");
let seq = if is_tmux() {
let doubled = inner.replace('\x1b', "\x1b\x1b");
format!("\x1bPtmux;{doubled}\x1b\\")
} else {
inner
};
write_to_tty(&seq)?;
Ok(if is_tmux() {
"to clipboard (via tmux)".to_string()
} else {
"to clipboard (via terminal)".to_string()
})
}
fn write_to_tty(data: &str) -> Result<(), String> {
#[cfg(unix)]
{
use std::fs::OpenOptions;
if let Ok(mut tty) = OpenOptions::new().write(true).open("/dev/tty") {
return tty
.write_all(data.as_bytes())
.and_then(|()| tty.flush())
.map_err(|e| format!("/dev/tty write error: {e}"));
}
}
let mut err = std::io::stderr().lock();
err.write_all(data.as_bytes())
.and_then(|()| err.flush())
.map_err(|e| format!("stderr write error: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn osc52_plain_sequence_structure() {
let encoded = B64.encode(b"hello, world");
let seq = format!("\x1b]52;c;{encoded}\x07");
assert!(
seq.starts_with("\x1b]52;c;"),
"must start with OSC 52 header"
);
assert!(seq.ends_with('\x07'), "must end with BEL");
assert!(seq.contains(&encoded));
}
#[test]
fn osc52_tmux_dcs_doubles_inner_esc_and_ends_with_st() {
let encoded = B64.encode(b"hi");
let inner = format!("\x1b]52;c;{encoded}\x07");
let doubled = inner.replace('\x1b', "\x1b\x1b");
let wrapped = format!("\x1bPtmux;{doubled}\x1b\\");
assert!(wrapped.starts_with("\x1bPtmux;"));
assert!(wrapped.ends_with("\x1b\\"));
let esc_count = wrapped.chars().filter(|&c| c == '\x1b').count();
assert_eq!(esc_count, 4, "1 inner ESC doubled + DCS open + ST = 4");
}
#[test]
fn osc52_base64_round_trips_including_emoji() {
let original = "koda clipboard test 🐶";
let encoded = B64.encode(original.as_bytes());
let decoded = B64.decode(&encoded).unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), original);
}
#[test]
fn osc52_rejects_oversized_payload() {
let big_len = OSC52_MAX_RAW_BYTES + 1;
let result: Result<(), _> = if big_len > OSC52_MAX_RAW_BYTES {
Err("too large")
} else {
Ok(())
};
assert!(result.is_err());
}
#[test]
fn ssh_detection_uses_ssh_connection_not_ssh_tty() {
let _ = is_ssh_connection();
}
#[test]
fn tmux_detection_smoke() {
let _ = is_tmux();
}
}