From 2541bf4c4bba16e591500e58f028005e9f131973 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Wed, 5 Mar 2025 18:11:54 +0100 Subject: [PATCH 1/2] Copy text using OSC52 if X/Wayland methods fail --- CHANGELOG.md | 1 + Cargo.lock | 1 + Cargo.toml | 1 + src/clipboard.rs | 45 ++++++++++++++++++++++++++++++++++++++------- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c47274c74..d4cc478372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524)) * After commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476)) +* use OSC52 copying in case other methods fail [[@naseschwarz](https://github.com/naseschwarz)] ([#2366](https://github.com/gitui-org/gitui/issues/2366)) ## [0.27.0] - 2024-01-14 diff --git a/Cargo.lock b/Cargo.lock index 66c9ac65b7..3f78a915d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1162,6 +1162,7 @@ dependencies = [ "anyhow", "asyncgit", "backtrace", + "base64", "bitflags 2.8.0", "bugreport", "bwrap", diff --git a/Cargo.toml b/Cargo.toml index c510a62b01..09ed43fba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ build = "build.rs" anyhow = "1.0" asyncgit = { path = "./asyncgit", version = "0.27.0", default-features = false } backtrace = "0.3" +base64 = "0.21" bitflags = "2.8" bugreport = "0.5.1" bwrap = { version = "1.3", features = ["use_std"] } diff --git a/src/clipboard.rs b/src/clipboard.rs index 649f285a0e..5cc5bf189a 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use base64::prelude::{Engine, BASE64_STANDARD}; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; @@ -63,10 +64,27 @@ fn is_wsl() -> bool { false } +// Copy text using escape sequence Ps = 5 2. +// This enables copying even if there is no Wayland or X socket available, +// e.g. via SSH, as long as it supported by the terminal. +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +fn copy_string_osc52(text: &str, out: &mut impl Write) -> Result<()> { + const OSC52_DESTINATION_CLIPBOARD: char = 'c'; + write!( + out, + "\x1b]52;{destination};{encoded_text}\x07", + destination = OSC52_DESTINATION_CLIPBOARD, + encoded_text = BASE64_STANDARD.encode(text) + )?; + Ok(()) +} + #[cfg(all(target_family = "unix", not(target_os = "macos")))] pub fn copy_string(text: &str) -> Result<()> { if std::env::var("WAYLAND_DISPLAY").is_ok() { - return exec_copy_with_args("wl-copy", &[], text, false); + if exec_copy_with_args("wl-copy", &[], text, false).is_err() { + copy_string_osc52(text, &mut std::io::stdout())?; + } } if is_wsl() { @@ -81,12 +99,11 @@ pub fn copy_string(text: &str) -> Result<()> { ) .is_err() { - return exec_copy_with_args( - "xsel", - &["--clipboard"], - text, - true, - ); + if exec_copy_with_args("xsel", &["--clipboard"], text, true) + .is_err() + { + copy_string_osc52(text, &mut std::io::stdout())?; + } } Ok(()) @@ -106,3 +123,17 @@ pub fn copy_string(text: &str) -> Result<()> { pub fn copy_string(text: &str) -> Result<()> { exec_copy("clip", text) } + +#[cfg(test)] +mod tests { + #[test] + fn test_copy_string_osc52() { + let mut buffer = Vec::::new(); + { + let mut cursor = std::io::Cursor::new(&mut buffer); + super::copy_string_osc52("foo", &mut cursor).unwrap(); + } + let output = String::from_utf8(buffer).unwrap(); + assert_eq!(output, "\x1b]52;c;Zm9v\x07"); + } +} From 85d7018c3fdc033ed8d3c913f11226f041af2566 Mon Sep 17 00:00:00 2001 From: Naseschwarz Date: Wed, 5 Mar 2025 18:26:33 +0100 Subject: [PATCH 2/2] Move Wayland/X string copying out of copy_string Copying logic seems too nested to comprehend with the introcution of two paths towards OSC52 otherwise. --- src/clipboard.rs | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/clipboard.rs b/src/clipboard.rs index 5cc5bf189a..3bd8b2d6fc 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; -use base64::prelude::{Engine, BASE64_STANDARD}; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; @@ -68,7 +67,12 @@ fn is_wsl() -> bool { // This enables copying even if there is no Wayland or X socket available, // e.g. via SSH, as long as it supported by the terminal. // See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +#[cfg(any( + all(target_family = "unix", not(target_os = "macos")), + test +))] fn copy_string_osc52(text: &str, out: &mut impl Write) -> Result<()> { + use base64::prelude::{Engine, BASE64_STANDARD}; const OSC52_DESTINATION_CLIPBOARD: char = 'c'; write!( out, @@ -80,33 +84,47 @@ fn copy_string_osc52(text: &str, out: &mut impl Write) -> Result<()> { } #[cfg(all(target_family = "unix", not(target_os = "macos")))] -pub fn copy_string(text: &str) -> Result<()> { - if std::env::var("WAYLAND_DISPLAY").is_ok() { - if exec_copy_with_args("wl-copy", &[], text, false).is_err() { - copy_string_osc52(text, &mut std::io::stdout())?; - } +fn copy_string_wayland(text: &str) -> Result<()> { + if exec_copy_with_args("wl-copy", &[], text, false).is_ok() { + return Ok(()); } - if is_wsl() { - return exec_copy_with_args("clip.exe", &[], text, false); - } + copy_string_osc52(text, &mut std::io::stdout()) +} +#[cfg(all(target_family = "unix", not(target_os = "macos")))] +fn copy_string_x(text: &str) -> Result<()> { if exec_copy_with_args( "xclip", &["-selection", "clipboard"], text, false, ) - .is_err() + .is_ok() { - if exec_copy_with_args("xsel", &["--clipboard"], text, true) - .is_err() - { - copy_string_osc52(text, &mut std::io::stdout())?; - } + return Ok(()); } - Ok(()) + if exec_copy_with_args("xsel", &["--clipboard"], text, true) + .is_ok() + { + return Ok(()); + } + + copy_string_osc52(text, &mut std::io::stdout()) +} + +#[cfg(all(target_family = "unix", not(target_os = "macos")))] +pub fn copy_string(text: &str) -> Result<()> { + if std::env::var("WAYLAND_DISPLAY").is_ok() { + return copy_string_wayland(text); + } + + if is_wsl() { + return exec_copy_with_args("clip.exe", &[], text, false); + } + + copy_string_x(text) } #[cfg(any(target_os = "macos", windows))]