From 01d6e62689c5eea6be320757c2bcaa7abf8c3db3 Mon Sep 17 00:00:00 2001 From: graelo Date: Fri, 29 May 2020 11:50:19 +0200 Subject: [PATCH] feat: first complete workflow --- src/bridge.rs | 437 ++++------------------------------------------ src/lib.rs | 1 + src/tmux.rs | 163 +++++++++-------- tmux-copyrat.tmux | 6 +- tmux-thumbs.sh | 28 --- 5 files changed, 119 insertions(+), 516 deletions(-) delete mode 100755 tmux-thumbs.sh diff --git a/src/bridge.rs b/src/bridge.rs index dd9be5c..a4831e3 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,351 +1,11 @@ use clap::Clap; -use regex::Regex; use std::collections::HashMap; -use std::process::Command; use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; use copyrat::{error, process, CliOpt}; mod tmux; -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("Execution failed"); - - self.executed = Some(args); - - let output: String = String::from_utf8_lossy(&execution.stdout).into(); - - output.trim_end().to_string() - } - - fn last_executed(&self) -> Option> { - self.executed.clone() - } -} - -const TMP_FILE: &str = "/tmp/copyrat-last"; - -pub struct Swapper<'a> { - executor: Box<&'a mut dyn Executor>, - // directory: &'a path::Path, - command: &'a str, - alt_command: &'a str, - active_pane_id: Option, - active_pane_height: Option, - active_pane_scroll_position: Option, - active_pane_in_copy_mode: Option, - thumbs_pane_id: Option, - content: Option, - signal: String, -} - -impl<'a> Swapper<'a> { - fn new( - executor: Box<&'a mut dyn Executor>, - // directory: &'a path::Path, - command: &'a str, - alt_command: &'a str, - ) -> Swapper<'a> { - let since_the_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"); - let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs()); - - Swapper { - executor, - // directory, - command, - alt_command, - active_pane_id: None, - active_pane_height: None, - active_pane_scroll_position: None, - active_pane_in_copy_mode: None, - thumbs_pane_id: None, - content: None, - signal, - } - } - - pub fn capture_active_pane(&mut self) { - let active_command = vec![ - "tmux", - "list-panes", - "-F", - "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?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 active_pane = chunks - .iter() - .find(|&chunks| *chunks.get(4).unwrap() == "active") - .expect("Unable to find active pane"); - - let pane_id = active_pane.get(0).unwrap(); - let pane_in_copy_mode = active_pane.get(1).unwrap().to_string(); - - self.active_pane_id = Some(pane_id.to_string()); - self.active_pane_in_copy_mode = Some(pane_in_copy_mode); - - if self.active_pane_in_copy_mode.clone().unwrap() == "1" { - let pane_height = active_pane - .get(2) - .unwrap() - .parse() - .expect("Unable to retrieve pane height"); - let pane_scroll_position = active_pane - .get(3) - .unwrap() - .parse() - .expect("Unable to retrieve pane scroll"); - - self.active_pane_height = Some(pane_height); - self.active_pane_scroll_position = Some(pane_scroll_position); - } - } - - 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]+) "?(\w+)"?"#).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)]; - } - - 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), format!("'{}'", value)]; - } - - if name.starts_with("regexp") { - return vec!["--regexp".to_string(), format!("'{}'", value)]; - } - - vec![] - } else { - vec![] - } - }) - .collect::>(); - - let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); - - let scroll_params = if self.active_pane_in_copy_mode.is_some() { - if let (Some(pane_height), Some(scroll_position)) = ( - self.active_pane_scroll_position, - self.active_pane_scroll_position, - ) { - format!( - " -S {} -E {}", - -scroll_position, - pane_height - scroll_position - 1 - ) - } else { - "".to_string() - } - } else { - "".to_string() - }; - - // NOTE: For debugging add echo $PWD && sleep 5 after tee - let pane_command = format!( - "tmux capture-pane -t {active_id} -p{scroll_params} | target/release/thumbs -f '%U:%H' -t {tmpfile} {args}; tmux swap-pane -t {active_id}; tmux wait-for -S {signal}", - active_id = active_pane_id, - scroll_params = scroll_params, - // dir = self.directory.to_str().unwrap(), - tmpfile = TMP_FILE, - args = args.join(" "), - signal = self.signal - ); - - let thumbs_command = vec![ - "tmux", - "new-window", - "-P", - "-d", - "-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", self.signal.as_str()]; - 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_FILE]; - 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, ':'); - - if let Some(upcase) = splitter.next() { - if let Some(text) = splitter.next() { - let execute_command = if upcase.trim_end() == "true" { - self.alt_command.clone() - } else { - self.command.clone() - }; - - let final_command = str::replace(execute_command, "{}", text.trim_end()); - 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, - } - } - } - - 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: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), "", ""); - - 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: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), "", ""); - - 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); - } -} - /// Main configuration, parsed from command line. #[derive(Clap, Debug)] #[clap(author, about, version)] @@ -358,18 +18,26 @@ struct BridgeOpt { #[clap(long, default_value = "tmux set-buffer {} && tmux-paste-buffer")] alt_command: String, - /// Retrieve options from tmux. + /// Don't read options from Tmux. /// - /// If active, options formatted like `copyrat-*` are read from tmux. - /// You should consider reading them from the config file (the default - /// option) as this saves both a command call (about 10ms) and a Regex - /// compilation. - #[clap(short = "T", long)] - get_options_from_tmux: bool, + /// By default, options formatted like `copyrat-*` are read from tmux. + /// However, you should consider reading them from the config file (the + /// default option) as this saves both a command call (about 10ms) and a + /// Regex compilation. + #[clap(long)] + ignore_options_from_tmux: bool, - /// Optionally capture entire pane history. - #[clap(long, arg_enum, default_value = "entire-history")] - capture: tmux::CaptureRegion, + /// Name of the copyrat temporary window. + /// + /// Copyrat is launched in a temporary window of that name. The only pane + /// in this temp window gets swapped with the current active one for + /// in-place searching, then swapped back and killed after we exit. + #[clap(long, default_value = "[copyrat]")] + window_name: String, + + /// Capture visible area or entire pane history. + #[clap(long, arg_enum, default_value = "visible-area")] + capture_region: tmux::CaptureRegion, // Include CLI Options #[clap(flatten)] @@ -391,7 +59,7 @@ impl BridgeOpt { self.alt_command = String::from(value); } "@copyrat-capture" => { - self.capture = tmux::CaptureRegion::from_str(&value)?; + self.capture_region = tmux::CaptureRegion::from_str(&value)?; } // Ignore unknown options. @@ -406,52 +74,35 @@ impl BridgeOpt { } } +/// fn main() -> Result<(), error::ParseError> { let mut opt = BridgeOpt::parse(); - if opt.get_options_from_tmux { - let tmux_options = tmux::get_options("@copyrat-")?; + if !opt.ignore_options_from_tmux { + let tmux_options: HashMap = tmux::get_options("@copyrat-")?; + + // Override default values with those coming from tmux. opt.merge_map(&tmux_options)?; } let panes: Vec = tmux::list_panes()?; let active_pane = panes - .iter() + .into_iter() .find(|p| p.is_active) - .expect("One tmux pane should be active"); + .expect("Exactly one tmux pane should be active in the current window."); - let buffer = tmux::capture_pane(&active_pane, &opt.capture)?; + let buffer = tmux::capture_pane(&active_pane, &opt.capture_region)?; - let selections: Vec<(String, bool)> = if active_pane.in_mode { - // If the current pane is in copy mode, we have to dance a little with - // Panes, because the current pane has already locked the Alternate - // Screen, preventing copyrat::run to execute. - let initial_pane = active_pane; + // We have to dance a little with Panes, because this process i/o streams + // are connected to the pane in the window newly created for us, instead + // of the active current pane. + let temp_pane_spec = format!("{}.0", opt.window_name); + tmux::swap_pane_with(&temp_pane_spec)?; - // Create a new window without switching to it, with a `sh` command - // for faster startup. - let temp_pane: tmux::Pane = tmux::create_new_window("[copyrat]", "sh")?; + let selections = copyrat::run(buffer, &opt.cli_options); - // Swap the two panes, changing the active pane to be the temp_pane. - // After swap, temp_pane has the same height than the initial_pane - // had before being swapped. - tmux::swap_panes(initial_pane, &temp_pane)?; - - // Running copyrat now will render in the newly created temp_pane - // (locking stdin, writing to its stdout), but this is almost - // transparent to the user. - let selections = copyrat::run(buffer, &opt.cli_options); - - // Swap back the two panes, making initial_pane the active one again. - tmux::swap_panes(&temp_pane, initial_pane)?; - - tmux::kill_pane(&temp_pane)?; - - selections - } else { - copyrat::run(buffer, &opt.cli_options) - }; + tmux::swap_pane_with(&temp_pane_spec)?; // Execute a command on each selection. // TODO: consider getting rid of multi-selection mode. @@ -461,6 +112,7 @@ fn main() -> Result<(), error::ParseError> { } else { opt.command.replace("{}", text) }; + let mut it = raw_command.split(' ').into_iter(); let command = it.next().unwrap(); let args: Vec<&str> = it.collect(); @@ -470,24 +122,5 @@ fn main() -> Result<(), error::ParseError> { process::execute(&command, &args).unwrap(); }); - if false { - let mut executor = RealShell::new(); - - let mut swapper = Swapper::new( - Box::new(&mut executor), - // opt.directory.as_path(), - &opt.command, - &opt.alt_command, - ); - - swapper.capture_active_pane(); - swapper.execute_thumbs(); - swapper.swap_panes(); - swapper.wait_thumbs(); - swapper.retrieve_content(); - swapper.destroy_content(); - swapper.execute_command(); - } - Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 54096f5..f2e2ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,6 +136,7 @@ impl FromStr for HintStyleCli { } } +/// Try to parse a `&str` into a tuple of `char`s. fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { if src.len() != 2 { return Err(error::ParseError::ExpectedSurroundingPair); diff --git a/src/tmux.rs b/src/tmux.rs index 2c009f6..6d9b83c 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -1,6 +1,7 @@ use clap::Clap; use regex::Regex; use std::collections::HashMap; +use std::fmt; use std::str::FromStr; use copyrat::error::ParseError; @@ -8,8 +9,8 @@ use copyrat::process; #[derive(Debug, PartialEq)] pub struct Pane { - /// Pane identifier. - pub id: u32, + /// Pane identifier, e.g. `%37`. + pub id: PaneId, /// Describes if the pane is in some mode. pub in_mode: bool, /// Number of lines in the pane. @@ -24,7 +25,9 @@ pub struct Pane { pub is_active: bool, } -impl Pane { +impl FromStr for Pane { + type Err = ParseError; + /// Parse a string containing tmux panes status into a new `Pane`. /// /// This returns a `Result` as this call can obviously @@ -37,17 +40,20 @@ impl Pane { /// /// For definitions, look at `Pane` type, /// and at the tmux man page for definitions. - pub fn parse(src: &str) -> Result { + fn from_str(src: &str) -> Result { let items: Vec<&str> = src.split(':').collect(); assert_eq!(items.len(), 5, "tmux should have returned 5 items per line"); let mut iter = items.iter(); + // Pane id must be start with '%' followed by a `u32` let id_str = iter.next().unwrap(); - if !id_str.starts_with('%') { - return Err(ParseError::ExpectedPaneIdMarker); - } - let id = id_str[1..].parse::()?; + let id = PaneId::from_str(id_str)?; + // if !id_str.starts_with('%') { + // return Err(ParseError::ExpectedPaneIdMarker); + // } + // let id = id_str[1..].parse::()?; + // let id = format!("%{}", id); let in_mode = iter.next().unwrap().parse::()?; @@ -73,6 +79,58 @@ impl Pane { } } +#[derive(Debug, PartialEq)] +pub struct PaneId(String); + +impl FromStr for PaneId { + type Err = ParseError; + + /// Parse into PaneId. The `&str` must be start with '%' + /// followed by a `u32`. + fn from_str(src: &str) -> Result { + if !src.starts_with('%') { + return Err(ParseError::ExpectedPaneIdMarker); + } + let id = src[1..].parse::()?; + let id = format!("%{}", id); + Ok(PaneId(id)) + } +} + +impl fmt::Display for PaneId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clap, Debug)] +pub enum CaptureRegion { + /// The entire history. + /// + /// This will end up sending `-S - -E -` to `tmux capture-pane`. + EntireHistory, + /// The visible area. + VisibleArea, + ///// Region from start line to end line + ///// + ///// This works as defined in tmux's docs (order does not matter). + //Region(i32, i32), +} + +impl FromStr for CaptureRegion { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(CaptureRegion::EntireHistory), + "trailing" => Ok(CaptureRegion::VisibleArea), + _ => Err(ParseError::ExpectedString(String::from( + "entire-history or visible-area", + ))), + } + } +} + /// Returns a list of `Pane` from the current tmux session. pub fn list_panes() -> Result, ParseError> { let args = vec![ @@ -88,7 +146,7 @@ pub fn list_panes() -> Result, ParseError> { let result: Result, ParseError> = output .trim_end() // trim last '\n' as it would create an empty line .split('\n') - .map(|line| Pane::parse(line)) + .map(|line| Pane::from_str(line)) .collect(); result @@ -124,34 +182,6 @@ pub fn get_options(prefix: &str) -> Result, ParseError> Ok(args) } -#[derive(Clap, Debug)] -pub enum CaptureRegion { - /// The entire history. - /// - /// This will end up sending `-S - -E -` to `tmux capture-pane`. - EntireHistory, - /// The visible area. - VisibleArea, - ///// Region from start line to end line - ///// - ///// This works as defined in tmux's docs (order does not matter). - //Region(i32, i32), -} - -impl FromStr for CaptureRegion { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "leading" => Ok(CaptureRegion::EntireHistory), - "trailing" => Ok(CaptureRegion::VisibleArea), - _ => Err(ParseError::ExpectedString(String::from( - "entire-history or visible-area", - ))), - } - } -} - /// Returns the entire Pane content as a `String`. /// /// `CaptureRegion` specifies if the visible area is captured, or the entire @@ -169,11 +199,10 @@ impl FromStr for CaptureRegion { /// position. To support both cases, the implementation always provides those /// parameters to tmux. pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { - let mut args = format!("capture-pane -t %{id} -p", id = pane.id); + let mut args = format!("capture-pane -t {pane_id} -p", pane_id = pane.id); let region_str = match region { CaptureRegion::VisibleArea => { - // Will capture the visible area. // Providing start/end helps support both copy and normal modes. format!( " -S {start} -E {end}", @@ -190,51 +219,12 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result Result { - let args = vec!["new-window", "-P", "-d", "-n", name, "-F", - "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}", - command]; - - let output = process::execute("tmux", &args)?; - - let pane = Pane::parse(output.trim_end())?; // trim last '\n' as it would create an empty line - - Ok(pane) -} - -/// Ask tmux to swap two `Pane`s and change the active pane to be the target -/// `Pane`. -pub fn swap_panes(pane_a: &Pane, pane_b: &Pane) -> Result<(), ParseError> { - let pa_id = format!("%{}", pane_a.id); - let pb_id = format!("%{}", pane_b.id); - - let args = vec!["swap-pane", "-s", &pa_id, "-t", &pb_id]; - - process::execute("tmux", &args)?; - - Ok(()) -} - -/// Ask tmux to kill the provided `Pane`. -pub fn kill_pane(pane: &Pane) -> Result<(), ParseError> { - let p_id = format!("%{}", pane.id); - - let args = vec!["kill-pane", "-t", &p_id]; +/// Ask tmux to swap the current Pane with the target_pane (uses Tmux format). +pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { + // -Z: keep the window zoomed if it was zoomed. + let args = vec!["swap-pane", "-Z", "-s", target_pane]; process::execute("tmux", &args)?; @@ -244,25 +234,28 @@ pub fn kill_pane(pane: &Pane) -> Result<(), ParseError> { #[cfg(test)] mod tests { use super::Pane; + use super::PaneId; use copyrat::error; + use std::str::FromStr; #[test] fn test_parse_pass() { let output = vec!["%52:false:62:3:false", "%53:false:23::true"]; let panes: Result, error::ParseError> = - output.iter().map(|&line| Pane::parse(line)).collect(); + output.iter().map(|&line| Pane::from_str(line)).collect(); let panes = panes.expect("Could not parse tmux panes"); let expected = vec![ Pane { - id: 52, + id: PaneId::from_str("%52").unwrap(), in_mode: false, height: 62, scroll_position: 3, is_active: false, }, Pane { - id: 53, + // id: PaneId::from_str("%53").unwrap(), + id: PaneId(String::from("%53")), in_mode: false, height: 23, scroll_position: 0, diff --git a/tmux-copyrat.tmux b/tmux-copyrat.tmux index 9409dc9..a7b7c1f 100755 --- a/tmux-copyrat.tmux +++ b/tmux-copyrat.tmux @@ -6,9 +6,13 @@ DEFAULT_COPYRAT_KEY="space" COPYRAT_KEY=$(tmux show-option -gqv @copyrat-key) COPYRAT_KEY=${COPYRAT_KEY:-$DEFAULT_COPYRAT_KEY} +DEFAULT_COPYRAT_WINDOW_NAME="[copyrat]" +COPYRAT_WINDOW_NAME=$(tmux show-option -gqv @copyrat-window-name) +COPYRAT_WINDOW_NAME=${COPYRAT_WINDOW_NAME:-$DEFAULT_COPYRAT_WINDOW_NAME} + BINARY="${CURRENT_DIR}/target/release/tmux-copyrat" -tmux bind-key $COPYRAT_KEY run-shell -b "${BINARY} -T" +tmux bind-key ${COPYRAT_KEY} new-window -d -n ${COPYRAT_WINDOW_NAME} "${BINARY} --window-name ${COPYRAT_WINDOW_NAME} --reverse --unique" if [ ! -f "$BINARY" ]; then cd "${CURRENT_DIR}" && cargo build --release diff --git a/tmux-thumbs.sh b/tmux-thumbs.sh deleted file mode 100755 index fce71f1..0000000 --- a/tmux-thumbs.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -[ -f ~/.bash_profile ] && source ~/.bash_profile - -PARAMS=() - -function add-option-param { - VALUE=$(tmux show-options -vg @thumbs-$1 2> /dev/null) - - if [[ ${VALUE} ]]; then - PARAMS+=("--$1=${VALUE}") - fi -} - -add-option-param "command" -add-option-param "upcase-command" - -# Remove empty arguments from PARAMS. -# Otherwise, they would choke up tmux-thumbs when passed to it. -for i in "${!PARAMS[@]}"; do - [ -n "${PARAMS[$i]}" ] || unset "PARAMS[$i]" -done - -CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -${CURRENT_DIR}/target/release/tmux-thumbs --dir "${CURRENT_DIR}" "${PARAMS[@]}" - -true