use anyhow::{Context, Result};
use std::env;
use std::process::Command;
fn run_command(command: &str, args: &[&str]) -> Result<std::process::Output> {
Command::new(command)
.args(args)
.output()
.with_context(|| format!("Failed to run {} with args {:?}", command, args))
}
fn run_command_timeout(
command: &str,
args: &[&str],
_timeout_ms: u64,
) -> Result<std::process::Output> {
run_command(command, args)
}
fn is_wsl() -> bool {
env::var("WSL_DISTRO_NAME").is_ok()
|| env::var("WSLENV").is_ok()
|| std::path::Path::new("/proc/version").exists()
&& std::fs::read_to_string("/proc/version")
.map(|v| v.contains("microsoft") || v.contains("WSL"))
.unwrap_or(false)
}
fn is_wayland() -> bool {
env::var("WAYLAND_DISPLAY").is_ok()
}
fn is_termux() -> bool {
env::var("TERMUX_VERSION").is_ok()
}
fn copy_via_wl_copy(text: &str) -> Option<Result<()>> {
let output = run_command("wl-copy", &[text]).ok()?;
if output.status.success() {
Some(Ok(()))
} else {
Some(Err(anyhow::anyhow!("wl-copy failed")))
}
}
fn copy_via_xclip(text: &str) -> Option<Result<()>> {
let output = Command::new("xclip")
.args(["-selection", "clipboard"])
.pipe_output()
.spawn()
.ok()?;
output.write_all(text.as_bytes()).ok()?;
drop(output);
Some(Ok(()))
}
fn copy_via_pbcopy(text: &str) -> Option<Result<()>> {
let mut child = Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
.ok()?;
use std::io::Write;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(text.as_bytes()).ok()?;
}
drop(child);
let output = child.wait_with_output().ok()?;
if output.status.success() {
Some(Ok(()))
} else {
Some(Err(anyhow::anyhow!("pbcopy failed")))
}
}
fn copy_via_clip(text: &str) -> Option<Result<()>> {
let output = run_command("cmd.exe", &["/c", &format!("echo {} | clip", text)]).ok()?;
if output.status.success() {
Some(Ok(()))
} else {
Some(Err(anyhow::anyhow!("clip failed")))
}
}
fn copy_via_powershell(text: &str) -> Option<Result<()>> {
let escaped = text.replace("'", "''").replace("`", "``");
let ps_script = format!("Set-Clipboard -Value '{}'", escaped);
let output = run_command_timeout(
"powershell.exe",
&["-NoProfile", "-Command", &ps_script],
3000,
)
.ok()?;
if output.status.success() {
Some(Ok(()))
} else {
Some(Err(anyhow::anyhow!("PowerShell Set-Clipboard failed")))
}
}
fn copy_via_termux(text: &str) -> Option<Result<()>> {
let output = run_command("termux-clipboard-set", &[text]).ok()?;
if output.status.success() {
Some(Ok(()))
} else {
Some(Err(anyhow::anyhow!("termux-clipboard-set failed")))
}
}
pub fn generate_osc52_sequence(text: &str) -> String {
use base64::Engine as _;
let base64_text = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
format!("\x1b]52;;{}δΈε€", base64_text)
}
pub fn copy_to_clipboard(text: &str) -> Result<()> {
if is_termux() {
copy_via_termux(text)
.ok_or_else(|| anyhow::anyhow!("Failed to copy to clipboard in Termux"))??
} else if cfg!(target_os = "linux") {
if is_wayland() {
copy_via_wl_copy(text)
.or_else(|| copy_via_xclip(text))
.ok_or_else(|| anyhow::anyhow!("No clipboard tool available for Linux"))??
} else {
copy_via_xclip(text)
.or_else(|| copy_via_wl_copy(text))
.ok_or_else(|| anyhow::anyhow!("No clipboard tool available for Linux"))??
}
} else if cfg!(target_os = "macos") {
copy_via_pbcopy(text).ok_or_else(|| anyhow::anyhow!("pbcopy failed"))??
} else if cfg!(target_os = "windows") {
copy_via_powershell(text)
.or_else(|| copy_via_clip(text))
.ok_or_else(|| anyhow::anyhow!("No clipboard tool available for Windows"))??
} else if is_wsl() {
copy_via_wl_copy(text)
.or_else(|| copy_via_xclip(text))
.or_else(|| copy_via_powershell(text))
.ok_or_else(|| anyhow::anyhow!("No clipboard tool available"))??
} else {
print!("{}", generate_osc52_sequence(text));
std::io::stdout().flush().ok();
Ok(())
}
}
pub fn is_clipboard_supported() -> bool {
if is_termux() {
return std::process::Command::new("termux-clipboard-set")
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
}
if cfg!(target_os = "linux") {
return std::process::Command::new("wl-copy")
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
|| std::process::Command::new("xclip")
.arg("-version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
}
if cfg!(target_os = "macos") {
return std::process::Command::new("pbcopy")
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
}
if cfg!(target_os = "windows") {
return true; }
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_osc52_sequence() {
let text = "Hello, World!";
let seq = generate_osc52_sequence(text);
assert!(seq.starts_with("\x1b]52;;"));
assert!(seq.ends_with("δΈε€"));
}
#[test]
fn test_generate_osc52_sequence_empty() {
let text = "";
let seq = generate_osc52_sequence(text);
assert!(seq.starts_with("\x1b]52;;"));
}
#[test]
fn test_generate_osc52_sequence_unicode() {
let text = "Hello π";
let seq = generate_osc52_sequence(text);
assert!(seq.starts_with("\x1b]52;;"));
}
#[test]
fn test_is_wsl_detection() {
let _ = is_wsl();
}
#[test]
fn test_is_wayland_detection() {
let _ = is_wayland();
}
#[test]
fn test_is_termux_detection() {
let _ = is_termux();
}
#[test]
fn test_is_clipboard_supported() {
let _ = is_clipboard_supported();
}
}