Copy text using OSC52 (#2548)

* Copy text using OSC52 if X/Wayland methods fail
* 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.

---------

Co-authored-by: Naseschwarz <naseschwarz@0x53a.de>
Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com>
This commit is contained in:
Johannes Agricola 2025-03-15 09:33:54 +01:00 committed by GitHub
parent 381ab45d3a
commit 3c1e35eec7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 14 deletions

View file

@ -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

1
Cargo.lock generated
View file

@ -1162,6 +1162,7 @@ dependencies = [
"anyhow",
"asyncgit",
"backtrace",
"base64",
"bitflags 2.8.0",
"bugreport",
"bwrap",

View file

@ -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"] }

View file

@ -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::<u8>::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");
}
}