use std::io::Write;
#[cfg(not(windows))]
use std::process::{Command, Stdio};
use std::sync::{Mutex, OnceLock};
fn cell() -> &'static Mutex<String> {
static CELL: OnceLock<Mutex<String>> = OnceLock::new();
CELL.get_or_init(|| Mutex::new(String::new()))
}
fn system_enabled() -> &'static Mutex<bool> {
static ENABLED: OnceLock<Mutex<bool>> = OnceLock::new();
ENABLED.get_or_init(|| Mutex::new(false))
}
pub fn enable_system() {
*system_enabled().lock().unwrap() = true;
}
fn system_is_enabled() -> bool {
*system_enabled().lock().unwrap()
}
pub fn register() -> String {
cell().lock().unwrap().clone()
}
pub fn set_register(text: &str) {
*cell().lock().unwrap() = text.to_string();
}
pub fn set(text: &str) {
set_register(text);
if !system_is_enabled() {
return;
}
emit_osc52(text);
Provider::detect().set(text);
}
pub fn get() -> String {
if system_is_enabled() {
let external = Provider::detect().get();
if !external.is_empty() {
return external;
}
}
register()
}
fn emit_osc52(text: &str) {
let mut out = std::io::stdout();
let _ = write!(out, "\x1b]52;c;{}\x07", base64(text.as_bytes()));
let _ = out.flush();
}
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 b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
out.push(ALPHABET[(n >> 18) as usize & 63] as char);
out.push(ALPHABET[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[n as usize & 63] as char
} else {
'='
});
}
out
}
enum Provider {
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
Pasteboard,
#[cfg(not(windows))]
Wayland,
#[cfg(not(windows))]
XClip,
#[cfg(not(windows))]
XSel,
#[cfg(not(windows))]
Tmux,
#[cfg(not(windows))]
Termux,
#[cfg(not(windows))]
None,
}
pub fn provider_name() -> &'static str {
match Provider::detect() {
#[cfg(windows)]
Provider::Windows => "windows",
#[cfg(target_os = "macos")]
Provider::Pasteboard => "pbcopy",
#[cfg(not(windows))]
Provider::Wayland => "wl-copy",
#[cfg(not(windows))]
Provider::XClip => "xclip",
#[cfg(not(windows))]
Provider::XSel => "xsel",
#[cfg(not(windows))]
Provider::Tmux => "tmux",
#[cfg(not(windows))]
Provider::Termux => "termux-clipboard",
#[cfg(not(windows))]
Provider::None => "none (internal register only)",
}
}
#[cfg(not(windows))]
fn has(binary: &str) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("command -v {binary}"))
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(windows))]
fn env_set(name: &str) -> bool {
std::env::var_os(name).is_some_and(|v| !v.is_empty())
}
impl Provider {
fn detect() -> &'static Self {
static PROVIDER: OnceLock<Provider> = OnceLock::new();
PROVIDER.get_or_init(Self::detect_uncached)
}
#[cfg(windows)]
fn detect_uncached() -> Self {
Self::Windows
}
#[cfg(not(windows))]
fn detect_uncached() -> Self {
if env_set("TMUX") && has("tmux") {
return Self::Tmux;
}
if has("termux-clipboard-set") {
return Self::Termux;
}
#[cfg(target_os = "macos")]
if has("pbcopy") {
return Self::Pasteboard;
}
if env_set("WAYLAND_DISPLAY") && has("wl-copy") {
return Self::Wayland;
}
if env_set("DISPLAY") && has("xclip") {
return Self::XClip;
}
if env_set("DISPLAY") && has("xsel") {
return Self::XSel;
}
Self::None
}
#[cfg(not(windows))]
fn commands(&self) -> Option<(Vec<&'static str>, Vec<&'static str>)> {
match self {
#[cfg(target_os = "macos")]
Self::Pasteboard => Some((vec!["pbcopy"], vec!["pbpaste"])),
Self::Wayland => Some((
vec!["wl-copy", "--foreground", "--type", "text/plain"],
vec!["wl-paste", "--no-newline"],
)),
Self::XClip => Some((
vec!["xclip", "-i", "-selection", "clipboard"],
vec!["xclip", "-o", "-selection", "clipboard"],
)),
Self::XSel => Some((vec!["xsel", "-i", "-b"], vec!["xsel", "-o", "-b"])),
Self::Tmux => Some((
vec!["tmux", "load-buffer", "-w", "-"],
vec!["tmux", "save-buffer", "-"],
)),
Self::Termux => Some((vec!["termux-clipboard-set"], vec!["termux-clipboard-get"])),
Self::None => None,
}
}
#[cfg(windows)]
fn set(&self, text: &str) {
let _ = clipboard_win::set_clipboard(clipboard_win::formats::Unicode, text);
}
#[cfg(not(windows))]
fn set(&self, text: &str) {
let Some((write, _)) = self.commands() else {
return;
};
let Ok(mut child) = Command::new(write[0])
.args(&write[1..])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return;
};
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(text.as_bytes());
}
drop(child.stdin.take());
let _ = child.wait();
}
#[cfg(windows)]
fn get(&self) -> String {
clipboard_win::get_clipboard(clipboard_win::formats::Unicode).unwrap_or_default()
}
#[cfg(not(windows))]
fn get(&self) -> String {
let Some((_, read)) = self.commands() else {
return String::new();
};
Command::new(read[0])
.args(&read[1..])
.stderr(Stdio::null())
.output()
.ok()
.filter(|out| out.status.success())
.map(|out| String::from_utf8_lossy(&out.stdout).into_owned())
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::base64;
#[test]
fn base64_matches_the_rfc_examples() {
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 base64_handles_non_ascii() {
assert_eq!(base64("é".as_bytes()), "w6k=");
assert_eq!(base64("日本".as_bytes()), "5pel5pys");
}
}