From 6598c88d35268eec8f0902799152b34680f44fdd Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sat, 11 Apr 2026 03:16:06 +0300 Subject: fix: prefer native clipboard tools over osc52 fallback --- src/platform/fallback.rs | 5 +++ src/platform/linux.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++- src/platform/macos.rs | 42 ++++++++++++++++++++- src/platform/mod.rs | 6 +++ src/selection.rs | 25 +++++++++---- 5 files changed, 165 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index 4bf78352..e90cdaf0 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -24,3 +24,8 @@ pub fn signal_processes(_pids: &[u32], _signal: Signal) {} pub fn process_exists(_pid: u32) -> bool { false } + +/// Unsupported platform stub. +pub fn write_clipboard(_bytes: &[u8]) -> bool { + false +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index b6fc0272..f95c7418 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,6 +1,10 @@ -use std::path::PathBuf; +use std::{ + io::Write, + path::PathBuf, + process::{Command, Stdio}, +}; -use super::{ForegroundJob, ForegroundProcess, Signal}; +use super::{ClipboardCommand, ForegroundJob, ForegroundProcess, Signal}; /// Collect the foreground terminal job for a given child PID. pub fn foreground_job(child_pid: u32) -> Option { @@ -142,9 +146,98 @@ pub fn process_exists(pid: u32) -> bool { } } +pub fn write_clipboard(bytes: &[u8]) -> bool { + for command in clipboard_commands() { + if run_clipboard_command(&command, bytes) { + return true; + } + } + false +} + +fn clipboard_commands() -> Vec { + let mut commands = Vec::new(); + + if std::env::var_os("WAYLAND_DISPLAY").is_some() { + commands.push(ClipboardCommand { + program: "wl-copy", + args: &["--type", "text/plain;charset=utf-8"], + }); + } + + if std::env::var_os("DISPLAY").is_some() { + commands.push(ClipboardCommand { + program: "xclip", + args: &["-selection", "clipboard", "-in"], + }); + commands.push(ClipboardCommand { + program: "xsel", + args: &["--clipboard", "--input"], + }); + } + + commands +} + +fn run_clipboard_command(command: &ClipboardCommand, bytes: &[u8]) -> bool { + let mut child = match Command::new(command.program) + .args(command.args) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + { + Ok(child) => child, + Err(_) => return false, + }; + + let Some(mut stdin) = child.stdin.take() else { + let _ = child.kill(); + let _ = child.wait(); + return false; + }; + + if stdin.write_all(bytes).is_err() { + let _ = child.kill(); + let _ = child.wait(); + return false; + } + drop(stdin); + + child.wait().map(|status| status.success()).unwrap_or(false) +} + fn process_session_id(pid: u32) -> Option { let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?; let rest = stat.get(stat.rfind(')')? + 2..)?; let fields: Vec<&str> = rest.split_whitespace().collect(); fields.get(3)?.parse().ok() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clipboard_commands_prefer_wayland_when_available() { + unsafe { + std::env::set_var("WAYLAND_DISPLAY", "wayland-0"); + std::env::remove_var("DISPLAY"); + } + let commands = clipboard_commands(); + assert_eq!(commands.len(), 1); + assert_eq!(commands[0].program, "wl-copy"); + } + + #[test] + fn clipboard_commands_include_x11_fallbacks() { + unsafe { + std::env::remove_var("WAYLAND_DISPLAY"); + std::env::set_var("DISPLAY", ":0"); + } + let commands = clipboard_commands(); + assert_eq!(commands.len(), 2); + assert_eq!(commands[0].program, "xclip"); + assert_eq!(commands[1].program, "xsel"); + } +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index a5653d2b..f5de67ce 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,8 +1,10 @@ use std::ffi::OsStr; +use std::io::Write; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; -use super::{ForegroundJob, ForegroundProcess, Signal}; +use super::{ClipboardCommand, ForegroundJob, ForegroundProcess, Signal}; /// Collect the foreground terminal job for a given child PID. pub fn foreground_job(child_pid: u32) -> Option { @@ -182,6 +184,44 @@ fn process_comm_name(pid: u32) -> Option { process_bsdinfo(pid).and_then(|info| comm_from_bsdinfo(&info)) } +pub fn write_clipboard(bytes: &[u8]) -> bool { + run_clipboard_command( + &ClipboardCommand { + program: "pbcopy", + args: &[], + }, + bytes, + ) +} + +fn run_clipboard_command(command: &ClipboardCommand, bytes: &[u8]) -> bool { + let mut child = match Command::new(command.program) + .args(command.args) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + { + Ok(child) => child, + Err(_) => return false, + }; + + let Some(mut stdin) = child.stdin.take() else { + let _ = child.kill(); + let _ = child.wait(); + return false; + }; + + if stdin.write_all(bytes).is_err() { + let _ = child.kill(); + let _ = child.wait(); + return false; + } + drop(stdin); + + child.wait().map(|status| status.success()).unwrap_or(false) +} + fn process_bsdinfo(pid: u32) -> Option { let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::() as libc::c_int; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 8023cd11..58675d33 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -24,6 +24,12 @@ pub enum Signal { Kill, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardCommand { + pub program: &'static str, + pub args: &'static [&'static str], +} + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] diff --git a/src/selection.rs b/src/selection.rs index 050964fd..9d29b08e 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -165,18 +165,24 @@ fn clamp_to_pane(screen_col: u16, screen_row: u16, pane_inner: Rect) -> (u16, u1 (clamped_row - pane_inner.y, clamped_col - pane_inner.x) } +fn osc52_sequence(bytes: &[u8]) -> String { + use base64::Engine; + let encoded = base64::engine::general_purpose::STANDARD.encode(bytes); + format!("\x1b]52;c;{encoded}\x07") +} + /// Write clipboard bytes to the system clipboard via OSC 52. /// /// OSC 52 format: `ESC ] 52 ; c ; BEL` /// -/// The terminal emulator (Ghostty, kitty, etc.) intercepts this -/// and sets the system clipboard. Ghostty has `clipboard-write = allow` -/// by default. +/// Some terminals still only honor BEL-terminated OSC 52 writes, so herdr +/// emits BEL here even though ST works in newer emulators. pub fn write_osc52_bytes(bytes: &[u8]) { - use base64::Engine; - let encoded = base64::engine::general_purpose::STANDARD.encode(bytes); - // Use ST (ESC \) terminator — more widely supported than BEL in some terminals - let sequence = format!("\x1b]52;c;{encoded}\x1b\\"); + if crate::platform::write_clipboard(bytes) { + return; + } + + let sequence = osc52_sequence(bytes); let _ = std::io::stdout().write_all(sequence.as_bytes()); let _ = std::io::stdout().flush(); } @@ -198,6 +204,11 @@ mod tests { sel } + #[test] + fn osc52_sequence_uses_bel_terminator() { + assert_eq!(osc52_sequence(b"hello"), "\x1b]52;c;aGVsbG8=\x07"); + } + #[test] fn ordering_forward() { let sel = make_sel(2, 5, 4, 10); -- cgit v1.2.3