From 8ae9fa9137b185cb848f6c5f4efd0065847e2547 Mon Sep 17 00:00:00 2001 From: Ferran Basora Date: Mon, 9 Dec 2019 00:21:25 +0100 Subject: [PATCH] Decouple thumbs from tmux --- Cargo.toml | 8 ++ README.md | 4 +- src/main.rs | 69 ++++------- src/swapper.rs | 310 +++++++++++++++++++++++++++++++++++++++++++++++++ tmux-thumbs.sh | 6 +- 5 files changed, 346 insertions(+), 51 deletions(-) create mode 100644 src/swapper.rs diff --git a/Cargo.toml b/Cargo.toml index 82ea9e3..a8d6235 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,11 @@ license = "MIT" rustbox = "0.11.0" regex = "1.1.2" clap = "2.32.0" + +[[bin]] +name = "thumbs" +path = "src/main.rs" + +[[bin]] +name = "tmux-thumbs" +path = "src/swapper.rs" diff --git a/README.md b/README.md index 16831aa..0119d5e 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ Add extra patterns to match. This paramenter can have multiple instances. For example: ``` -set @thumbs-regexp-1 '[a-z]+@[a-z]+.com' # Match emails -set @thumbs-regexp-2 '[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:' # Match MAC addresses +set -g @thumbs-regexp-1 '[a-z]+@[a-z]+.com' # Match emails +set -g @thumbs-regexp-2 '[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:' # Match MAC addresses ``` ### @thumbs-command diff --git a/src/main.rs b/src/main.rs index 7bf1ce1..f5517be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,17 +8,10 @@ mod view; use self::clap::{App, Arg}; use clap::crate_version; -use std::process::Command; - -fn exec_command(args: Vec<&str>) -> std::process::Output { - return Command::new(args[0]) - .args(&args[1..]) - .output() - .expect("Couldn't run it"); -} +use std::io::{self, Read}; fn app_args<'a>() -> clap::ArgMatches<'a> { - return App::new("tmux-thumbs") + return App::new("thumbs") .version(crate_version!()) .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") .arg( @@ -28,6 +21,13 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .short("a") .default_value("qwerty"), ) + .arg( + Arg::with_name("format") + .help("Specifies the out format for the picked hint. (%U: Upcase, %H: Hint)") + .long("format") + .short("f") + .default_value("%H"), + ) .arg( Arg::with_name("foreground_color") .help("Sets the foregroud color for matches") @@ -83,24 +83,6 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .default_value("left") .short("p"), ) - .arg( - Arg::with_name("tmux_pane") - .help("Get this tmux pane as reference pane") - .long("tmux-pane") - .takes_value(true), - ) - .arg( - Arg::with_name("command") - .help("Pick command") - .long("command") - .default_value("tmux set-buffer {}"), - ) - .arg( - Arg::with_name("upcase_command") - .help("Upcase command") - .long("upcase-command") - .default_value("tmux set-buffer {} && tmux paste-buffer"), - ) .arg( Arg::with_name("regexp") .help("Use this regexp as extra pattern to match") @@ -120,6 +102,7 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { fn main() { let args = app_args(); + let format = args.value_of("format").unwrap(); let alphabet = args.value_of("alphabet").unwrap(); let position = args.value_of("position").unwrap(); let reverse = args.is_present("reverse"); @@ -140,17 +123,12 @@ fn main() { let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap()); - let command = args.value_of("command").unwrap(); - let upcase_command = args.value_of("upcase_command").unwrap(); + let stdin = io::stdin(); + let mut handle = stdin.lock(); + let mut output = String::new(); - let mut capture_command = vec!["tmux", "capture-pane", "-e", "-J", "-p"]; + handle.read_to_string(&mut output).unwrap(); - if let Some(pane) = args.value_of("tmux_pane") { - capture_command.extend(vec!["-t", pane].iter().cloned()); - } - - let execution = exec_command(capture_command); - let output = String::from_utf8_lossy(&execution.stdout); let lines = output.split("\n").collect::>(); let mut state = state::State::new(&lines, alphabet, ®exp); @@ -173,17 +151,20 @@ fn main() { viewbox.present() }; - if let Some(pane) = args.value_of("tmux_pane") { - exec_command(vec!["tmux", "swap-pane", "-t", pane]); - }; - if let Some((text, upcase)) = selected { - let final_command = if upcase { - str::replace(upcase_command, "{}", text.as_str()) + let mut output = format.to_string(); + + let upcase_value = if upcase { + "true" } else { - str::replace(command, "{}", text.as_str()) + "false" }; - exec_command(vec!["bash", "-c", final_command.as_str()]); + output = str::replace(&output, "%U", upcase_value); + output = str::replace(&output, "%H", text.as_str()); + + print!("{}", output); + } else { + ::std::process::exit(1); } } diff --git a/src/swapper.rs b/src/swapper.rs new file mode 100644 index 0000000..d600fcf --- /dev/null +++ b/src/swapper.rs @@ -0,0 +1,310 @@ +extern crate clap; + +use self::clap::{App, Arg}; +use clap::crate_version; +use regex::Regex; +use std::process::Command; + +trait Executor { + fn execute(&mut self, args: Vec) -> String; + fn last_executed(&self) -> Option>; +} + +struct RealShell { + executed: Option>, +} + +impl RealShell { + fn new() -> RealShell { + RealShell { executed: None } + } +} + +impl Executor for RealShell { + fn execute(&mut self, args: Vec) -> String { + let execution = Command::new(args[0].as_str()) + .args(&args[1..]) + .output() + .expect("Couldn't run it"); + + self.executed = Some(args); + + String::from_utf8_lossy(&execution.stdout).into() + } + + fn last_executed(&self) -> Option> { + self.executed.clone() + } +} + +const TMP_FILE: &str = "/tmp/thumbs-last"; +const TMUX_SIGNAL: &str = "thumbs-finished"; + +pub struct Swapper<'a> { + executor: Box<&'a mut dyn Executor>, + command: String, + upcase_command: String, + active_pane_id: Option, + thumbs_pane_id: Option, + content: Option, +} + +impl<'a> Swapper<'a> { + fn new(executor: Box<&'a mut dyn Executor>, command: String, upcase_command: String) -> Swapper { + Swapper { + executor: executor, + command: command, + upcase_command: upcase_command, + active_pane_id: None, + thumbs_pane_id: None, + content: None, + } + } + + pub fn capture_active_pane(&mut self) { + let active_command = vec![ + "tmux", + "list-panes", + "-F", + "#{pane_id}:#{?pane_active,active,nope}", + ]; + let output = self + .executor + .execute(active_command.iter().map(|arg| arg.to_string()).collect()); + let lines: Vec<&str> = output.split("\n").collect(); + let chunks: Vec> = lines + .into_iter() + .map(|line| line.split(":").collect()) + .collect(); + + let chunk = chunks + .iter() + .find(|&chunks| *chunks.iter().nth(1).unwrap() == "active") + .expect("Unable to find active pane"); + + let pane_id = chunk.iter().nth(0).unwrap().to_string(); + + self.active_pane_id = Some(pane_id); + } + + pub fn execute_thumbs(&mut self) { + let options_command = vec!["tmux", "show", "-g"]; + let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); + let options = self.executor.execute(params); + let lines: Vec<&str> = options.split("\n").collect(); + + let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "(.*)""#).unwrap(); + + let args = lines + .iter() + .flat_map(|line| { + if let Some(captures) = pattern.captures(line) { + let name = captures.get(1).unwrap().as_str(); + let value = captures.get(2).unwrap().as_str(); + + let boolean_params = vec!["reverse", "unique", "contrast"]; + + if boolean_params.iter().any(|&x| x == name) { + return vec![format!("--{}", name).to_string()]; + } + + let string_params = vec![ + "position", + "fg-color", + "bg-color", + "hint-bg-color", + "hint-fg-color", + "select-fg-color", + "select-bg-color", + ]; + + if string_params.iter().any(|&x| x == name) { + return vec![ + format!("--{}", name).to_string(), + format!("'{}'", value).to_string(), + ]; + } + + if name.starts_with("regexp") { + return vec!["--regexp".to_string(), format!("'{}'", value).to_string()]; + } + + vec![] + } else { + vec![] + } + }) + .collect::>(); + + let pane_command = format!("tmux capture-pane -t \"{}\" -p | ./target/release/thumbs -f '%U:%H' {} | tee {}; tmux wait-for -S {}", self.active_pane_id.clone().unwrap(), args.join(" "), TMP_FILE, TMUX_SIGNAL); + let thumbs_command = vec![ + "tmux", + "new-window", + "-P", + "-n", + "[thumbs]", + pane_command.as_str(), + ]; + let params: Vec = thumbs_command.iter().map(|arg| arg.to_string()).collect(); + + self.thumbs_pane_id = Some(self.executor.execute(params)); + } + + pub fn swap_panes(&mut self) { + let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); + let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); + + let swap_command = vec![ + "tmux", + "swap-pane", + "-d", + "-s", + active_pane_id.as_str(), + "-t", + thumbs_pane_id.as_str(), + ]; + let params = swap_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } + + pub fn wait_thumbs(&mut self) { + let wait_command = vec!["tmux", "wait-for", TMUX_SIGNAL]; + let params = wait_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } + + pub fn retrieve_content(&mut self) { + let retrieve_command = vec!["cat", TMP_FILE]; + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + + self.content = Some(self.executor.execute(params)); + } + + pub fn destroy_content(&mut self) { + let retrieve_command = vec!["rm", "/tmp/thumbs-last"]; + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } + + pub fn execute_command(&mut self) { + let content = self.content.clone().unwrap(); + let mut splitter = content.splitn(2, ':'); + let upcase = splitter.next().unwrap().trim_end(); + let text = splitter.next().unwrap().trim_end(); + + let execute_command = if upcase == "true" { + self.upcase_command.clone() + } else { + self.command.clone() + }; + + let final_command = str::replace(execute_command.as_str(), "{}", text); + let retrieve_command = vec!["bash", "-c", final_command.as_str()]; + let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); + + self.executor.execute(params); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestShell { + outputs: Vec, + executed: Option>, + } + + impl TestShell { + fn new(outputs: Vec) -> TestShell { + TestShell { + executed: None, + outputs: outputs, + } + } + } + + impl Executor for TestShell { + fn execute(&mut self, args: Vec) -> String { + self.executed = Some(args); + self.outputs.pop().unwrap() + } + + fn last_executed(&self) -> Option> { + self.executed.clone() + } + } + + #[test] + fn retrieve_active_pane() { + let last_command_outputs = vec!["%97:active\n%106:nope\n%107: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()); + + swapper.capture_active_pane(); + + assert_eq!(swapper.active_pane_id.unwrap(), "%97"); + } + + #[test] + fn swap_panes() { + let last_command_outputs = vec![ + "".to_string(), + "%100".to_string(), + "".to_string(), + "%106:nope\n%98:active\n%107: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()); + + swapper.capture_active_pane(); + swapper.execute_thumbs(); + swapper.swap_panes(); + + let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"]; + + assert_eq!(executor.last_executed().unwrap(), expectation); + } +} +fn app_args<'a>() -> clap::ArgMatches<'a> { + return App::new("tmux-thumbs") + .version(crate_version!()) + .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") + .arg( + Arg::with_name("command") + .help("Pick command") + .long("command") + .default_value("tmux set-buffer {}"), + ) + .arg( + Arg::with_name("upcase_command") + .help("Upcase command") + .long("upcase-command") + .default_value("tmux set-buffer {} && tmux paste-buffer"), + ) + .get_matches(); +} + +fn main() { + let args = app_args(); + let command = args.value_of("command").unwrap(); + let upcase_command = args.value_of("upcase_command").unwrap(); + + let mut executor = RealShell::new(); + let mut swapper = Swapper::new( + Box::new(&mut executor), + command.to_string(), + upcase_command.to_string(), + ); + + swapper.capture_active_pane(); + swapper.execute_thumbs(); + swapper.swap_panes(); + swapper.wait_thumbs(); + swapper.retrieve_content(); + swapper.destroy_content(); + swapper.execute_command(); +} diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh index 00ae8ca..b0d76f7 100755 --- a/tmux-thumbs.sh +++ b/tmux-thumbs.sh @@ -49,9 +49,5 @@ for i in "${!PARAMS[@]}"; do done CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -TARGET_RELEASE="/target/release/" -CURRENT_PANE_ID=$(tmux list-panes -F "#{pane_id}:#{?pane_active,active,nope}" | grep active | cut -d: -f1) -NEW_ID=$(tmux new-window -P -d -n "[thumbs]" ${CURRENT_DIR}${TARGET_RELEASE}tmux-thumbs "${PARAMS[@]}" "--tmux-pane=${CURRENT_PANE_ID}") -NEW_PANE_ID=$(tmux list-panes -a | grep ${NEW_ID} | grep --color=never -o '%[0-9]\+') -tmux swap-pane -d -s ${CURRENT_PANE_ID} -t ${NEW_PANE_ID} +${CURRENT_DIR}/target/release/tmux-thumbs