aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-11 03:16:06 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-11 03:16:28 +0300
commit6598c88d35268eec8f0902799152b34680f44fdd (patch)
treee75ac9251bd69b4ef1f4a895ac33a673dd108dbb /src
parentaa6da3c8335e871c1dbed1da721c39ae9fcbe97f (diff)
fix: prefer native clipboard tools over osc52 fallback
Diffstat (limited to 'src')
-rw-r--r--src/platform/fallback.rs5
-rw-r--r--src/platform/linux.rs97
-rw-r--r--src/platform/macos.rs42
-rw-r--r--src/platform/mod.rs6
-rw-r--r--src/selection.rs25
5 files changed, 165 insertions, 10 deletions
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<ForegroundJob> {
@@ -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<ClipboardCommand> {
+ 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<i32> {
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<ForegroundJob> {
@@ -182,6 +184,44 @@ fn process_comm_name(pid: u32) -> Option<String> {
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<libc::proc_bsdinfo> {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() 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 ; <base64> 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();
}
@@ -199,6 +205,11 @@ mod tests {
}
#[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);
assert_eq!(sel.ordered(), ((2, 5), (4, 10)));