use std::{
env,
ffi::OsStr,
io::Write,
process::{Command, Stdio},
};
struct ClipboardCommand {
program: &'static str,
args: &'static [&'static str],
}
pub fn write_native(text: &str) -> bool {
if prefers_osc52() {
return false;
}
clipboard_commands()
.iter()
.any(|command| run_clipboard_command(command, text))
}
pub fn prefers_osc52() -> bool {
env::var_os("SSH_CONNECTION").is_some() || env::var_os("SSH_TTY").is_some()
}
pub fn preferred_tool() -> Option<&'static str> {
clipboard_commands()
.iter()
.find(|command| on_path(command.program))
.map(|command| command.program)
}
fn on_path(program: &str) -> bool {
let Some(path_var) = env::var_os("PATH") else {
return false;
};
on_path_in(program, &path_var)
}
#[cfg(unix)]
const EXECUTABLE_BITS: u32 = 0o111;
fn on_path_in(program: &str, path: &OsStr) -> bool {
env::split_paths(path).any(|dir| {
let candidate = dir.join(program);
let Ok(meta) = std::fs::metadata(&candidate) else {
return false;
};
if !meta.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & EXECUTABLE_BITS != 0
}
#[cfg(not(unix))]
{
true
}
})
}
#[cfg(target_os = "macos")]
fn clipboard_commands() -> Vec<ClipboardCommand> {
vec![ClipboardCommand {
program: "pbcopy",
args: &[],
}]
}
#[cfg(all(unix, not(target_os = "macos")))]
fn clipboard_commands() -> Vec<ClipboardCommand> {
let mut commands = Vec::new();
if env::var_os("WAYLAND_DISPLAY").is_some() {
commands.push(ClipboardCommand {
program: "wl-copy",
args: &["--type", "text/plain;charset=utf-8"],
});
}
if env::var_os("DISPLAY").is_some() {
commands.push(ClipboardCommand {
program: "xclip",
args: &["-selection", "clipboard", "-in"],
});
commands.push(ClipboardCommand {
program: "xsel",
args: &["--clipboard", "--input"],
});
}
commands
}
#[cfg(not(unix))]
fn clipboard_commands() -> Vec<ClipboardCommand> {
Vec::new()
}
fn run_clipboard_command(command: &ClipboardCommand, text: &str) -> bool {
let Ok(mut child) = Command::new(command.program)
.args(command.args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return false;
};
let written = child
.stdin
.take()
.is_some_and(|mut stdin| stdin.write_all(text.as_bytes()).is_ok());
let exited = child.wait().map(|status| status.success()).unwrap_or(false);
written && exited
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
const NON_EXECUTABLE_MODE: u32 = 0o644;
#[cfg(unix)]
const EXECUTABLE_MODE: u32 = 0o755;
#[test]
fn preferred_tool_only_reports_a_tool_that_exists_on_path() {
match preferred_tool() {
Some(tool) => assert!(on_path(tool), "{tool} was reported but is not on PATH"),
None => {
for command in clipboard_commands() {
assert!(!on_path(command.program));
}
},
}
}
#[cfg(unix)]
#[test]
fn on_path_in_accepts_a_non_utf8_path() {
use std::{
ffi::OsString,
fs,
os::unix::{ffi::OsStringExt, fs::PermissionsExt},
};
let dir = std::env::temp_dir().join(format!("muster-clip-utf8-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&dir).unwrap();
let tool = dir.join("muster-fake-tool");
fs::write(&tool, "").unwrap();
fs::set_permissions(&tool, fs::Permissions::from_mode(EXECUTABLE_MODE)).unwrap();
let mut path_var = OsString::from_vec(vec![b'/', b't', b'm', b'p', b'/', 0xFF]);
path_var.push(":");
path_var.push(&dir);
assert!(on_path_in("muster-fake-tool", &path_var));
fs::remove_dir_all(dir).unwrap();
}
#[cfg(unix)]
#[test]
fn on_path_in_rejects_non_executable_files() {
use std::{ffi::OsString, fs, os::unix::fs::PermissionsExt};
let dir = std::env::temp_dir().join(format!("muster-clipboard-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&dir).unwrap();
let program = "fake-clipboard-tool";
let path = dir.join(program);
fs::write(&path, "#!/bin/sh\n").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(NON_EXECUTABLE_MODE)).unwrap();
let path_var: OsString = dir.as_os_str().to_os_string();
assert!(
!on_path_in(program, &path_var),
"non-executable file must not match"
);
fs::set_permissions(&path, fs::Permissions::from_mode(EXECUTABLE_MODE)).unwrap();
assert!(on_path_in(program, &path_var), "executable file must match");
fs::remove_dir_all(dir).unwrap();
}
}