use std::io::Write;
use std::process::Command;
#[derive(Debug, PartialEq)]
pub enum ClipboardResult {
Success(ClipboardMethod),
Failed,
}
#[derive(Debug, PartialEq)]
pub enum ClipboardMethod {
Osc52,
Arboard,
OsCommand(&'static str),
}
impl std::fmt::Display for ClipboardMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClipboardMethod::Osc52 => write!(f, "OSC52"),
ClipboardMethod::Arboard => write!(f, "arboard"),
ClipboardMethod::OsCommand(cmd) => write!(f, "{}", cmd),
}
}
}
pub fn copy_to_clipboard(text: &str) -> ClipboardResult {
let debug_enabled = std::env::var("SLACK_RS_DEBUG").is_ok();
if should_try_osc52() {
if debug_enabled {
eprintln!("[DEBUG] Trying OSC52...");
}
if let Some(osc52_str) = generate_osc52_sequence(text) {
if try_osc52(&osc52_str).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] OSC52 succeeded");
}
return ClipboardResult::Success(ClipboardMethod::Osc52);
} else if debug_enabled {
eprintln!("[DEBUG] OSC52 failed");
}
} else if debug_enabled {
eprintln!("[DEBUG] OSC52 not applicable");
}
}
if debug_enabled {
eprintln!("[DEBUG] Trying arboard...");
}
if try_arboard(text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] arboard succeeded");
}
return ClipboardResult::Success(ClipboardMethod::Arboard);
} else if debug_enabled {
eprintln!("[DEBUG] arboard failed");
}
if let Some(result) = try_os_commands(text, debug_enabled) {
return result;
}
ClipboardResult::Failed
}
fn should_try_osc52() -> bool {
use std::io::IsTerminal;
let is_tty = std::io::stdout().is_terminal();
let is_ssh = std::env::var("SSH_CONNECTION").is_ok() || std::env::var("SSH_TTY").is_ok();
is_tty && is_ssh
}
pub fn generate_osc52_sequence(text: &str) -> Option<String> {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(text);
let in_tmux = std::env::var("TMUX").is_ok();
if in_tmux {
Some(format!("\x1bPtmux;\x1b\x1b]52;c;{}\x07\x1b\\", encoded))
} else {
Some(format!("\x1b]52;c;{}\x07", encoded))
}
}
fn try_osc52(osc52_sequence: &str) -> Result<(), std::io::Error> {
use std::io::IsTerminal;
if !std::io::stdout().is_terminal() {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"stdout is not a terminal",
));
}
let mut stdout = std::io::stdout();
stdout.write_all(osc52_sequence.as_bytes())?;
stdout.flush()?;
Ok(())
}
fn try_arboard(text: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut clipboard = arboard::Clipboard::new()?;
clipboard.set_text(text)?;
Ok(())
}
fn try_os_commands(text: &str, debug_enabled: bool) -> Option<ClipboardResult> {
#[cfg(target_os = "macos")]
{
if debug_enabled {
eprintln!("[DEBUG] Trying pbcopy...");
}
if try_command_with_stdin("pbcopy", &[], text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] pbcopy succeeded");
}
return Some(ClipboardResult::Success(ClipboardMethod::OsCommand(
"pbcopy",
)));
} else if debug_enabled {
eprintln!("[DEBUG] pbcopy failed");
}
}
#[cfg(target_os = "windows")]
{
if debug_enabled {
eprintln!("[DEBUG] Trying clip...");
}
if try_command_with_stdin("cmd", &["/C", "clip"], text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] clip succeeded");
}
return Some(ClipboardResult::Success(ClipboardMethod::OsCommand("clip")));
} else if debug_enabled {
eprintln!("[DEBUG] clip failed");
}
}
#[cfg(target_os = "linux")]
{
if debug_enabled {
eprintln!("[DEBUG] Trying wl-copy...");
}
if command_exists("wl-copy") {
if try_command_with_stdin("wl-copy", &[], text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] wl-copy succeeded");
}
return Some(ClipboardResult::Success(ClipboardMethod::OsCommand(
"wl-copy",
)));
} else if debug_enabled {
eprintln!("[DEBUG] wl-copy failed");
}
} else if debug_enabled {
eprintln!("[DEBUG] wl-copy not found");
}
if debug_enabled {
eprintln!("[DEBUG] Trying xclip...");
}
if command_exists("xclip") {
if try_command_with_stdin("xclip", &["-selection", "clipboard"], text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] xclip succeeded");
}
return Some(ClipboardResult::Success(ClipboardMethod::OsCommand(
"xclip",
)));
} else if debug_enabled {
eprintln!("[DEBUG] xclip failed");
}
} else if debug_enabled {
eprintln!("[DEBUG] xclip not found");
}
if debug_enabled {
eprintln!("[DEBUG] Trying xsel...");
}
if command_exists("xsel") {
if try_command_with_stdin("xsel", &["--clipboard", "--input"], text).is_ok() {
if debug_enabled {
eprintln!("[DEBUG] xsel succeeded");
}
return Some(ClipboardResult::Success(ClipboardMethod::OsCommand("xsel")));
} else if debug_enabled {
eprintln!("[DEBUG] xsel failed");
}
} else if debug_enabled {
eprintln!("[DEBUG] xsel not found");
}
}
None
}
#[cfg(target_os = "linux")]
fn command_exists(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.output()
.or_else(|_| Command::new(cmd).arg("-v").output())
.is_ok()
}
fn try_command_with_stdin(
cmd: &str,
args: &[&str],
input: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut child = Command::new(cmd)
.args(args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(input.as_bytes())?;
stdin.flush()?;
drop(stdin);
}
let status = child.wait()?;
if status.success() {
Ok(())
} else {
Err(format!("Command failed with status: {}", status).into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[serial_test::serial]
fn test_generate_osc52_sequence_without_tmux() {
let original = std::env::var("TMUX").ok();
std::env::remove_var("TMUX");
let text = "Hello, World!";
let result = generate_osc52_sequence(text);
assert!(result.is_some());
let sequence = result.unwrap();
assert!(sequence.contains("SGVsbG8sIFdvcmxkIQ=="));
assert!(sequence.starts_with("\x1b]52;c;"));
assert!(sequence.ends_with("\x07"));
assert!(!sequence.contains("Ptmux"));
if let Some(val) = original {
std::env::set_var("TMUX", val);
}
}
#[test]
#[serial_test::serial]
fn test_generate_osc52_sequence_with_tmux() {
let original = std::env::var("TMUX").ok();
std::env::set_var("TMUX", "/tmp/tmux-1000/default,12345,0");
let text = "Hello, World!";
let result = generate_osc52_sequence(text);
assert!(result.is_some());
let sequence = result.unwrap();
assert!(sequence.contains("SGVsbG8sIFdvcmxkIQ=="));
assert!(sequence.starts_with("\x1bPtmux;"));
assert!(sequence.ends_with("\x1b\\"));
assert!(sequence.contains("\x1b\x1b]52;c;"));
match original {
Some(val) => std::env::set_var("TMUX", val),
None => std::env::remove_var("TMUX"),
}
}
#[test]
fn test_should_try_osc52_requires_ssh_env() {
let _ = should_try_osc52();
}
#[test]
fn test_clipboard_method_display() {
assert_eq!(format!("{}", ClipboardMethod::Osc52), "OSC52");
assert_eq!(format!("{}", ClipboardMethod::Arboard), "arboard");
assert_eq!(
format!("{}", ClipboardMethod::OsCommand("pbcopy")),
"pbcopy"
);
}
#[test]
fn test_clipboard_result_enum() {
let success = ClipboardResult::Success(ClipboardMethod::Arboard);
let failed = ClipboardResult::Failed;
assert!(matches!(success, ClipboardResult::Success(_)));
assert!(matches!(failed, ClipboardResult::Failed));
}
#[test]
#[cfg(target_os = "linux")]
fn test_command_exists() {
assert!(command_exists("ls") || command_exists("echo"));
assert!(!command_exists(
"this-command-definitely-does-not-exist-12345"
));
}
#[test]
#[serial_test::serial]
fn test_osc52_sequence_format() {
let original_tmux = std::env::var("TMUX").ok();
std::env::remove_var("TMUX");
let seq = generate_osc52_sequence("test").unwrap();
assert!(seq.starts_with("\x1b]52;c;"));
assert!(seq.ends_with("\x07"));
std::env::set_var("TMUX", "dummy");
let seq_tmux = generate_osc52_sequence("test").unwrap();
assert!(seq_tmux.starts_with("\x1bPtmux;"));
assert!(seq_tmux.contains("\x1b\x1b]52;c;"));
assert!(seq_tmux.ends_with("\x1b\\"));
match original_tmux {
Some(val) => std::env::set_var("TMUX", val),
None => std::env::remove_var("TMUX"),
}
}
}