From e29b07f8e941182c81f7f5b62c5e092c01cd60f0 Mon Sep 17 00:00:00 2001 From: Ferran Basora Date: Sat, 25 Jun 2022 16:51:12 +0000 Subject: [PATCH] New set of available keys (Ctrl, Alt) A part of the common `tmux-thumbs` hints + the shift ones, we want to introduce a new set of combinations with `ctrl` and `alt` keys. This is a breaking change because the configuration options has been renamed. --- README.md | 26 +++++++++-- src/main.rs | 8 ++-- src/swapper.rs | 117 +++++++++++++++++++++++++++++++++---------------- src/view.rs | 51 ++++++++++++++++----- tmux-thumbs.sh | 9 ++-- 5 files changed, 151 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index d7063c0..8478775 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,19 @@ These are the list of matched patterns that will be highlighted by default. If you want to highlight a pattern that is not in this list you can add one or more with `--regexp` parameter. + +## Key shortcuts + +While in **[thumbs]** mode, you can use the following shortcuts: + +* a-z: copies selected match to the clipboard +* CTRL + a-z: copies selected match to the clipboard and triggers [@thumbs-ctrl-action](#thumbs-ctrl-action). +* SHIFT + a-z: copies selected match to the clipboard and triggers [@thumbs-shift-action](#thumbs-shift-action). +* ALT + a-z: copies selected match to the clipboard and triggers [@thumbs-alt-action](#thumbs-alt-action). +* CTRL + c: exit **[thumbs]** mode +* ESC: exit help or **[thumbs]** mode +* ?: show help. + ## Demo [![demo](https://asciinema.org/a/232775.png?ts=1)](https://asciinema.org/a/232775?autoplay=1) @@ -93,9 +106,13 @@ NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` * [@thumbs-unique](#thumbs-unique) * [@thumbs-position](#thumbs-position) * [@thumbs-regexp-N](#thumbs-regexp-N) -* [@thumbs-command](#thumbs-command) -* [@thumbs-upcase-command](#thumbs-upcase-command) -* [@thumbs-multi-command](#thumbs-multi-command) + +* [@thumbs-main-action](#thumbs-main-action) +* [@thumbs-shift-action](#thumbs-shift-action) +* [@thumbs-ctrl-action](#thumbs-ctrl-action) +* [@thumbs-alt-action](#thumbs-alt-action) +* [@thumbs-multi-action](#thumbs-multi-action) + * [@thumbs-bg-color](#thumbs-bg-color) * [@thumbs-fg-color](#thumbs-fg-color) * [@thumbs-hint-bg-color](#thumbs-hint-bg-color) @@ -106,6 +123,9 @@ NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` * [@thumbs-multi-bg-color](#thumbs-multi-bg-color) * [@thumbs-contrast](#thumbs-contrast) * [@thumbs-osc52](#thumbs-osc52) +* deprecated: [@thumbs-command](#thumbs-command) +* deprecated: [@thumbs-upcase-command](#thumbs-upcase-command) +* deprecated: [@thumbs-multi-command](#thumbs-multi-command) ### @thumbs-key diff --git a/src/main.rs b/src/main.rs index 49ded04..6261376 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { ) .arg( Arg::with_name("format") - .help("Specifies the out format for the picked hint. (%U: Upcase, %H: Hint)") + .help("Specifies the out format for the picked hint. (%K: Key, %H: Hint)") .long("format") .short("f") .default_value("%H"), @@ -198,12 +198,10 @@ fn main() { if !selected.is_empty() { let output = selected .iter() - .map(|(text, upcase)| { - let upcase_value = if *upcase { "true" } else { "false" }; - + .map(|(text, modkey)| { let mut output = format.to_string(); - output = str::replace(&output, "%U", upcase_value); + output = str::replace(&output, "%K", modkey); output = str::replace(&output, "%H", text.as_str()); output }) diff --git a/src/swapper.rs b/src/swapper.rs index 6cf1e89..b318949 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -1,5 +1,7 @@ extern crate clap; +mod view; + use self::clap::{App, Arg}; use clap::crate_version; use regex::Regex; @@ -58,9 +60,11 @@ fn dbg(msg: &str) { pub struct Swapper<'a> { executor: Box<&'a mut dyn Executor>, dir: String, - command: String, - upcase_command: String, - multi_command: String, + main_action: String, + shift_action: String, + ctrl_action: String, + alt_action: String, + multi_action: String, osc52: bool, active_pane_id: Option, active_pane_height: Option, @@ -75,9 +79,11 @@ impl<'a> Swapper<'a> { fn new( executor: Box<&'a mut dyn Executor>, dir: String, - command: String, - upcase_command: String, - multi_command: String, + main_action: String, + shift_action: String, + ctrl_action: String, + alt_action: String, + multi_action: String, osc52: bool, ) -> Swapper { let since_the_epoch = SystemTime::now() @@ -88,9 +94,11 @@ impl<'a> Swapper<'a> { Swapper { executor, dir, - command, - upcase_command, - multi_command, + main_action, + shift_action, + ctrl_action, + alt_action, + multi_action, osc52, active_pane_id: None, active_pane_height: None, @@ -215,7 +223,7 @@ impl<'a> Swapper<'a> { }; let pane_command = format!( - "tmux capture-pane -t {active_pane_id} -p{scroll_params} | tail -n {height} | {dir}/target/release/thumbs -f '%U:%H' -t {tmp} {args}; tmux swap-pane -t {active_pane_id}; {zoom_command} tmux wait-for -S {signal}", + "tmux capture-pane -t {active_pane_id} -p{scroll_params} | tail -n {height} | {dir}/target/release/thumbs -f '%K:%H' -t {tmp} {args}; tmux swap-pane -t {active_pane_id}; {zoom_command} tmux wait-for -S {signal}", active_pane_id = active_pane_id, scroll_params = scroll_params, height = self.active_pane_height.unwrap_or(i32::MAX), @@ -320,7 +328,8 @@ impl<'a> Swapper<'a> { .collect::>() .join(" "); - self.execute_final_command(&text, &self.multi_command.clone()); + // REVIEW: Not sure about the ModifierKeys here + self.execute_final_command(&text, &view::ModifierKeys::Main.to_string(), &self.multi_action.clone()); return; } @@ -330,7 +339,7 @@ impl<'a> Swapper<'a> { let mut splitter = item.splitn(2, ':'); - if let Some(upcase) = splitter.next() { + if let Some(modkey) = splitter.next() { if let Some(text) = splitter.next() { if self.osc52 { let base64_text = base64::encode(text.as_bytes()); @@ -360,10 +369,17 @@ impl<'a> Swapper<'a> { std::io::stdout().flush().unwrap(); } - let execute_command = if upcase.trim_end() == "true" { - self.upcase_command.clone() - } else { - self.command.clone() + let modkey = modkey.trim_end(); + + let shift = view::ModifierKeys::Shift.to_string(); + let ctrl = view::ModifierKeys::Ctrl.to_string(); + let alt = view::ModifierKeys::Alt.to_string(); + + let execute_command = match modkey { + shift => self.shift_action.clone(), + ctrl => self.ctrl_action.clone(), + alt => self.alt_action.clone(), + _ => self.main_action.clone(), }; // The command we run has two arguments: @@ -389,19 +405,20 @@ impl<'a> Swapper<'a> { // Ideally user commands would just use "${THUMB}" to begin with rather than having any // sort of ad-hoc string splicing here at all, and then they could specify the quoting they // want, but that would break backwards compatibility. - self.execute_final_command(text.trim_end(), &execute_command); + self.execute_final_command(text.trim_end(), modkey, &execute_command); } } } - pub fn execute_final_command(&mut self, text: &str, execute_command: &str) { + pub fn execute_final_command(&mut self, text: &str, modkey: &str, execute_command: &str) { let final_command = str::replace(execute_command, "{}", "${THUMB}"); let retrieve_command = vec![ "bash", "-c", - "THUMB=\"$1\"; eval \"$2\"", + "THUMB=\"$1\"; MODIFIER=\"$2\"; eval \"$3\"", "--", text, + modkey, final_command.as_str(), ]; @@ -450,6 +467,8 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), + "".to_string(), + "".to_string(), false, ); @@ -473,6 +492,8 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), + "".to_string(), + "".to_string(), false, ); @@ -490,15 +511,19 @@ mod tests { let last_command_outputs = vec!["Blah blah blah, the ignored user script output".to_string()]; let mut executor = TestShell::new(last_command_outputs); - let user_command = "echo \"{}\"".to_string(); - let upcase_command = "open \"{}\"".to_string(); - let multi_command = "open \"{}\"".to_string(); + let main_action = "echo \"{}\"".to_string(); + let shift_action = "open \"{}\"".to_string(); + let ctrl_action = "echo \"{}\"".to_string(); + let alt_action = "echo \"{}\"".to_string(); + let multi_action = "open \"{}\"".to_string(); let mut swapper = Swapper::new( Box::new(&mut executor), "".to_string(), - user_command, - upcase_command, - multi_command, + main_action, + shift_action, + ctrl_action, + alt_action, + multi_action, false, ); @@ -539,21 +564,33 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .default_value(""), ) .arg( - Arg::with_name("command") + Arg::with_name("main-action") .help("Command to execute after choose a hint") - .long("command") + .long("main-action") .default_value("tmux set-buffer -- \"{}\" && tmux display-message \"Copied {}\""), ) .arg( - Arg::with_name("upcase_command") - .help("Command to execute after choose a hint, in upcase") - .long("upcase-command") + Arg::with_name("shift-action") + .help("Command to execute after choose a hint with SHIFT key pressed (Upcase)") + .long("shift-action") .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Copied {}\""), ) .arg( - Arg::with_name("multi_command") + Arg::with_name("ctrl-action") + .help("Command to execute after choose a hint with CTRL key pressed") + .long("ctrl-action") + .default_value("tmux display-message \"No CTRL action configured! Hint: {}\""), + ) + .arg( + Arg::with_name("alt-action") + .help("Command to execute after choose a hint with ALT key pressed") + .long("alt-action") + .default_value("tmux display-message \"No ALT action configured! Hint: {}\" && echo \"FOO $MODIFIER ~ $HINT BAR\" > /tmp/tx.txt"), + ) + .arg( + Arg::with_name("multi-action") .help("Command to execute after choose multiple hints") - .long("multi-command") + .long("multi-action") .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Multi copied {}\""), ) .arg( @@ -568,9 +605,11 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { fn main() -> std::io::Result<()> { let args = app_args(); let dir = args.value_of("dir").unwrap(); - let command = args.value_of("command").unwrap(); - let upcase_command = args.value_of("upcase_command").unwrap(); - let multi_command = args.value_of("multi_command").unwrap(); + let main_action = args.value_of("main-action").unwrap(); + let shift_action = args.value_of("shift-action").unwrap(); + let ctrl_action = args.value_of("ctrl-action").unwrap(); + let alt_action = args.value_of("alt-action").unwrap(); + let multi_action = args.value_of("multi-action").unwrap(); let osc52 = args.is_present("osc52"); if dir.is_empty() { @@ -581,9 +620,11 @@ fn main() -> std::io::Result<()> { let mut swapper = Swapper::new( Box::new(&mut executor), dir.to_string(), - command.to_string(), - upcase_command.to_string(), - multi_command.to_string(), + main_action.to_string(), + shift_action.to_string(), + ctrl_action.to_string(), + alt_action.to_string(), + multi_action.to_string(), osc52, ); diff --git a/src/view.rs b/src/view.rs index e92cf95..0e2fe59 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,6 +1,6 @@ use super::*; -use std::char; use std::io::{stdout, Read, Write}; +use std::{char, fmt, vec}; use termion::async_stdin; use termion::event::Key; use termion::input::TermRead; @@ -25,7 +25,7 @@ pub struct View<'a> { background_color: Box, hint_background_color: Box, hint_foreground_color: Box, - chosen: Vec<(String, bool)>, + chosen: Vec<(String, String)>, } enum CaptureEvent { @@ -33,6 +33,25 @@ enum CaptureEvent { Hint, } +#[derive(Debug, Clone, Copy)] +pub enum ModifierKeys { + Main, + Shift, + Ctrl, + Alt, +} + +impl fmt::Display for ModifierKeys { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ModifierKeys::Main => write!(f, "main"), + ModifierKeys::Shift => write!(f, "shift"), + ModifierKeys::Ctrl => write!(f, "ctrl"), + ModifierKeys::Alt => write!(f, "alt"), + } + } +} + impl<'a> View<'a> { pub fn new( state: &'a mut state::State<'a>, @@ -196,10 +215,10 @@ impl<'a> View<'a> { loop { match stdin.keys().next() { - Some(key) => { - match key { - Ok(key) => { - match key { + Some(original_key) => { + match original_key { + Ok(original_key) => { + match original_key { Key::Esc => { if self.multi && !typed_hint.is_empty() { typed_hint.clear(); @@ -222,11 +241,13 @@ impl<'a> View<'a> { Key::Backspace => { typed_hint.pop(); } - Key::Char(ch) => { + Key::Char(ch) | Key::Alt(ch) | Key::Ctrl(ch) => { match ch { '\n' => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) { Some(hm) => { - self.chosen.push((hm.1.text.to_string(), false)); + self + .chosen + .push((hm.1.text.to_string(), ModifierKeys::Main.to_string())); if !self.multi { return CaptureEvent::Hint; @@ -251,9 +272,19 @@ impl<'a> View<'a> { let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone())); + let modkey = if original_key == Key::Alt(ch) { + ModifierKeys::Alt.to_string() + } else if original_key == Key::Ctrl(ch) { + ModifierKeys::Ctrl.to_string() + } else if key != lower_key { + ModifierKeys::Shift.to_string() + } else { + ModifierKeys::Main.to_string() + }; + match selection { Some(mat) => { - self.chosen.push((mat.text.to_string(), key != lower_key)); + self.chosen.push((mat.text.to_string(), modkey)); if self.multi { typed_hint.clear(); @@ -293,7 +324,7 @@ impl<'a> View<'a> { CaptureEvent::Exit } - pub fn present(&mut self) -> Vec<(String, bool)> { + pub fn present(&mut self) -> Vec<(String, String)> { let mut stdin = async_stdin(); let mut stdout = AlternateScreen::from(stdout().into_raw_mode().unwrap()); diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh index 7e060e8..4978331 100755 --- a/tmux-thumbs.sh +++ b/tmux-thumbs.sh @@ -45,9 +45,10 @@ function add-param() { fi } -add-param command string -add-param upcase-command string -add-param multi-command string -add-param osc52 boolean +add-param main-action string +add-param shift-action string +add-param ctrl-action string +add-param alt-action string +add-param osc52 boolean "${TMUX_THUMBS_BINARY}" "${PARAMS[@]}" || true