From 25ed6dc78fb8a0254ce3e8ecf47a1ce0daa721de Mon Sep 17 00:00:00 2001 From: Jiahao Li Date: Sat, 23 Nov 2019 17:49:15 -0500 Subject: [PATCH 1/2] Adds OSC52 copy escape sequence support Currently I can only get the copied content in the tmux copy buffer, not in the system clipboard. This commit adds an option to print the copied text as a OSC52 copy escape sequence, which in supported terminals (tested in iTerm) will be copied to the system clipboard. --- Cargo.toml | 1 + README.md | 13 +++++++++++++ src/main.rs | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5ffbe36..f8eb3b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" termion = "1.5" regex = "1.3.1" clap = "2.33.0" +base64 = "0.11.0" [[bin]] name = "thumbs" diff --git a/README.md b/README.md index 63162b7..3aabcc7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` * [@thumbs-select-fg-color](#thumbs-select-fg-color) * [@thumbs-select-bg-color](#thumbs-select-bg-color) * [@thumbs-contrast](#thumbs-contrast) +* [@thumbs-osc52](#thumbs-osc52) ### @thumbs-key @@ -273,6 +274,18 @@ For example: set -g @thumbs-contrast 1 ``` +### @thumbs-osc52 + +`default: 0` + +If this is set to `1`, `tmux-thumbs` will print a OSC52 copy escape sequence when you select a match, in addition to running the pick command. This sequence, in terminals that support it (e.g. iTerm), allows the content to be copied into the system clipboard in addition to the tmux copy buffer. + +For example: + +``` +set -g @thumbs-osc52 1 +``` + #### Colors This is the list of available colors: diff --git a/src/main.rs b/src/main.rs index eda20eb..3bd1721 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +extern crate base64; extern crate clap; extern crate termion; @@ -10,7 +11,7 @@ use self::clap::{App, Arg}; use clap::crate_version; use std::fs::OpenOptions; use std::io::prelude::*; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; fn app_args<'a>() -> clap::ArgMatches<'a> { App::new("thumbs") @@ -84,6 +85,12 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .long("unique") .short("u"), ) + .arg( + Arg::with_name("osc52") + .help("Print OSC52 copy escape sequence in addition to running the pick command") + .long("osc52") + .short("o"), + ) .arg( Arg::with_name("position") .help("Hint position") @@ -124,6 +131,7 @@ fn main() { let multi = args.is_present("multi"); let reverse = args.is_present("reverse"); let unique = args.is_present("unique"); + let osc52 = args.is_present("osc52"); let contrast = args.is_present("contrast"); let regexp = if let Some(items) = args.values_of("regexp") { items.collect::>() @@ -182,6 +190,32 @@ fn main() { .collect::>() .join("\n"); + if osc52 { + let base64_text = base64::encode(text.as_bytes()); + let osc_seq = format!("\x1b]52;0;{}\x07", base64_text); + let tmux_seq = format!("\x1bPtmux;{}\x1b\\", osc_seq.replace("\x1b", "\x1b\x1b")); + + // When the user selects a match: + // 1. The `rustbox` object created in the `viewbox` above is dropped. + // 2. During its `drop`, the `rustbox` object sends a CSI 1049 escape + // sequence to tmux. + // 3. This escape sequence causes the `window_pane_alternate_off` function + // in tmux to be called. + // 4. In `window_pane_alternate_off`, tmux sets the needs-redraw flag in the + // pane. + // 5. If we print the OSC copy escape sequence before the redraw is completed, + // tmux will *not* send the sequence to the host terminal. See the following + // call chain in tmux: `input_dcs_dispatch` -> `screen_write_rawstring` + // -> `tty_write` -> `tty_client_ready`. In this case, `tty_client_ready` + // will return false, thus preventing the escape sequence from being sent. + // + // Therefore, for now we wait a little bit here for the redraw to finish. + std::thread::sleep(std::time::Duration::from_millis(100)); + + std::io::stdout().write_all(tmux_seq.as_bytes()).unwrap(); + std::io::stdout().flush().unwrap(); + } + if let Some(target) = target { let mut file = OpenOptions::new() .create(true) From 34d8fa03e9d609545ce4234e59b13bfef95c7875 Mon Sep 17 00:00:00 2001 From: Ferran Basora Date: Sat, 30 May 2020 15:45:37 +0000 Subject: [PATCH 2/2] Adapt old osc52 implementation to swapper and rustbox era OSC52 support was implemented before two major refactors in tmux-thubs: - Decouple tmux-thumbs from tmux - Removal of rustbox with termion I'm not sure if is the right decision, but I moved the arguments to swapper land because it looks like more tmux related. If it makes sense for other terminals to support this we can move it back. The question we need to answer here if it makes sense for standalone `thumbs`: ``` cat sample/test1 | ./target/release/thumbs --osc52 ``` --- Cargo.lock | 7 +++++++ src/main.rs | 34 --------------------------------- src/swapper.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- tmux-thumbs.sh | 9 +++++++++ 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a95911..bbe6f51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.1" @@ -138,6 +143,7 @@ dependencies = [ name = "thumbs" version = "0.4.1" dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -176,6 +182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" diff --git a/src/main.rs b/src/main.rs index 3bd1721..d33e078 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ mod view; use self::clap::{App, Arg}; use clap::crate_version; use std::fs::OpenOptions; -use std::io::prelude::*; use std::io::{self, Read, Write}; fn app_args<'a>() -> clap::ArgMatches<'a> { @@ -85,12 +84,6 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .long("unique") .short("u"), ) - .arg( - Arg::with_name("osc52") - .help("Print OSC52 copy escape sequence in addition to running the pick command") - .long("osc52") - .short("o"), - ) .arg( Arg::with_name("position") .help("Hint position") @@ -131,7 +124,6 @@ fn main() { let multi = args.is_present("multi"); let reverse = args.is_present("reverse"); let unique = args.is_present("unique"); - let osc52 = args.is_present("osc52"); let contrast = args.is_present("contrast"); let regexp = if let Some(items) = args.values_of("regexp") { items.collect::>() @@ -190,32 +182,6 @@ fn main() { .collect::>() .join("\n"); - if osc52 { - let base64_text = base64::encode(text.as_bytes()); - let osc_seq = format!("\x1b]52;0;{}\x07", base64_text); - let tmux_seq = format!("\x1bPtmux;{}\x1b\\", osc_seq.replace("\x1b", "\x1b\x1b")); - - // When the user selects a match: - // 1. The `rustbox` object created in the `viewbox` above is dropped. - // 2. During its `drop`, the `rustbox` object sends a CSI 1049 escape - // sequence to tmux. - // 3. This escape sequence causes the `window_pane_alternate_off` function - // in tmux to be called. - // 4. In `window_pane_alternate_off`, tmux sets the needs-redraw flag in the - // pane. - // 5. If we print the OSC copy escape sequence before the redraw is completed, - // tmux will *not* send the sequence to the host terminal. See the following - // call chain in tmux: `input_dcs_dispatch` -> `screen_write_rawstring` - // -> `tty_write` -> `tty_client_ready`. In this case, `tty_client_ready` - // will return false, thus preventing the escape sequence from being sent. - // - // Therefore, for now we wait a little bit here for the redraw to finish. - std::thread::sleep(std::time::Duration::from_millis(100)); - - std::io::stdout().write_all(tmux_seq.as_bytes()).unwrap(); - std::io::stdout().flush().unwrap(); - } - if let Some(target) = target { let mut file = OpenOptions::new() .create(true) diff --git a/src/swapper.rs b/src/swapper.rs index f5994fa..cf07313 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -1,5 +1,8 @@ extern crate clap; +use std::fs::File; + +use std::io::Write; use self::clap::{App, Arg}; use clap::crate_version; use regex::Regex; @@ -47,6 +50,7 @@ pub struct Swapper<'a> { dir: String, command: String, upcase_command: String, + osc52: bool, active_pane_id: Option, active_pane_height: Option, active_pane_scroll_position: Option, @@ -57,7 +61,7 @@ pub struct Swapper<'a> { } impl<'a> Swapper<'a> { - fn new(executor: Box<&'a mut dyn Executor>, dir: String, command: String, upcase_command: String) -> Swapper { + fn new(executor: Box<&'a mut dyn Executor>, dir: String, command: String, upcase_command: String, osc52: bool) -> Swapper { let since_the_epoch = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards"); @@ -68,6 +72,7 @@ impl<'a> Swapper<'a> { dir, command, upcase_command, + osc52, active_pane_id: None, active_pane_height: None, active_pane_scroll_position: None, @@ -247,12 +252,44 @@ impl<'a> Swapper<'a> { self.executor.execute(params); } + pub fn send_osc52(&mut self) { + } + + pub fn execute_command(&mut self) { let content = self.content.clone().unwrap(); let mut splitter = content.splitn(2, ':'); if let Some(upcase) = splitter.next() { if let Some(text) = splitter.next() { + if self.osc52 { + let base64_text = base64::encode(text.as_bytes()); + let osc_seq = format!("\x1b]52;0;{}\x07", base64_text); + let tmux_seq = format!("\x1bPtmux;{}\x1b\\", osc_seq.replace("\x1b", "\x1b\x1b")); + + // FIXME: Review if this comment is still rellevant + // + // When the user selects a match: + // 1. The `rustbox` object created in the `viewbox` above is dropped. + // 2. During its `drop`, the `rustbox` object sends a CSI 1049 escape + // sequence to tmux. + // 3. This escape sequence causes the `window_pane_alternate_off` function + // in tmux to be called. + // 4. In `window_pane_alternate_off`, tmux sets the needs-redraw flag in the + // pane. + // 5. If we print the OSC copy escape sequence before the redraw is completed, + // tmux will *not* send the sequence to the host terminal. See the following + // call chain in tmux: `input_dcs_dispatch` -> `screen_write_rawstring` + // -> `tty_write` -> `tty_client_ready`. In this case, `tty_client_ready` + // will return false, thus preventing the escape sequence from being sent. + // + // Therefore, for now we wait a little bit here for the redraw to finish. + std::thread::sleep(std::time::Duration::from_millis(100)); + + std::io::stdout().write_all(tmux_seq.as_bytes()).unwrap(); + std::io::stdout().flush().unwrap(); + } + let execute_command = if upcase.trim_end() == "true" { self.upcase_command.clone() } else { @@ -302,7 +339,7 @@ mod tests { fn retrieve_active_pane() { let last_command_outputs = vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()]; let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string()); + let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string(), false); swapper.capture_active_pane(); @@ -318,7 +355,7 @@ mod tests { "%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(), ]; let mut executor = TestShell::new(last_command_outputs); - let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string()); + let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string(), false); swapper.capture_active_pane(); swapper.execute_thumbs(); @@ -352,6 +389,12 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .long("upcase-command") .default_value("tmux set-buffer {} && tmux paste-buffer"), ) + .arg( + Arg::with_name("osc52") + .help("Print OSC52 copy escape sequence in addition to running the pick command") + .long("osc52") + .short("o"), + ) .get_matches() } @@ -360,6 +403,7 @@ fn main() -> std::io::Result<()> { let dir = args.value_of("dir").unwrap(); let command = args.value_of("command").unwrap(); let upcase_command = args.value_of("upcase_command").unwrap(); + let osc52 = args.is_present("osc52"); if dir.is_empty() { panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") @@ -371,6 +415,7 @@ fn main() -> std::io::Result<()> { dir.to_string(), command.to_string(), upcase_command.to_string(), + osc52, ); swapper.capture_active_pane(); diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh index ec6b736..7be8a3f 100755 --- a/tmux-thumbs.sh +++ b/tmux-thumbs.sh @@ -4,6 +4,14 @@ PARAMS=() +function add-boolean-param { + VALUE=$(tmux show -vg @thumbs-$1 2> /dev/null) + + if [[ "${VALUE}" == "1" ]]; then + PARAMS+=("--$1") + fi +} + function add-option-param { VALUE=$(tmux show -vg @thumbs-$1 2> /dev/null) @@ -14,6 +22,7 @@ function add-option-param { add-option-param "command" add-option-param "upcase-command" +add-boolean-param "osc52" # Remove empty arguments from PARAMS. # Otherwise, they would choke up tmux-thumbs when passed to it.