From a403638d84521f29b7849a45194c80faa4586a15 Mon Sep 17 00:00:00 2001 From: Ferran Basora Date: Sat, 19 Jun 2021 10:50:19 +0000 Subject: [PATCH] Improve multi selection mode --- README.md | 51 +++++++++++++++++++++++++++++- src/main.rs | 16 ++++++++++ src/swapper.rs | 71 +++++++++++++++++++++++++++++++---------- src/view.rs | 86 +++++++++++++++++++++++++++++++------------------- 4 files changed, 174 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index e6d583f..ba5b9a5 100644 --- a/README.md +++ b/README.md @@ -91,12 +91,15 @@ NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` * [@thumbs-regexp-N](#thumbs-regexp-N) * [@thumbs-command](#thumbs-command) * [@thumbs-upcase-command](#thumbs-upcase-command) +* [@thumbs-multi-command](#thumbs-multi-command) * [@thumbs-bg-color](#thumbs-bg-color) * [@thumbs-fg-color](#thumbs-fg-color) * [@thumbs-hint-bg-color](#thumbs-hint-bg-color) * [@thumbs-hint-fg-color](#thumbs-hint-fg-color) * [@thumbs-select-fg-color](#thumbs-select-fg-color) * [@thumbs-select-bg-color](#thumbs-select-bg-color) +* [@thumbs-multi-fg-color](#thumbs-multi-fg-color) +* [@thumbs-multi-bg-color](#thumbs-multi-bg-color) * [@thumbs-contrast](#thumbs-contrast) * [@thumbs-osc52](#thumbs-osc52) @@ -198,6 +201,18 @@ For example: set -g @thumbs-upcase-command 'echo -n {} | pbcopy' ``` +### @thumbs-multi-command + +`default: 'tmux set-buffer -- {} && tmux paste-buffer && tmux send-keys ' ' && tmux display-message \"Copied multiple items!\"'` + +Choose which command execute when you select multiple items. `tmux-thumbs` will replace `{}` with the picked hint for each one. + +For example: + +``` +set -g @thumbs-multi-command 'echo -n {}' +``` + ### @thumbs-bg-color `default: black` @@ -270,6 +285,30 @@ For example: set -g @thumbs-select-bg-color red ``` +### @thumbs-multi-fg-color + +`default: yellow` + +Sets the foreground color for multi selected item + +For example: + +``` +set -g @thumbs-multi-fg-color green +``` + +### @thumbs-select-bg-color + +`default: black` + +Sets the background color for multi selected item + +For example: + +``` +set -g @thumbs-select-bg-color red +``` + ### @thumbs-contrast `default: 0` @@ -341,7 +380,15 @@ This is the list of available alphabets: - **Arrow navigation:** You can use the arrows to move around between all matched items. - **Auto paste:** If your last typed hint character is uppercase, you are going to pick and paste the desired hint. -- **Multi selection:** If you run thumb with multi selection mode you will be able to choose multiple hints pressing the desired letter and `Space` to finalize the selection. + +### Multi selection + +If you want to enable the capability to choose multiple matches, you have to +press Space. Then, choose the matches with highlighted hints or +Enter (moving with cursors) and then Space again to +output all of them. + +If you run standalone `thumbs` with multi selection mode (-m) you will be able to choose multiple hints pressing the desired letter and Space to finalize the selection. ## Tmux compatibility @@ -402,6 +449,8 @@ OPTIONS: -x, --regexp ... Use this regexp as extra pattern to match --select-bg-color Sets the background color for selection [default: black] --select-fg-color Sets the foreground color for selection [default: blue] + --multi-bg-color Sets the background color for a multi selected item [default: black] + --multi-fg-color Sets the foreground color for a multi selected item [default: cyan] -t, --target Stores the hint in the specified path ``` diff --git a/src/main.rs b/src/main.rs index a810286..49ded04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,18 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .long("hint-bg-color") .default_value("black"), ) + .arg( + Arg::with_name("multi_foreground_color") + .help("Sets the foreground color for a multi selected item") + .long("multi-fg-color") + .default_value("yellow"), + ) + .arg( + Arg::with_name("multi_background_color") + .help("Sets the background color for a multi selected item") + .long("multi-bg-color") + .default_value("black"), + ) .arg( Arg::with_name("select_foreground_color") .help("Sets the foreground color for selection") @@ -149,6 +161,8 @@ fn main() { let hint_background_color = colors::get_color(args.value_of("hint_background_color").unwrap()); let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap()); let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap()); + let multi_foreground_color = colors::get_color(args.value_of("multi_foreground_color").unwrap()); + let multi_background_color = colors::get_color(args.value_of("multi_background_color").unwrap()); let stdin = io::stdin(); let mut handle = stdin.lock(); @@ -170,6 +184,8 @@ fn main() { position, select_foreground_color, select_background_color, + multi_foreground_color, + multi_background_color, foreground_color, background_color, hint_foreground_color, diff --git a/src/swapper.rs b/src/swapper.rs index aff33e1..cce19e0 100644 --- a/src/swapper.rs +++ b/src/swapper.rs @@ -60,6 +60,7 @@ pub struct Swapper<'a> { dir: String, command: String, upcase_command: String, + multi_command: String, osc52: bool, active_pane_id: Option, active_pane_height: Option, @@ -76,6 +77,7 @@ impl<'a> Swapper<'a> { dir: String, command: String, upcase_command: String, + multi_command: String, osc52: bool, ) -> Swapper { let since_the_epoch = SystemTime::now() @@ -88,6 +90,7 @@ impl<'a> Swapper<'a> { dir, command, upcase_command, + multi_command, osc52, active_pane_id: None, active_pane_height: None, @@ -306,7 +309,24 @@ impl<'a> Swapper<'a> { pub fn execute_command(&mut self) { let content = self.content.clone().unwrap(); - let mut splitter = content.splitn(2, ':'); + let items: Vec<&str> = content.split('\n').collect(); + + if items.len() > 1 { + let text = items + .iter() + .map(|item| item.splitn(2, ':').last().unwrap()) + .collect::>() + .join(" "); + + self.execute_final_command(&text, &self.multi_command.clone()); + + return; + } + + // Only one item + let item: &str = items.first().unwrap(); + + let mut splitter = item.splitn(2, ':'); if let Some(upcase) = splitter.next() { if let Some(text) = splitter.next() { @@ -367,21 +387,26 @@ 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. - let final_command = str::replace(execute_command.as_str(), "{}", "${THUMB}"); - let retrieve_command = vec![ - "bash", - "-c", - "THUMB=\"$1\"; eval \"$2\"", - "--", - text.trim_end(), - final_command.as_str(), - ]; - let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); - - self.executor.execute(params); + self.execute_final_command(text.trim_end(), &execute_command); } } } + + pub fn execute_final_command(&mut self, text: &str, execute_command: &str) { + let final_command = str::replace(execute_command, "{}", "${THUMB}"); + let retrieve_command = vec![ + "bash", + "-c", + "THUMB=\"$1\"; eval \"$2\"", + "--", + text, + final_command.as_str(), + ]; + + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } } #[cfg(test)] @@ -422,6 +447,7 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), + "".to_string(), false, ); @@ -444,6 +470,7 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), + "".to_string(), false, ); @@ -463,11 +490,13 @@ mod tests { let user_command = "echo \"{}\"".to_string(); let upcase_command = "open \"{}\"".to_string(); + let multi_command = "open \"{}\"".to_string(); let mut swapper = Swapper::new( Box::new(&mut executor), "".to_string(), user_command, upcase_command, + multi_command, false, ); @@ -509,15 +538,21 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { ) .arg( Arg::with_name("command") - .help("Pick command") + .help("Command to execute after choose a hint") .long("command") - .default_value("tmux set-buffer -- {} && tmux display-message \"Copied {}\""), + .default_value("tmux set-buffer -- \"{}\" && tmux display-message \"Copied {}\""), ) .arg( Arg::with_name("upcase_command") - .help("Upcase command") + .help("Command to execute after choose a hint, in upcase") .long("upcase-command") - .default_value("tmux set-buffer -- {} && tmux paste-buffer && tmux display-message \"Copied {}\""), + .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Copied {}\""), + ) + .arg( + Arg::with_name("multi_command") + .help("Command to execute after choose multiple hints") + .long("multi-command") + .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Multi copied {}\""), ) .arg( Arg::with_name("osc52") @@ -533,6 +568,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 multi_command = args.value_of("multi_command").unwrap(); let osc52 = args.is_present("osc52"); if dir.is_empty() { @@ -545,6 +581,7 @@ fn main() -> std::io::Result<()> { dir.to_string(), command.to_string(), upcase_command.to_string(), + multi_command.to_string(), osc52, ); diff --git a/src/view.rs b/src/view.rs index 2bd483f..eea57fb 100644 --- a/src/view.rs +++ b/src/view.rs @@ -19,15 +19,18 @@ pub struct View<'a> { matches: Vec>, select_foreground_color: Box, select_background_color: Box, + multi_foreground_color: Box, + multi_background_color: Box, foreground_color: Box, background_color: Box, hint_background_color: Box, hint_foreground_color: Box, + chosen: Vec<(String, bool)>, } enum CaptureEvent { Exit, - Hint(Vec<(String, bool)>), + Hint, } impl<'a> View<'a> { @@ -40,6 +43,8 @@ impl<'a> View<'a> { position: &'a str, select_foreground_color: Box, select_background_color: Box, + multi_foreground_color: Box, + multi_background_color: Box, foreground_color: Box, background_color: Box, hint_foreground_color: Box, @@ -57,10 +62,13 @@ impl<'a> View<'a> { matches, select_foreground_color, select_background_color, + multi_foreground_color, + multi_background_color, foreground_color, background_color, hint_foreground_color, hint_background_color, + chosen: vec![], } } @@ -98,12 +106,18 @@ impl<'a> View<'a> { let selected = self.matches.get(self.skip); for mat in self.matches.iter() { - let selected_color = if selected == Some(mat) { + let chosen_hint = self.chosen.iter().any(|(hint, _)| hint == mat.text); + + let selected_color = if chosen_hint { + &self.multi_foreground_color + } else if selected == Some(mat) { &self.select_foreground_color } else { &self.foreground_color }; - let selected_background_color = if selected == Some(mat) { + let selected_background_color = if chosen_hint { + &self.multi_background_color + } else if selected == Some(mat) { &self.select_background_color } else { &self.background_color @@ -157,7 +171,6 @@ impl<'a> View<'a> { return CaptureEvent::Exit; } - let mut chosen = vec![]; let mut typed_hint: String = "".to_owned(); let longest_hint = self .matches @@ -195,43 +208,49 @@ impl<'a> View<'a> { self.next(); } Key::Char(ch) => { - if ch == '\n' { - match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) { + match ch { + '\n' => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) { Some(hm) => { - chosen.push((hm.1.text.to_string(), false)); + self.chosen.push((hm.1.text.to_string(), false)); if !self.multi { - return CaptureEvent::Hint(chosen); + return CaptureEvent::Hint; } } _ => panic!("Match not found?"), - } - } - - if ch == ' ' && self.multi { - return CaptureEvent::Hint(chosen); - } - - let key = ch.to_string(); - let lower_key = key.to_lowercase(); - - typed_hint.push_str(lower_key.as_str()); - - let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone())); - - match selection { - Some(mat) => { - chosen.push((mat.text.to_string(), key != lower_key)); - + }, + ' ' => { if self.multi { - typed_hint.clear(); + // Finalize the multi selection + return CaptureEvent::Hint; } else { - return CaptureEvent::Hint(chosen); + // Enable the multi selection + self.multi = true; } } - None => { - if !self.multi && typed_hint.len() >= longest_hint.len() { - break; + key => { + let key = key.to_string(); + let lower_key = key.to_lowercase(); + + typed_hint.push_str(lower_key.as_str()); + + let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone())); + + match selection { + Some(mat) => { + self.chosen.push((mat.text.to_string(), key != lower_key)); + + if self.multi { + typed_hint.clear(); + } else { + return CaptureEvent::Hint; + } + } + None => { + if !self.multi && typed_hint.len() >= longest_hint.len() { + break; + } + } } } } @@ -265,7 +284,7 @@ impl<'a> View<'a> { let hints = match self.listen(&mut stdin, &mut stdout) { CaptureEvent::Exit => vec![], - CaptureEvent::Hint(chosen) => chosen, + CaptureEvent::Hint => self.chosen.clone(), }; write!(stdout, "{}", cursor::Show).unwrap(); @@ -296,10 +315,13 @@ mod tests { matches: vec![], select_foreground_color: colors::get_color("default"), select_background_color: colors::get_color("default"), + multi_foreground_color: colors::get_color("default"), + multi_background_color: colors::get_color("default"), foreground_color: colors::get_color("default"), background_color: colors::get_color("default"), hint_background_color: colors::get_color("default"), hint_foreground_color: colors::get_color("default"), + chosen: vec![], }; let result = view.make_hint_text("a");