Improve multi selection mode

This commit is contained in:
Ferran Basora 2021-06-19 10:50:19 +00:00
parent f461267542
commit a403638d84
4 changed files with 174 additions and 50 deletions

View File

@ -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-regexp-N](#thumbs-regexp-N)
* [@thumbs-command](#thumbs-command) * [@thumbs-command](#thumbs-command)
* [@thumbs-upcase-command](#thumbs-upcase-command) * [@thumbs-upcase-command](#thumbs-upcase-command)
* [@thumbs-multi-command](#thumbs-multi-command)
* [@thumbs-bg-color](#thumbs-bg-color) * [@thumbs-bg-color](#thumbs-bg-color)
* [@thumbs-fg-color](#thumbs-fg-color) * [@thumbs-fg-color](#thumbs-fg-color)
* [@thumbs-hint-bg-color](#thumbs-hint-bg-color) * [@thumbs-hint-bg-color](#thumbs-hint-bg-color)
* [@thumbs-hint-fg-color](#thumbs-hint-fg-color) * [@thumbs-hint-fg-color](#thumbs-hint-fg-color)
* [@thumbs-select-fg-color](#thumbs-select-fg-color) * [@thumbs-select-fg-color](#thumbs-select-fg-color)
* [@thumbs-select-bg-color](#thumbs-select-bg-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-contrast](#thumbs-contrast)
* [@thumbs-osc52](#thumbs-osc52) * [@thumbs-osc52](#thumbs-osc52)
@ -198,6 +201,18 @@ For example:
set -g @thumbs-upcase-command 'echo -n {} | pbcopy' 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 ### @thumbs-bg-color
`default: black` `default: black`
@ -270,6 +285,30 @@ For example:
set -g @thumbs-select-bg-color red 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 ### @thumbs-contrast
`default: 0` `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. - **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. - **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 <kbd>Space</kbd>. Then, choose the matches with highlighted hints or
<kbd>Enter</kbd> (moving with cursors) and then <kbd>Space</kbd> 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 <kbd>Space</kbd> to finalize the selection.
## Tmux compatibility ## Tmux compatibility
@ -402,6 +449,8 @@ OPTIONS:
-x, --regexp <regexp>... Use this regexp as extra pattern to match -x, --regexp <regexp>... Use this regexp as extra pattern to match
--select-bg-color <select_background_color> Sets the background color for selection [default: black] --select-bg-color <select_background_color> Sets the background color for selection [default: black]
--select-fg-color <select_foreground_color> Sets the foreground color for selection [default: blue] --select-fg-color <select_foreground_color> Sets the foreground color for selection [default: blue]
--multi-bg-color <multi_background_color> Sets the background color for a multi selected item [default: black]
--multi-fg-color <multi_foreground_color> Sets the foreground color for a multi selected item [default: cyan]
-t, --target <target> Stores the hint in the specified path -t, --target <target> Stores the hint in the specified path
``` ```

View File

@ -66,6 +66,18 @@ fn app_args<'a>() -> clap::ArgMatches<'a> {
.long("hint-bg-color") .long("hint-bg-color")
.default_value("black"), .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(
Arg::with_name("select_foreground_color") Arg::with_name("select_foreground_color")
.help("Sets the foreground color for selection") .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 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_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 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 stdin = io::stdin();
let mut handle = stdin.lock(); let mut handle = stdin.lock();
@ -170,6 +184,8 @@ fn main() {
position, position,
select_foreground_color, select_foreground_color,
select_background_color, select_background_color,
multi_foreground_color,
multi_background_color,
foreground_color, foreground_color,
background_color, background_color,
hint_foreground_color, hint_foreground_color,

View File

@ -60,6 +60,7 @@ pub struct Swapper<'a> {
dir: String, dir: String,
command: String, command: String,
upcase_command: String, upcase_command: String,
multi_command: String,
osc52: bool, osc52: bool,
active_pane_id: Option<String>, active_pane_id: Option<String>,
active_pane_height: Option<i32>, active_pane_height: Option<i32>,
@ -76,6 +77,7 @@ impl<'a> Swapper<'a> {
dir: String, dir: String,
command: String, command: String,
upcase_command: String, upcase_command: String,
multi_command: String,
osc52: bool, osc52: bool,
) -> Swapper { ) -> Swapper {
let since_the_epoch = SystemTime::now() let since_the_epoch = SystemTime::now()
@ -88,6 +90,7 @@ impl<'a> Swapper<'a> {
dir, dir,
command, command,
upcase_command, upcase_command,
multi_command,
osc52, osc52,
active_pane_id: None, active_pane_id: None,
active_pane_height: None, active_pane_height: None,
@ -306,7 +309,24 @@ impl<'a> Swapper<'a> {
pub fn execute_command(&mut self) { pub fn execute_command(&mut self) {
let content = self.content.clone().unwrap(); 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::<Vec<&str>>()
.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(upcase) = splitter.next() {
if let Some(text) = 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 // 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 // sort of ad-hoc string splicing here at all, and then they could specify the quoting they
// want, but that would break backwards compatibility. // want, but that would break backwards compatibility.
let final_command = str::replace(execute_command.as_str(), "{}", "${THUMB}"); 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![ let retrieve_command = vec![
"bash", "bash",
"-c", "-c",
"THUMB=\"$1\"; eval \"$2\"", "THUMB=\"$1\"; eval \"$2\"",
"--", "--",
text.trim_end(), text,
final_command.as_str(), final_command.as_str(),
]; ];
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
self.executor.execute(params); self.executor.execute(params);
} }
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -422,6 +447,7 @@ mod tests {
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
"".to_string(),
false, false,
); );
@ -444,6 +470,7 @@ mod tests {
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
"".to_string(),
false, false,
); );
@ -463,11 +490,13 @@ mod tests {
let user_command = "echo \"{}\"".to_string(); let user_command = "echo \"{}\"".to_string();
let upcase_command = "open \"{}\"".to_string(); let upcase_command = "open \"{}\"".to_string();
let multi_command = "open \"{}\"".to_string();
let mut swapper = Swapper::new( let mut swapper = Swapper::new(
Box::new(&mut executor), Box::new(&mut executor),
"".to_string(), "".to_string(),
user_command, user_command,
upcase_command, upcase_command,
multi_command,
false, false,
); );
@ -509,15 +538,21 @@ fn app_args<'a>() -> clap::ArgMatches<'a> {
) )
.arg( .arg(
Arg::with_name("command") Arg::with_name("command")
.help("Pick command") .help("Command to execute after choose a hint")
.long("command") .long("command")
.default_value("tmux set-buffer -- {} && tmux display-message \"Copied {}\""), .default_value("tmux set-buffer -- \"{}\" && tmux display-message \"Copied {}\""),
) )
.arg( .arg(
Arg::with_name("upcase_command") Arg::with_name("upcase_command")
.help("Upcase command") .help("Command to execute after choose a hint, in upcase")
.long("upcase-command") .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(
Arg::with_name("osc52") Arg::with_name("osc52")
@ -533,6 +568,7 @@ fn main() -> std::io::Result<()> {
let dir = args.value_of("dir").unwrap(); let dir = args.value_of("dir").unwrap();
let command = args.value_of("command").unwrap(); let command = args.value_of("command").unwrap();
let upcase_command = args.value_of("upcase_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"); let osc52 = args.is_present("osc52");
if dir.is_empty() { if dir.is_empty() {
@ -545,6 +581,7 @@ fn main() -> std::io::Result<()> {
dir.to_string(), dir.to_string(),
command.to_string(), command.to_string(),
upcase_command.to_string(), upcase_command.to_string(),
multi_command.to_string(),
osc52, osc52,
); );

View File

@ -19,15 +19,18 @@ pub struct View<'a> {
matches: Vec<state::Match<'a>>, matches: Vec<state::Match<'a>>,
select_foreground_color: Box<dyn color::Color>, select_foreground_color: Box<dyn color::Color>,
select_background_color: Box<dyn color::Color>, select_background_color: Box<dyn color::Color>,
multi_foreground_color: Box<dyn color::Color>,
multi_background_color: Box<dyn color::Color>,
foreground_color: Box<dyn color::Color>, foreground_color: Box<dyn color::Color>,
background_color: Box<dyn color::Color>, background_color: Box<dyn color::Color>,
hint_background_color: Box<dyn color::Color>, hint_background_color: Box<dyn color::Color>,
hint_foreground_color: Box<dyn color::Color>, hint_foreground_color: Box<dyn color::Color>,
chosen: Vec<(String, bool)>,
} }
enum CaptureEvent { enum CaptureEvent {
Exit, Exit,
Hint(Vec<(String, bool)>), Hint,
} }
impl<'a> View<'a> { impl<'a> View<'a> {
@ -40,6 +43,8 @@ impl<'a> View<'a> {
position: &'a str, position: &'a str,
select_foreground_color: Box<dyn color::Color>, select_foreground_color: Box<dyn color::Color>,
select_background_color: Box<dyn color::Color>, select_background_color: Box<dyn color::Color>,
multi_foreground_color: Box<dyn color::Color>,
multi_background_color: Box<dyn color::Color>,
foreground_color: Box<dyn color::Color>, foreground_color: Box<dyn color::Color>,
background_color: Box<dyn color::Color>, background_color: Box<dyn color::Color>,
hint_foreground_color: Box<dyn color::Color>, hint_foreground_color: Box<dyn color::Color>,
@ -57,10 +62,13 @@ impl<'a> View<'a> {
matches, matches,
select_foreground_color, select_foreground_color,
select_background_color, select_background_color,
multi_foreground_color,
multi_background_color,
foreground_color, foreground_color,
background_color, background_color,
hint_foreground_color, hint_foreground_color,
hint_background_color, hint_background_color,
chosen: vec![],
} }
} }
@ -98,12 +106,18 @@ impl<'a> View<'a> {
let selected = self.matches.get(self.skip); let selected = self.matches.get(self.skip);
for mat in self.matches.iter() { 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 &self.select_foreground_color
} else { } else {
&self.foreground_color &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 &self.select_background_color
} else { } else {
&self.background_color &self.background_color
@ -157,7 +171,6 @@ impl<'a> View<'a> {
return CaptureEvent::Exit; return CaptureEvent::Exit;
} }
let mut chosen = vec![];
let mut typed_hint: String = "".to_owned(); let mut typed_hint: String = "".to_owned();
let longest_hint = self let longest_hint = self
.matches .matches
@ -195,24 +208,28 @@ impl<'a> View<'a> {
self.next(); self.next();
} }
Key::Char(ch) => { Key::Char(ch) => {
if ch == '\n' { match ch {
match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) { '\n' => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) {
Some(hm) => { Some(hm) => {
chosen.push((hm.1.text.to_string(), false)); self.chosen.push((hm.1.text.to_string(), false));
if !self.multi { if !self.multi {
return CaptureEvent::Hint(chosen); return CaptureEvent::Hint;
} }
} }
_ => panic!("Match not found?"), _ => panic!("Match not found?"),
},
' ' => {
if self.multi {
// Finalize the multi selection
return CaptureEvent::Hint;
} else {
// Enable the multi selection
self.multi = true;
} }
} }
key => {
if ch == ' ' && self.multi { let key = key.to_string();
return CaptureEvent::Hint(chosen);
}
let key = ch.to_string();
let lower_key = key.to_lowercase(); let lower_key = key.to_lowercase();
typed_hint.push_str(lower_key.as_str()); typed_hint.push_str(lower_key.as_str());
@ -221,12 +238,12 @@ impl<'a> View<'a> {
match selection { match selection {
Some(mat) => { Some(mat) => {
chosen.push((mat.text.to_string(), key != lower_key)); self.chosen.push((mat.text.to_string(), key != lower_key));
if self.multi { if self.multi {
typed_hint.clear(); typed_hint.clear();
} else { } else {
return CaptureEvent::Hint(chosen); return CaptureEvent::Hint;
} }
} }
None => { None => {
@ -236,6 +253,8 @@ impl<'a> View<'a> {
} }
} }
} }
}
}
_ => { _ => {
// Unknown key // Unknown key
} }
@ -265,7 +284,7 @@ impl<'a> View<'a> {
let hints = match self.listen(&mut stdin, &mut stdout) { let hints = match self.listen(&mut stdin, &mut stdout) {
CaptureEvent::Exit => vec![], CaptureEvent::Exit => vec![],
CaptureEvent::Hint(chosen) => chosen, CaptureEvent::Hint => self.chosen.clone(),
}; };
write!(stdout, "{}", cursor::Show).unwrap(); write!(stdout, "{}", cursor::Show).unwrap();
@ -296,10 +315,13 @@ mod tests {
matches: vec![], matches: vec![],
select_foreground_color: colors::get_color("default"), select_foreground_color: colors::get_color("default"),
select_background_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"), foreground_color: colors::get_color("default"),
background_color: colors::get_color("default"), background_color: colors::get_color("default"),
hint_background_color: colors::get_color("default"), hint_background_color: colors::get_color("default"),
hint_foreground_color: colors::get_color("default"), hint_foreground_color: colors::get_color("default"),
chosen: vec![],
}; };
let result = view.make_hint_text("a"); let result = view.make_hint_text("a");