use base64::Engine;
use std::io::Write;
#[cfg(not(target_os = "android"))]
pub fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
if std::env::var_os("SSH_CONNECTION").is_some() || std::env::var_os("SSH_TTY").is_some() {
return copy_via_osc52(text);
}
let error = match arboard::Clipboard::new() {
Ok(mut clipboard) => match clipboard.set_text(text.to_string()) {
Ok(()) => return Ok(()),
Err(err) => format!("clipboard unavailable: {err}"),
},
Err(err) => format!("clipboard unavailable: {err}"),
};
#[cfg(target_os = "linux")]
let error = if is_probably_wsl() {
match copy_via_wsl_clipboard(text) {
Ok(()) => return Ok(()),
Err(wsl_err) => format!("{error}; WSL fallback failed: {wsl_err}"),
}
} else {
error
};
Err(error)
}
#[cfg(not(target_os = "android"))]
fn copy_via_osc52(text: &str) -> Result<(), String> {
let sequence = osc52_sequence(text, std::env::var_os("TMUX").is_some());
#[cfg(unix)]
{
use std::fs::OpenOptions;
let mut tty = OpenOptions::new()
.write(true)
.open("/dev/tty")
.map_err(|e| format!("failed to open /dev/tty: {e}"))?;
tty.write_all(sequence.as_bytes())
.map_err(|e| format!("failed to write OSC 52: {e}"))?;
tty.flush()
.map_err(|e| format!("failed to flush OSC 52: {e}"))?;
}
#[cfg(windows)]
{
use std::io::stdout;
stdout()
.write_all(sequence.as_bytes())
.map_err(|e| format!("failed to write OSC 52: {e}"))?;
stdout()
.flush()
.map_err(|e| format!("failed to flush OSC 52: {e}"))?;
}
Ok(())
}
fn osc52_sequence(text: &str, tmux: bool) -> String {
let payload = base64::engine::general_purpose::STANDARD.encode(text);
if tmux {
format!("\x1bPtmux;\x1b\x1b]52;c;{payload}\x07\x1b\\")
} else {
format!("\x1b]52;c;{payload}\x07")
}
}
#[cfg(all(not(target_os = "android"), target_os = "linux"))]
fn copy_via_wsl_clipboard(text: &str) -> Result<(), String> {
use std::process::{Command, Stdio};
let mut child = Command::new("powershell.exe")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.args([
"-NoProfile",
"-Command",
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; \
$ErrorActionPreference = 'Stop'; \
$text = [Console]::In.ReadToEnd(); \
Set-Clipboard -Value $text",
])
.spawn()
.map_err(|e| format!("failed to spawn powershell.exe: {e}"))?;
let Some(mut stdin) = child.stdin.take() else {
let _ = child.kill();
return Err("failed to open powershell.exe stdin".to_string());
};
stdin
.write_all(text.as_bytes())
.map_err(|e| format!("failed to write to powershell.exe: {e}"))?;
drop(stdin);
let output = child
.wait_with_output()
.map_err(|e| format!("failed to wait for powershell.exe: {e}"))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(if stderr.is_empty() {
format!("powershell.exe exited with status {}", output.status)
} else {
format!("powershell.exe failed: {stderr}")
})
}
}
#[cfg(target_os = "linux")]
pub(crate) fn is_probably_wsl() -> bool {
if let Ok(version) = std::fs::read_to_string("/proc/version") {
let version_lower = version.to_lowercase();
if version_lower.contains("microsoft") || version_lower.contains("wsl") {
return true;
}
}
std::env::var_os("WSL_DISTRO_NAME").is_some() || std::env::var_os("WSL_INTEROP").is_some()
}
#[cfg(all(test, not(target_os = "android")))]
mod tests {
use super::*;
#[test]
fn osc52_sequence_encodes_text_for_terminal_clipboard() {
assert_eq!(osc52_sequence("hello", false), "\u{1b}]52;c;aGVsbG8=\u{7}");
}
#[test]
fn osc52_sequence_wraps_tmux_passthrough() {
assert_eq!(
osc52_sequence("hello", true),
"\u{1b}Ptmux;\u{1b}\u{1b}]52;c;aGVsbG8=\u{7}\u{1b}\\"
);
}
}