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 30941dcbb2..00fd0a9a5a 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..3bd8b2d6fc 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -63,33 +63,68 @@ 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 +#[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, + "\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); +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() { - return exec_copy_with_args( - "xsel", - &["--clipboard"], - text, - true, - ); + 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))] @@ -106,3 +141,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"); + } +}