├── .gitignore ├── video ├── screencast_img.png ├── README.md └── script.md ├── test ├── helpers │ ├── test.exp │ ├── run_tmux.exp │ ├── setup.exp │ ├── setup_with_custom_searches.exp │ ├── setup_tmux_conf.sh │ ├── expect_copycat_helpers.exp │ ├── setup_tmux_conf_with_custom_searches.sh │ ├── expect_copycat_assertions.exp │ └── expect_helpers.exp ├── README.md ├── test_git_hash_search.exp ├── run-tests-within-vm ├── test_git_status_search.exp ├── test_digit_search.exp ├── test_user_defined_search.exp ├── test_file_search.exp ├── test_free_search.exp └── test_url_search.exp ├── scripts ├── copycat_search.sh ├── copycat_mode_quit.sh ├── copycat_mode_start.sh ├── stored_search_helpers.sh ├── variables.sh ├── copycat_git_special.sh ├── copycat_generate_results.sh ├── copycat_mode_bindings.sh ├── check_tmux_version.sh ├── helpers.sh └── copycat_jump.sh ├── Vagrantfile ├── .gitattributes ├── docs ├── installation_for_tmux_2.3.md ├── limitations.md ├── customizations.md └── defining_new_stored_searches.md ├── .travis.yml ├── LICENSE.md ├── vagrant_ubuntu_provisioning_two_five.sh ├── run-tests ├── copycat.tmux ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | -------------------------------------------------------------------------------- /video/screencast_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmux-plugins/tmux-copycat/HEAD/video/screencast_img.png -------------------------------------------------------------------------------- /test/helpers/test.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | spawn bash 4 | send "./test/helpers/setup_tmux_conf.sh\r" 5 | -------------------------------------------------------------------------------- /test/helpers/run_tmux.exp: -------------------------------------------------------------------------------- 1 | # steps for starting tmux within an expect script 2 | 3 | spawn tmux 4 | # delay with sleep to compensate for tmux starting time 5 | sleep 1 6 | -------------------------------------------------------------------------------- /scripts/copycat_search.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | main() { 6 | tmux command-prompt -p "copycat search:" "run-shell \"$CURRENT_DIR/copycat_mode_start.sh '%1'\"" 7 | } 8 | main 9 | -------------------------------------------------------------------------------- /video/README.md: -------------------------------------------------------------------------------- 1 | ## Tmux copycat screencast 2 | 3 | This directory contains docs used for creating 4 | [tmux copycat screencast](https://vimeo.com/101867689). 5 | 6 | - `script.md` - this file contains a script and a voiceover used to produce the 7 | screencast 8 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | VAGRANTFILE_API_VERSION = '2' 2 | 3 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 4 | config.vm.define :ubuntu_two_five do |ubuntu| 5 | ubuntu.vm.box = 'hashicorp/precise32' 6 | ubuntu.vm.provision 'shell', path: 'vagrant_ubuntu_provisioning_two_five.sh' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force text files to have unix eols, so Windows/Cygwin does not break them 2 | *.* eol=lf 3 | 4 | # These files are unfortunately not recognized as text files so 5 | # explicitly listing them here 6 | Vagrantfile eol=lf 7 | run-tests eol=lf 8 | test/run-tests-within-vm eol=lf 9 | *.png binary 10 | -------------------------------------------------------------------------------- /scripts/copycat_mode_quit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR/helpers.sh" 6 | 7 | main() { 8 | if in_copycat_mode; then 9 | reset_copycat_position 10 | unset_copycat_mode 11 | copycat_decrease_counter 12 | fi 13 | } 14 | main 15 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ## Tmux copycat test suite 2 | 3 | This directory contains test files for tmux copycat. 4 | 5 | Tests are written with the [expect tool](http://expect.sourceforge.net/). 6 | 7 | ### Dependencies 8 | 9 | - [Vagrant](https://www.vagrantup.com/) 10 | 11 | ### Running the test suite 12 | 13 | From the `tmux copycat` project top directory run: 14 | 15 | $ ./run-tests 16 | -------------------------------------------------------------------------------- /test/helpers/setup.exp: -------------------------------------------------------------------------------- 1 | # sourcing helper functions 2 | source "./test/helpers/expect_helpers.exp" 3 | source "./test/helpers/expect_copycat_helpers.exp" 4 | source "./test/helpers/expect_copycat_assertions.exp" 5 | 6 | # .tmux.conf 7 | exec "./test/helpers/setup_tmux_conf.sh" 8 | expect_setup 9 | 10 | # exit status global var is successful by default 11 | set exit_status 0 12 | 13 | # run tmux (doesn't work when within a proc) 14 | source "./test/helpers/run_tmux.exp" 15 | -------------------------------------------------------------------------------- /test/helpers/setup_with_custom_searches.exp: -------------------------------------------------------------------------------- 1 | # sourcing helper functions 2 | source "./test/helpers/expect_helpers.exp" 3 | source "./test/helpers/expect_copycat_helpers.exp" 4 | source "./test/helpers/expect_copycat_assertions.exp" 5 | 6 | # .tmux.conf 7 | exec "./test/helpers/setup_tmux_conf_with_custom_searches.sh" 8 | expect_setup 9 | 10 | # exit status global var is successful by default 11 | set exit_status 0 12 | 13 | # run tmux (doesn't work when within a proc) 14 | source "./test/helpers/run_tmux.exp" 15 | -------------------------------------------------------------------------------- /scripts/copycat_mode_start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | SUPPORTED_VERSION="1.9" 6 | 7 | PATTERN="$1" 8 | 9 | supported_tmux_version_ok() { 10 | $CURRENT_DIR/check_tmux_version.sh "$SUPPORTED_VERSION" 11 | } 12 | 13 | main() { 14 | local pattern="$1" 15 | if supported_tmux_version_ok; then 16 | $CURRENT_DIR/copycat_generate_results.sh "$pattern" # will `exit 0` if no results 17 | $CURRENT_DIR/copycat_jump.sh 'next' 18 | fi 19 | } 20 | main "$PATTERN" 21 | -------------------------------------------------------------------------------- /test/test_git_hash_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | enter_test_git_repo 6 | 7 | # Match regular SHA-1 hashes 8 | #--------------------------- 9 | git_log_reverse 10 | tmux_alt_h 11 | assert_highlighted "935929c4c7265666e41e727f97a87d1af00a8b40" "match regular SHA-1 hashes" 12 | 13 | #Match shortened SHA-1 hashes 14 | #---------------------------- 15 | git_log_reverse_short 16 | tmux_alt_h 17 | assert_highlighted "935929c" "match shortened SHA-1 hashes" 18 | 19 | # quit 20 | #----- 21 | remove_test_git_repo 22 | teardown_and_exit 23 | -------------------------------------------------------------------------------- /docs/installation_for_tmux_2.3.md: -------------------------------------------------------------------------------- 1 | # Installation for Tmux 2.3 and earlier 2 | 3 | The installation steps for Tmux 2.3 are based on 4 | [manual installation](https://github.com/tmux-plugins/tmux-copycat#manual-installation) 5 | steps, with the addition of using `tmux-23` branch. 6 | 7 | Create tmux plugins dir: 8 | 9 | $ mkdir -p ~/.tmux/plugins 10 | 11 | Clone the repo: 12 | 13 | $ git clone -b tmux-23 https://github.com/tmux-plugins/tmux-copycat ~/.tmux/plugins/tmux-copycat 14 | 15 | Add this line to the bottom of `.tmux.conf`: 16 | 17 | run-shell ~/clone/path/copycat.tmux 18 | 19 | Reload TMUX environment with: `$ tmux source-file ~/.tmux.conf`. You should now 20 | be able to use the plugin. 21 | -------------------------------------------------------------------------------- /test/helpers/setup_tmux_conf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tests helper script for setting up `.tmux.conf` within the VM. 4 | # To be used by sourcing from within individual test scripts. 5 | BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" 6 | 7 | setup_tmux_conf() { 8 | # Copy mode (vi or emacs) is automatically determined from EDITOR 9 | # environment variable set in test runner file `test/run-tests-within-vm`. 10 | echo "bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel" > ~/.tmux.conf 11 | echo "bind-key -T copy-mode y send-keys -X copy-selection-and-cancel" >> ~/.tmux.conf 12 | echo "run-shell '$BASE_DIR/copycat.tmux'" >> ~/.tmux.conf 13 | } 14 | setup_tmux_conf 15 | -------------------------------------------------------------------------------- /scripts/stored_search_helpers.sh: -------------------------------------------------------------------------------- 1 | stored_search_not_defined() { 2 | local key="$1" 3 | local search_value="$(tmux show-option -gqv "${COPYCAT_VAR_PREFIX}_${key}")" 4 | [ -z $search_value ] 5 | } 6 | 7 | stored_search_vars() { 8 | tmux show-options -g | 9 | \grep -i "^${COPYCAT_VAR_PREFIX}_" | 10 | cut -d ' ' -f1 | # cut just variable names 11 | xargs # splat var names in one line 12 | } 13 | 14 | # get the search key from the variable name 15 | get_stored_search_key() { 16 | local search_var="$1" 17 | echo "$(echo "$search_var" | sed "s/^${COPYCAT_VAR_PREFIX}_//")" 18 | } 19 | 20 | get_stored_search_pattern() { 21 | local search_var="$1" 22 | echo "$(get_tmux_option "$search_var" "")" 23 | } 24 | -------------------------------------------------------------------------------- /scripts/variables.sh: -------------------------------------------------------------------------------- 1 | # stored search variable prefix 2 | COPYCAT_VAR_PREFIX="@copycat_search" 3 | 4 | # basic search 5 | default_copycat_search_key="/" 6 | copycat_search_option="@copycat_search" 7 | 8 | # git special search 9 | default_git_search_key="C-g" 10 | copycat_git_search_option="@copycat_git_special" 11 | 12 | # regular searches 13 | default_file_search_key="C-f" 14 | copycat_file_search_option="@copycat_file_search" 15 | 16 | default_url_search_key="C-u" 17 | copycat_url_search_option="@copycat_url_search" 18 | 19 | default_digit_search_key="C-d" 20 | copycat_digit_search_option="@copycat_digit_search" 21 | 22 | default_hash_search_key="M-h" 23 | copycat_hash_search_option="@copycat_hash_search" 24 | 25 | default_ip_search_key="M-i" 26 | copycat_ip_search_option="@copycat_ip_search" 27 | -------------------------------------------------------------------------------- /test/helpers/expect_copycat_helpers.exp: -------------------------------------------------------------------------------- 1 | # a set of tmux specific helpers 2 | 3 | proc tmux_ctrl_f {} { 4 | send "" 5 | sleep 0.7 6 | } 7 | 8 | proc tmux_ctrl_d {} { 9 | send "" 10 | sleep 0.7 11 | } 12 | 13 | proc tmux_ctrl_u {} { 14 | send "" 15 | sleep 0.7 16 | } 17 | 18 | proc tmux_ctrl_r {} { 19 | send "" 20 | sleep 0.7 21 | } 22 | 23 | proc tmux_ctrl_g {} { 24 | send "" 25 | sleep 0.7 26 | } 27 | 28 | proc tmux_alt_h {} { 29 | send "h" 30 | sleep 0.7 31 | } 32 | 33 | proc tmux_ctrl_t {} { 34 | send "" 35 | sleep 0.7 36 | } 37 | 38 | proc search {text} { 39 | send "/" 40 | sleep 0.5 41 | send "$text\r" 42 | sleep 0.7 43 | } 44 | 45 | proc next_match {} { 46 | send "n" 47 | sleep 0.7 48 | } 49 | 50 | proc previous_match {} { 51 | send "N" 52 | sleep 0.7 53 | } 54 | -------------------------------------------------------------------------------- /test/helpers/setup_tmux_conf_with_custom_searches.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tests helper script for setting up `.tmux.conf` within the VM. 4 | # To be used by sourcing from within individual test scripts. 5 | 6 | BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" 7 | 8 | setup_tmux_conf() { 9 | # Copy mode (vi or emacs) is automatically determined from EDITOR 10 | # environment variable set in test runner file `test/run-tests-within-vm`. 11 | echo "bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel" > ~/.tmux.conf 12 | echo "bind-key -T copy-mode y send-keys -X copy-selection-and-cancel" >> ~/.tmux.conf 13 | echo "set -g @copycat_search_C-t 'random string[[:digit:]]+'" >> ~/.tmux.conf 14 | echo "run-shell '$BASE_DIR/copycat.tmux'" >> ~/.tmux.conf 15 | } 16 | setup_tmux_conf 17 | -------------------------------------------------------------------------------- /docs/limitations.md: -------------------------------------------------------------------------------- 1 | # Limitations 2 | 3 | - This plugin tries hard to consistently enable "marketed" features. It uses some 4 | hacks to go beyond the APIs Tmux provides. Because of this, it might have some 5 | "rough edges" and there's nothing that can be done. 6 | 7 | Examples: non-perfect file and url matching and selection. That said, usage 8 | should be fine in +90% cases. 9 | 10 | - feel free to report search cases you think should work, but are not 11 | (provide examples pls!). I'm open to the idea of adding more saved searches. 12 | 13 | - Tmux `vi` copy mode works faster than `emacs`. If you don't have a preference 14 | yet and to speed up `tmux_copycat`, I recommend putting this in `.tmux.conf` 15 | to set Tmux copy mode to `vi`: 16 | 17 | set -g mode-keys vi 18 | 19 | - remapping `Escape` key in copy mode will break the plugin. If you have this 20 | in your `.tmux.conf`, please consider removing it: 21 | 22 | bind -t vi-copy Escape cancel 23 | 24 | After removing this key binding, don't forget to restart tmux server! 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # generic packages 2 | before_install: 3 | - sudo apt-get update 4 | - sudo apt-get install -y git-core expect gawk 5 | - sudo apt-get install -y python-software-properties software-properties-common 6 | 7 | # install Tmux 2.5 8 | install: 9 | - VERSION=2.5 10 | - sudo apt-get -y remove tmux 11 | - sudo apt-get -y install wget tar libevent-dev libncurses-dev make 12 | - wget https://github.com/tmux/tmux/releases/download/${VERSION}/tmux-${VERSION}.tar.gz 13 | - tar xf tmux-${VERSION}.tar.gz 14 | - rm -f tmux-${VERSION}.tar.gz 15 | - cd tmux-${VERSION} 16 | - ./configure 17 | - make 18 | - sudo make install 19 | - cd - 20 | - sudo rm -rf /usr/local/src/tmux-* 21 | - sudo mv tmux-${VERSION} /usr/local/src 22 | 23 | # override PS1 and irb prompt, fetch a git repo used for testing 24 | before_script: 25 | - echo 'export PS1="\$ "' >> ~/.bashrc 26 | - echo 'IRB.conf[:PROMPT_MODE] = :SIMPLE' >> ~/.irbrc 27 | - git clone https://github.com/tmux-plugins/tmux-example-plugin ~/tmux-example-plugin 28 | 29 | script: ./test/run-tests-within-vm 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Bruno Sutic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/customizations.md: -------------------------------------------------------------------------------- 1 | # Customizations 2 | 3 | Most of the behavior of tmux-copycat can be customized via tmux options.
4 | To set a value, just put `set -g @option 'value'` in your `.tmux.conf` before 5 | loading the tmux-copycat plugin. 6 | 7 | Other options: 8 | 9 | - `@copycat_search` (default `/`) defines the key-binding used (after prefix) to 10 | start an interactive search. 11 | - `@copycat_next` (default `n`) defines the key (without prefix) used to jump to 12 | next search result. 13 | - `@copycat_prev` (default `N`) defines the key (without prefix) used to jump to 14 | previous search result. 15 | 16 | Options for predefined searches: 17 | 18 | - `@copycat_git_special` (default `C-g`) git status search 19 | - `@copycat_file_search` (default `C-f`) file search 20 | - `@copycat_url_search` (default `C-u`) url search 21 | - `@copycat_digit_search` (default `C-d`) digit search 22 | - `@copycat_hash_search` (default `M-h`) SHA-1 hash search 23 | - `@copycat_ip_search` (default `M-i`) IP address search 24 | 25 | Example: to remap default file search to use `C-t` put 26 | `set -g @copycat_file_search 'C-t'` in `.tmux.conf`. 27 | -------------------------------------------------------------------------------- /test/run-tests-within-vm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | # running test suite is successful by default 6 | tests_exit_value=0 7 | 8 | test_files() { 9 | ls -1 $CURRENT_DIR | # test files are in current dir 10 | \grep -i '^test' | # test file names start with 'test' 11 | xargs # file names in one line 12 | } 13 | 14 | set_global_exit_val_to_false() { 15 | tests_exit_value=1 16 | } 17 | 18 | run_test() { 19 | local test_file="$1" 20 | local tmux_copy_mode="$2" 21 | 22 | # running test 23 | echo "Test: $test_file (copy-mode $tmux_copy_mode)" 24 | 25 | # by setting the EDITOR var tmux chooses vi or emacs copy mode 26 | EDITOR="$tmux_copy_mode" $CURRENT_DIR/$test_file 27 | 28 | # handling exit value 29 | local exit_value="$?" 30 | if [ "$exit_value" == 0 ]; then 31 | echo "Success" 32 | else 33 | echo "Test failed!" 34 | set_global_exit_val_to_false 35 | fi 36 | echo 37 | } 38 | 39 | main() { 40 | local test_file 41 | local test_dir_path="./" 42 | for test_file in $(test_files); do 43 | run_test "$test_file" "vi" 44 | run_test "$test_file" "emacs" 45 | done 46 | exit "$tests_exit_value" 47 | } 48 | main 49 | -------------------------------------------------------------------------------- /vagrant_ubuntu_provisioning_two_five.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # override PS1 prompt 4 | echo 'export PS1="\$ "' >> /home/vagrant/.bashrc 5 | # simplify irb prompt 6 | echo 'IRB.conf[:PROMPT_MODE] = :SIMPLE' >> /home/vagrant/.irbrc 7 | chown -R vagrant:vagrant /home/vagrant/.irbrc 8 | 9 | sudo apt-get update 10 | sudo apt-get install -y make 11 | sudo apt-get install -y git-core expect vim gawk 12 | sudo apt-get install -y python-software-properties software-properties-common 13 | 14 | # install Tmux 2.5 15 | VERSION=2.5 16 | sudo apt-get -y remove tmux 17 | sudo apt-get -y install wget tar libevent-dev libncurses-dev 18 | wget https://github.com/tmux/tmux/releases/download/${VERSION}/tmux-${VERSION}.tar.gz 19 | tar xf tmux-${VERSION}.tar.gz 20 | rm -f tmux-${VERSION}.tar.gz 21 | cd tmux-${VERSION} 22 | ./configure 23 | make 24 | sudo make install 25 | cd - 26 | sudo rm -rf /usr/local/src/tmux-* 27 | sudo mv tmux-${VERSION} /usr/local/src 28 | 29 | # clone a repo used later for tests 30 | git clone https://github.com/tmux-plugins/tmux-example-plugin /home/vagrant/tmux-example-plugin 31 | chown -R vagrant:vagrant /home/vagrant/tmux-example-plugin 32 | 33 | sudo locale-gen "en_US.UTF-8" 34 | sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 35 | -------------------------------------------------------------------------------- /docs/defining_new_stored_searches.md: -------------------------------------------------------------------------------- 1 | # Defining new stored searches 2 | 3 | To speed up the workflow you can define new bindings in `.tmux.conf` for 4 | searches you use often. 5 | 6 | After adding any of the below snippets, make sure to reload your tmux 7 | configuration: 8 | 9 | # type this in the terminal 10 | $ tmux source-file ~/.tmux.conf 11 | 12 | Dummy examples (just for testing): 13 | 14 | * `prefix + ctrl-t` example string search 15 | 16 | set -g @copycat_search_C-t 'search me' 17 | 18 | * `prefix + alt-t` example regex search 19 | 20 | set -g @copycat_search_M-t 'regex search[[:alnum:]]\*' 21 | 22 | ### Useful searches 23 | 24 | * `prefix + ctrl-e` in the Rails log output searches for previous request start 25 | 26 | set -g @copycat_search_C-e '^Processing[[:space:]]by[[:space:]][^[:space:]]*' 27 | 28 | * `prefix + D` searches for numbers at the *beginning* of line.
29 | Useful with `$ pgrep -lf process` command to quickly select process PID. 30 | 31 | set -g @copycat_search_D '^[[:digit:]]+' 32 | 33 | * `prefix + G` searches for git commit SHA1.
34 | Works for both the short (5 chars) and full (40 chars) versions. 35 | 36 | set -g @copycat_search_G '\b[0-9a-f]{5,40}\b' 37 | 38 | 39 | Have your own custom search? Please share it in 40 | [the discussion](https://github.com/tmux-plugins/tmux-copycat/issues/57). 41 | -------------------------------------------------------------------------------- /scripts/copycat_git_special.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | PANE_CURRENT_PATH="$1" 6 | 7 | source "$CURRENT_DIR/helpers.sh" 8 | 9 | git_status_files() { 10 | git -C "$PANE_CURRENT_PATH" status -s 11 | } 12 | 13 | formatted_git_status() { 14 | local raw_gist_status="$(git_status_files)" 15 | echo "$raw_gist_status" | cut -c 4- 16 | } 17 | 18 | exit_if_no_results() { 19 | local results="$1" 20 | if [ -z "$results" ]; then 21 | display_message "No results!" 22 | exit 0 23 | fi 24 | } 25 | 26 | concatenate_files() { 27 | local git_status_files="$(formatted_git_status)" 28 | exit_if_no_results "$git_status_files" 29 | 30 | local result="" 31 | # Undefined until later within a while loop. 32 | local file_separator 33 | while read -r line; do 34 | result="${result}${file_separator}${line}" 35 | file_separator="|" 36 | done <<< "$git_status_files" 37 | echo "$result" 38 | } 39 | 40 | # Creates one, big regex out of git status files. 41 | # Example: 42 | # `git status` shows files `foo.txt` and `bar.txt` 43 | # output regex will be: 44 | # `(foo.txt|bar.txt) 45 | git_status_files_regex() { 46 | local concatenated_files="$(concatenate_files)" 47 | local regex_result="(${concatenated_files})" 48 | echo "$regex_result" 49 | } 50 | 51 | main() { 52 | local search_regex="$(git_status_files_regex)" 53 | # starts copycat mode 54 | $CURRENT_DIR/copycat_mode_start.sh "$search_regex" 55 | } 56 | main 57 | -------------------------------------------------------------------------------- /run-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # For each virtual machine where tests run, this script performs the following: 4 | # - starts VM 5 | # - starts the test suite witin a VM 6 | # - stops the VM after the test suite is done 7 | 8 | # global variable for script exit value 9 | export EXIT_VALUE=0 10 | 11 | register_failing_specs() { 12 | EXIT_VALUE=1 13 | } 14 | 15 | run_vagrant() { 16 | local box="$1" 17 | vagrant up "$box" 18 | } 19 | 20 | # Halt vagrant after tests are done running, unless KEEP_RUNNING environment 21 | # variable is set to 'true'. 22 | stop_vagrant() { 23 | local box="$1" 24 | if [ -z "$KEEP_RUNNING" ]; then 25 | vagrant halt "$box" 26 | else 27 | echo 28 | echo "KEEP_RUNNING is set. Vagrant not halted." 29 | fi 30 | } 31 | 32 | run_tests() { 33 | local box="$1" 34 | local test_file="/vagrant/test/run-tests-within-vm" 35 | echo "Running test suite on $box from: $test_file" 36 | echo 37 | vagrant ssh "$box" -c "cd /vagrant; $test_file" 38 | } 39 | 40 | exit_message() { 41 | local exit_val="$1" 42 | echo 43 | if [ $exit_val == 0 ]; then 44 | echo "Success, tests pass!" 45 | else 46 | echo "Tests failed!" 1>&2 47 | fi 48 | } 49 | 50 | run_tests_on_vm() { 51 | local vm="$1" 52 | run_vagrant "$vm" 53 | run_tests "$vm" 54 | local tests_exit_value="$?" 55 | stop_vagrant "$vm" 56 | if [ $tests_exit_value -gt 0 ]; then 57 | register_failing_specs 58 | fi 59 | } 60 | 61 | main() { 62 | run_tests_on_vm "ubuntu_two_five" 63 | 64 | exit_message "$EXIT_VALUE" 65 | exit "$EXIT_VALUE" 66 | } 67 | main 68 | -------------------------------------------------------------------------------- /scripts/copycat_generate_results.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR/helpers.sh" 6 | 7 | search_pattern="$1" 8 | 9 | capture_pane() { 10 | local file=$1 11 | # copying 9M lines back will hopefully fetch the whole scrollback 12 | tmux capture-pane -S -9000000 -p > "$file" 13 | } 14 | 15 | # doing 2 things in 1 step so that we don't write to disk too much 16 | reverse_and_create_copycat_file() { 17 | local file=$1 18 | local copycat_file=$2 19 | local grep_pattern=$3 20 | (tac 2>/dev/null || tail -r) < "$file" | grep -oniE "$grep_pattern" > "$copycat_file" 21 | } 22 | 23 | delete_old_files() { 24 | local scrollback_filename="$(get_scrollback_filename)" 25 | local copycat_filename="$(get_copycat_filename)" 26 | rm -f "$scrollback_filename" "$copycat_filename" 27 | } 28 | 29 | generate_copycat_file() { 30 | local grep_pattern="$1" 31 | local scrollback_filename="$(get_scrollback_filename)" 32 | local copycat_filename="$(get_copycat_filename)" 33 | mkdir -p "$(_get_tmp_dir)" 34 | chmod 0700 "$(_get_tmp_dir)" 35 | capture_pane "$scrollback_filename" 36 | reverse_and_create_copycat_file "$scrollback_filename" "$copycat_filename" "$grep_pattern" 37 | } 38 | 39 | if_no_results_exit_with_message() { 40 | local copycat_filename="$(get_copycat_filename)" 41 | # check for empty filename 42 | if ! [ -s "$copycat_filename" ]; then 43 | display_message "No results!" 44 | exit 0 45 | fi 46 | } 47 | 48 | main() { 49 | local grep_pattern="$1" 50 | if not_in_copycat_mode; then 51 | delete_old_files 52 | generate_copycat_file "$grep_pattern" 53 | if_no_results_exit_with_message 54 | set_copycat_mode 55 | copycat_increase_counter 56 | fi 57 | } 58 | main "$search_pattern" 59 | -------------------------------------------------------------------------------- /test/helpers/expect_copycat_assertions.exp: -------------------------------------------------------------------------------- 1 | # tmux copycat assertion helpers 2 | 3 | # Asserts text that is crrently highlighted (in copy mode). 4 | proc assert_highlighted {text message} { 5 | set checker [ _generate_checker ] 6 | # Asserted text first has to be 'yanked' and displayed before `expect`. 7 | _display_highlighted_with_checker_text "$checker" 8 | expect { 9 | "$checker$text" { puts " Success: $message" } 10 | timeout { puts " Fail: $message"; exit_status_false } 11 | } 12 | } 13 | 14 | proc irb_assert_highlighted {text message} { 15 | set checker [ _generate_checker ] 16 | _irb_display_highlighted_with_checker_text "$checker" 17 | expect { 18 | "$checker$text" { puts " Success: $message" } 19 | timeout { puts " Fail: $message"; exit_status_false } 20 | } 21 | } 22 | 23 | proc assert_on_screen {text message} { 24 | expect { 25 | "$text" { puts " Success: $message" } 26 | timeout { puts " Fail: $message"; exit_status_false } 27 | } 28 | } 29 | 30 | # private functions 31 | 32 | proc _generate_checker {} { 33 | set random [ expr { rand()*10000 } ] 34 | set checker "Checker $random:" 35 | return $checker 36 | } 37 | 38 | proc _display_highlighted_with_checker_text {checker} { 39 | _copy_mode_copy 40 | send "" 41 | sleep 0.1 42 | send "echo $checker" 43 | sleep 0.1 44 | _tmux_paste 45 | send "\r" 46 | } 47 | 48 | proc _irb_display_highlighted_with_checker_text {checker} { 49 | _copy_mode_copy 50 | send "\r" 51 | sleep 0.1 52 | send "puts '$checker" 53 | sleep 0.1 54 | _tmux_paste 55 | send "'" 56 | send "\r" 57 | } 58 | 59 | proc _copy_mode_copy {} { 60 | send "y" 61 | sleep 0.2 62 | } 63 | 64 | proc _tmux_paste {} { 65 | send "]" 66 | sleep 0.1 67 | } 68 | -------------------------------------------------------------------------------- /scripts/copycat_mode_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR/helpers.sh" 6 | AWK_CMD='awk' 7 | if command_exists gawk; then 8 | AWK_CMD='gawk' 9 | fi 10 | 11 | # Extends a keyboard key. 12 | # Benefits: tmux won't report errors and everything will work fine even if the 13 | # script is deleted. 14 | extend_key() { 15 | local key="$1" 16 | local script="$2" 17 | local copy_mode 18 | copy_mode=$(tmux_copy_mode_string) 19 | 20 | # 1. The default command for 'key' is sent to tmux. This ensures the 21 | # default key action is done. 22 | # 2. Script is executed. 23 | # 3. `true` command ensures an exit status 0 is returned. This ensures 24 | # a user never gets an error msg - even if the script file from step 2 25 | # is deleted. 26 | tmux list-keys -T "$copy_mode" | 27 | "$AWK_CMD" -v mode="$copy_mode" -v key="$key" -v script="$script" ' 28 | /copycat/ { next } 29 | $3 == mode && $4 == key { 30 | $1="" 31 | $2="" 32 | $3="" 33 | $4="" 34 | cmd=$0 35 | gsub(/["\\]/, "\\\\&", cmd) 36 | system("tmux bind-key -T " mode " " key " run-shell \"tmux " cmd "; " script "; true\"") 37 | }' 38 | } 39 | 40 | copycat_cancel_bindings() { 41 | # keys that quit copy mode are enhanced to quit copycat mode as well. 42 | local cancel_mode_bindings=$(copycat_quit_copy_mode_keys) 43 | local key 44 | for key in $cancel_mode_bindings; do 45 | extend_key "$key" "$CURRENT_DIR/copycat_mode_quit.sh" 46 | done 47 | } 48 | 49 | copycat_mode_bindings() { 50 | extend_key "$(copycat_next_key)" "$CURRENT_DIR/copycat_jump.sh 'next'" 51 | extend_key "$(copycat_prev_key)" "$CURRENT_DIR/copycat_jump.sh 'prev'" 52 | } 53 | 54 | main() { 55 | copycat_mode_bindings 56 | copycat_cancel_bindings 57 | } 58 | main 59 | -------------------------------------------------------------------------------- /scripts/check_tmux_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION="$1" 4 | UNSUPPORTED_MSG="$2" 5 | 6 | get_tmux_option() { 7 | local option=$1 8 | local default_value=$2 9 | local option_value=$(tmux show-option -gqv "$option") 10 | if [ -z "$option_value" ]; then 11 | echo "$default_value" 12 | else 13 | echo "$option_value" 14 | fi 15 | } 16 | 17 | # Ensures a message is displayed for 5 seconds in tmux prompt. 18 | # Does not override the 'display-time' tmux option. 19 | display_message() { 20 | local message="$1" 21 | 22 | # display_duration defaults to 5 seconds, if not passed as an argument 23 | if [ "$#" -eq 2 ]; then 24 | local display_duration="$2" 25 | else 26 | local display_duration="5000" 27 | fi 28 | 29 | # saves user-set 'display-time' option 30 | local saved_display_time=$(get_tmux_option "display-time" "750") 31 | 32 | # sets message display time to 5 seconds 33 | tmux set-option -gq display-time "$display_duration" 34 | 35 | # displays message 36 | tmux display-message "$message" 37 | 38 | # restores original 'display-time' value 39 | tmux set-option -gq display-time "$saved_display_time" 40 | } 41 | 42 | # this is used to get "clean" integer version number. Examples: 43 | # `tmux 1.9` => `19` 44 | # `1.9a` => `19` 45 | get_digits_from_string() { 46 | local string="$1" 47 | local only_digits="$(echo "$string" | tr -dC '[:digit:]')" 48 | echo "$only_digits" 49 | } 50 | 51 | tmux_version_int() { 52 | local tmux_version_string=$(tmux -V) 53 | echo "$(get_digits_from_string "$tmux_version_string")" 54 | } 55 | 56 | unsupported_version_message() { 57 | if [ -n "$UNSUPPORTED_MSG" ]; then 58 | echo "$UNSUPPORTED_MSG" 59 | else 60 | echo "Error, Tmux version unsupported! Please install Tmux version $VERSION or greater!" 61 | fi 62 | } 63 | 64 | exit_if_unsupported_version() { 65 | local current_version="$1" 66 | local supported_version="$2" 67 | if [ "$current_version" -lt "$supported_version" ]; then 68 | display_message "$(unsupported_version_message)" 69 | exit 1 70 | fi 71 | } 72 | 73 | main() { 74 | local supported_version_int="$(get_digits_from_string "$VERSION")" 75 | local current_version_int="$(tmux_version_int)" 76 | exit_if_unsupported_version "$current_version_int" "$supported_version_int" 77 | } 78 | main 79 | -------------------------------------------------------------------------------- /test/test_git_status_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | enter_test_git_repo 6 | 7 | # single file tests 8 | #------------------ 9 | change_file "README.md" 10 | git_status 11 | tmux_ctrl_g 12 | assert_highlighted "README.md" "change commited file" 13 | clean_git_repo 14 | 15 | change_file "new_file.txt" 16 | git_status 17 | tmux_ctrl_g 18 | assert_highlighted "new_file.txt" "change new file" 19 | clean_git_repo 20 | 21 | change_file "another_file.txt" 22 | send "some_command another_file.txt" 23 | sleep 0.1 24 | tmux_ctrl_g 25 | assert_highlighted "another_file.txt" "search works for files just written on the command line" 26 | clean_git_repo 27 | 28 | # multile files changed 29 | #---------------------- 30 | change_file "README.md" 31 | change_file "some_new_file.txt" 32 | git_status 33 | tmux_ctrl_g 34 | assert_highlighted "some_new_file.txt" "multiple files, first file" 35 | git_status 36 | tmux_ctrl_g 37 | next_match 38 | assert_highlighted "README.md" "multiple files, second file" 39 | clean_git_repo 40 | 41 | # navigation tests 42 | #----------------- 43 | change_file "README.md" 44 | change_file "random_file1.txt" 45 | change_file "random_file2.txt" 46 | 47 | git_status 48 | tmux_ctrl_g 49 | next_match 50 | next_match 51 | assert_highlighted "README.md" "navigation, last file" 52 | 53 | git_status 54 | tmux_ctrl_g 55 | next_match 56 | next_match 57 | next_match 58 | previous_match 59 | previous_match 60 | previous_match 61 | assert_highlighted "random_file2.txt" "navigation, first file" 62 | 63 | clean_git_repo 64 | 65 | # files with unusual names 66 | #------------------------- 67 | change_file "'file with spaces.txt'" 68 | git_status 69 | tmux_ctrl_g 70 | assert_highlighted "file with spaces.txt" "filenames with spaces" 71 | clean_git_repo 72 | 73 | # no match, first and last match 74 | #------------------------------- 75 | new_tmux_pane 76 | enter_test_git_repo 77 | tmux_ctrl_g 78 | assert_on_screen "No results!" "No results is displayed when no results" 79 | clean_git_repo 80 | 81 | new_tmux_pane 82 | enter_test_git_repo 83 | change_file "README.md" 84 | git_status 85 | tmux_ctrl_g 86 | next_match 87 | next_match 88 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 89 | clean_git_repo 90 | 91 | new_tmux_pane 92 | enter_test_git_repo 93 | change_file "README.md" 94 | git_status 95 | tmux_ctrl_g 96 | previous_match 97 | assert_on_screen "First match!" "'First match' is displayed when first match" 98 | clean_git_repo 99 | 100 | # quit 101 | #----- 102 | remove_test_git_repo 103 | teardown_and_exit 104 | -------------------------------------------------------------------------------- /test/helpers/expect_helpers.exp: -------------------------------------------------------------------------------- 1 | # a set of expect helpers 2 | 3 | # basic setup for each script 4 | proc expect_setup {} { 5 | # disables script output 6 | log_user 0 7 | # standard timeout 8 | set timeout 5 9 | } 10 | 11 | proc exit_status_false {} { 12 | global exit_status 13 | set exit_status 1 14 | } 15 | 16 | proc sync_tmux {} { 17 | sleep 1.5 18 | } 19 | 20 | proc sync_irb {} { 21 | sleep 5.0 22 | } 23 | 24 | proc teardown_and_exit {} { 25 | global exit_status 26 | _kill_tmux_server 27 | exit $exit_status 28 | } 29 | 30 | proc create_output {} { 31 | # `yes` command just outputs `yes` 32 | send "yes\r" 33 | sleep 0.1 34 | # stop `yes` command 35 | send "" 36 | sync_tmux 37 | } 38 | 39 | proc clear_screen {} { 40 | send " " 41 | sync_tmux 42 | } 43 | 44 | proc display_text {text} { 45 | send "echo $text\r" 46 | sync_tmux 47 | } 48 | 49 | proc new_tmux_pane {} { 50 | sleep 0.3 51 | send "c" 52 | sleep 1.0 53 | } 54 | 55 | proc enter_irb {} { 56 | send "irb\r" 57 | sync_irb 58 | } 59 | 60 | proc exit_irb {} { 61 | send "\r" 62 | sync_irb 63 | send "exit\r" 64 | sync_tmux 65 | } 66 | 67 | proc irb_display_text {text} { 68 | send "puts '$text'\r" 69 | sync_irb 70 | } 71 | 72 | # Generates random output just to fill the screen. 73 | proc irb_generate_output {} { 74 | send "puts 'output\n' * 200\r" 75 | sync_irb 76 | } 77 | 78 | proc enter_test_git_repo {} { 79 | sync_tmux 80 | send "cd ~/tmux-example-plugin\r" 81 | sync_tmux 82 | send "git checkout --quiet tags/v0.0.1\r" 83 | sync_tmux 84 | } 85 | 86 | proc git_status {} { 87 | sync_tmux 88 | send "git status --short\r" 89 | sync_tmux 90 | } 91 | 92 | proc git_log_reverse_short {} { 93 | sync_tmux 94 | send "git --no-pager log --reverse --oneline -1\r" 95 | sync_tmux 96 | } 97 | 98 | proc git_log_reverse {} { 99 | sync_tmux 100 | send "git --no-pager log --reverse -1\r" 101 | sync_tmux 102 | } 103 | 104 | proc git_checkout {} { 105 | sync_tmux 106 | send "git checkout -- .\r" 107 | sync_tmux 108 | } 109 | 110 | proc git_clean_fd {} { 111 | sync_tmux 112 | send "git clean -f -d\r" 113 | sync_tmux 114 | } 115 | 116 | proc clean_git_repo {} { 117 | git_checkout 118 | git_clean_fd 119 | } 120 | 121 | proc change_file {file} { 122 | sync_tmux 123 | send "echo 'change' > $file\r" 124 | sync_tmux 125 | } 126 | 127 | proc create_new_file_in_repo {} { 128 | sync_tmux 129 | send "echo 'text' >> new_file.txt\r" 130 | sync_tmux 131 | } 132 | 133 | proc remove_test_git_repo {} { 134 | sync_tmux 135 | send "cd ~\r" 136 | sync_tmux 137 | send "rm -rf ~/tmux_example_plugin/\r" 138 | sync_tmux 139 | } 140 | 141 | # private functions 142 | 143 | proc _kill_tmux_server {} { 144 | send "" 145 | sync_tmux 146 | send "tmux kill-server\r" 147 | sync_tmux 148 | } 149 | 150 | -------------------------------------------------------------------------------- /copycat.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR/scripts/variables.sh" 6 | source "$CURRENT_DIR/scripts/helpers.sh" 7 | source "$CURRENT_DIR/scripts/stored_search_helpers.sh" 8 | 9 | # this function defines default stored searches 10 | set_default_stored_searches() { 11 | local file_search="$(get_tmux_option "$copycat_file_search_option" "$default_file_search_key")" 12 | local url_search="$(get_tmux_option "$copycat_url_search_option" "$default_url_search_key")" 13 | local digit_search="$(get_tmux_option "$copycat_digit_search_option" "$default_digit_search_key")" 14 | local hash_search="$(get_tmux_option "$copycat_hash_search_option" "$default_hash_search_key")" 15 | local ip_search="$(get_tmux_option "$copycat_ip_search_option" "$default_ip_search_key")" 16 | 17 | if stored_search_not_defined "$url_search"; then 18 | tmux set-option -g "${COPYCAT_VAR_PREFIX}_${url_search}" "(https?://|git@|git://|ssh://|ftp://|file:///)[[:alnum:]?=%/_.:,;~@!#$&()*+-]*" 19 | fi 20 | if stored_search_not_defined "$file_search"; then 21 | tmux set-option -g "${COPYCAT_VAR_PREFIX}_${file_search}" "(^|^\.|[[:space:]]|[[:space:]]\.|[[:space:]]\.\.|^\.\.)[[:alnum:]~_-]*/[][[:alnum:]_.#$%&+=/@-]*" 22 | fi 23 | if stored_search_not_defined "$digit_search"; then 24 | tmux set-option -g "${COPYCAT_VAR_PREFIX}_${digit_search}" "[[:digit:]]+" 25 | fi 26 | if stored_search_not_defined "$hash_search"; then 27 | tmux set-option -g "${COPYCAT_VAR_PREFIX}_${hash_search}" "\b([0-9a-f]{7,40}|[[:alnum:]]{52}|[0-9a-f]{64})\b" 28 | fi 29 | if stored_search_not_defined "$ip_search"; then 30 | tmux set-option -g "${COPYCAT_VAR_PREFIX}_${ip_search}" "[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}" 31 | fi 32 | } 33 | 34 | set_start_bindings() { 35 | set_default_stored_searches 36 | local stored_search_vars="$(stored_search_vars)" 37 | local search_var 38 | local key 39 | local pattern 40 | for search_var in $stored_search_vars; do 41 | key="$(get_stored_search_key "$search_var")" 42 | pattern="$(get_stored_search_pattern "$search_var")" 43 | tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_mode_start.sh '$pattern'" 44 | done 45 | } 46 | 47 | set_copycat_search_binding() { 48 | local key_bindings 49 | read -r -d '' -a key_bindings <<<"$(get_tmux_option "$copycat_search_option" "$default_copycat_search_key")" 50 | local key 51 | for key in "${key_bindings[@]}"; do 52 | tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_search.sh" 53 | done 54 | } 55 | 56 | set_copycat_git_special_binding() { 57 | local key_bindings 58 | read -r -d '' -a key_bindings <<<"$(get_tmux_option "$copycat_git_search_option" "$default_git_search_key")" 59 | local key 60 | for key in "${key_bindings[@]}"; do 61 | tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_git_special.sh #{pane_current_path}" 62 | done 63 | } 64 | 65 | set_copycat_mode_bindings() { 66 | "$CURRENT_DIR/scripts/copycat_mode_bindings.sh" 67 | } 68 | 69 | main() { 70 | set_start_bindings 71 | set_copycat_search_binding 72 | set_copycat_git_special_binding 73 | set_copycat_mode_bindings 74 | } 75 | main 76 | -------------------------------------------------------------------------------- /test/test_digit_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | # searches at the top of the pane 6 | #-------------------------------- 7 | display_text "123" 8 | tmux_ctrl_d 9 | assert_highlighted "123" "top of the pane" 10 | 11 | # middle of pane searches 12 | #------------------------ 13 | new_tmux_pane 14 | create_output 15 | clear_screen 16 | display_text "234" 17 | tmux_ctrl_d 18 | assert_highlighted "234" "middle of the pane, beginning of the line" 19 | 20 | display_text "some 345" 21 | tmux_ctrl_d 22 | assert_highlighted "345" "middle of the pane, not beginning of the line" 23 | 24 | create_output 25 | send "456" 26 | sync_tmux 27 | tmux_ctrl_d 28 | assert_highlighted "456" "middle of the pane, pane bottom" 29 | 30 | # match selection when line contains escaped chars 31 | #------------------------------------------------- 32 | new_tmux_pane 33 | display_text "filename=test.csv\r\nContent-Type: 567\r\n" 34 | tmux_ctrl_d 35 | assert_highlighted "567" "match selection when line contains escaped chars" 36 | 37 | # result navigation 38 | #------------------ 39 | new_tmux_pane 40 | display_text "678" 41 | display_text "789" 42 | display_text "890" 43 | tmux_ctrl_d 44 | # 890 45 | next_match 46 | # 890 47 | next_match 48 | # 789 49 | next_match 50 | # 789 51 | next_match 52 | # 678 53 | previous_match 54 | # 789 55 | assert_highlighted "789" "result navigation at the top of the pane" 56 | 57 | create_output 58 | display_text "012" 59 | display_text "123" 60 | tmux_ctrl_d 61 | # 123 62 | next_match 63 | # 123 64 | next_match 65 | # 012 66 | previous_match 67 | # 123 68 | assert_highlighted "123" "result navigation, middle of the pane" 69 | 70 | # 2 matches on the same line 71 | #--------------------------- 72 | new_tmux_pane 73 | display_text "234 345" 74 | tmux_ctrl_d 75 | assert_highlighted "234" "2 matches on the same line, first match" 76 | 77 | display_text "456 567" 78 | tmux_ctrl_d 79 | next_match 80 | assert_highlighted "567" "2 matches on the same line, second match" 81 | 82 | # no match, first and last match 83 | #------------------------------- 84 | new_tmux_pane 85 | tmux_ctrl_d 86 | assert_on_screen "No results!" "No results is displayed when no results" 87 | 88 | display_text "678" 89 | tmux_ctrl_d 90 | next_match 91 | next_match 92 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 93 | # exit copycat mode 94 | send "" 95 | 96 | new_tmux_pane 97 | display_text "789" 98 | tmux_ctrl_d 99 | next_match 100 | previous_match 101 | previous_match 102 | assert_on_screen "First match!" "'First match' is displayed when first match" 103 | # exit copycat mode 104 | send "" 105 | 106 | # irb console searches 107 | #--------------------- 108 | new_tmux_pane 109 | enter_irb 110 | irb_display_text "890" 111 | tmux_ctrl_d 112 | irb_assert_highlighted "890" "irb console, beggining of line" 113 | exit_irb 114 | 115 | enter_irb 116 | irb_display_text "901" 117 | tmux_ctrl_d 118 | irb_assert_highlighted "901" "irb console, not beggining of line" 119 | exit_irb 120 | 121 | enter_irb 122 | irb_generate_output 123 | send "puts 012" 124 | sync_tmux 125 | tmux_ctrl_d 126 | irb_assert_highlighted "012" "irb console, pane bottom, not beggining of line" 127 | exit_irb 128 | 129 | # quit 130 | #----- 131 | teardown_and_exit 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### master 4 | 5 | ### v3.0.0, Nov 01, 2017 6 | - if installed use `gawk` instead of `awk` (@metcalfc) 7 | - add stored search for matching git SHAs (@jbnicolai) 8 | - move documentation from github wiki to `docs/` folder 9 | - support for tmux 2.4+, not compatible with tmux 2.3 and earlier (@thalesmello) 10 | 11 | ### v2.1.0, Jan 01, 2015 12 | - combine send-keys calls to reduce flickering (@toupeira) 13 | - add `file:///` prefix for local file url's (@vellvisher) 14 | - add `git://` type url 15 | 16 | ### v2.0.0, Oct 16, 2014 17 | - add tmux options for default searches 18 | - use `session_id` instead of `session_name` in the copycat file name (solution 19 | provided by @toupeira) 20 | - use `grep -E` and remove eval (@toupeira) 21 | 22 | ### v1.1.0, Sep 29, 2014 23 | - add IP address search 24 | - enhance url search with git and ftp urls 25 | 26 | ### v1.0.0, Aug 31, 2014 27 | - simplify file search stored regex 28 | - match files starting with dot 29 | - improve README - add more relevant related plugins 30 | - do not use `copycat_clear_search` method when in copycat mode. It was causing 31 | mysterious issues for some users. 32 | - update `README.md` - warning about a breaking mapping 33 | - remove rails request stored search `C-r` 34 | 35 | ### v0.1.0, Aug 02, 2014 36 | - remove note about git history issue 37 | - url saved search includes `#` character 38 | - improve stored search handling 39 | - update README and document addding custom stored searches 40 | 41 | ### v0.0.7, Jul 31, 2014 42 | - add customization section to the readme (@soli) 43 | - remove screencast from the project. The video is too bit and plugin download 44 | is slow because of that. The video is moved to the separate `screencast` 45 | branch. 46 | - run test suite on 2 vagrant VMs: ubuntu and centos 47 | 48 | ### v0.0.6, Jul 28, 2014 49 | - update video script 50 | - update readme and invite for code contributions 51 | - update dockerfile with it's purpose 52 | - add test suite `README` file 53 | - add screencast original document to git 54 | - add video directory `README` file 55 | - update readme to reflect github organization change 56 | - add a screencast link to the readme 57 | 58 | ### v0.0.5, Jul 24, 2014 59 | - improve stored file matching search 60 | - fix wrong result highlighting for lines that have \r, \n chars 61 | - another improvement to file matching search: changed regex strategy to be 62 | "inclusive" 63 | - add test suite 64 | - update readme to show how test suite is started 65 | 66 | ### v0.0.4, Jul 9, 2014 67 | - bugfix for incorrect result highlighting 68 | - optimize and improve the function that centers the result vertically on the 69 | screen 70 | - fix OS X awk bug: awk variable content can't start with `=` char 71 | - fix a bug with wrong result highlighting caused by using `printf` 72 | - fix a bug with wrong result highlighting caused by a bug in OSX `grep` 73 | - improve URL matching regex. Matches don't include quotes anymore. 74 | 75 | ### v0.0.3, Jun 29, 2014 76 | - add notifications about the first and last match 77 | - improve "jump correction" handling by fetching the precise window height 78 | - improve result vertical centering & fix a related bug 79 | 80 | ### v0.0.2, Jun 26, 2014 81 | - search results are always at the bottom of the page. If possible center the 82 | result, or provide maximum possible padding. 83 | - refactoring in `copycat_jump.sh` - extract 2 constants to file global variables 84 | - improve file matching regex. `master...origin/master` is not detected as a 85 | string. 86 | 87 | ### v0.0.1, Jun 25, 2014 88 | - first version, plugin working 89 | -------------------------------------------------------------------------------- /test/test_user_defined_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup_with_custom_searches.exp" 4 | 5 | # searches at the top of the pane 6 | #-------------------------------- 7 | display_text "some text random string123 more text" 8 | tmux_ctrl_t 9 | assert_highlighted "random string123" "top of the pane, first string" 10 | 11 | new_tmux_pane 12 | display_text "some text random string123456 more text" 13 | tmux_ctrl_t 14 | assert_highlighted "random string123456" "top of the pane, second string" 15 | 16 | # middle of pane searches 17 | #------------------------ 18 | new_tmux_pane 19 | create_output 20 | clear_screen 21 | display_text "some text random string123 more text" 22 | tmux_ctrl_t 23 | assert_highlighted "random string123" "middle of the pane, first string" 24 | 25 | display_text "some text random string123456 more text" 26 | tmux_ctrl_t 27 | assert_highlighted "random string123456" "middle of the pane, second string" 28 | 29 | create_output 30 | sleep 0.2 31 | send " beginning random string001 text" 32 | sleep 0.2 33 | tmux_ctrl_t 34 | assert_highlighted "random string001" "middle of the pane, pane bottom" 35 | 36 | # match selection when line contains escaped chars 37 | #------------------------------------------------- 38 | new_tmux_pane 39 | display_text "filename=test.csv\r\nContent-Type: random string456\r\n" 40 | tmux_ctrl_t 41 | assert_highlighted "random string456" "match selection when line contains escaped chars" 42 | 43 | # result navigation 44 | #------------------ 45 | new_tmux_pane 46 | display_text "random string1" 47 | display_text "random string2" 48 | display_text "random string3" 49 | tmux_ctrl_t 50 | # random string3 51 | next_match 52 | # random string3 53 | next_match 54 | # random string2 55 | next_match 56 | # random string2 57 | next_match 58 | # random string1 59 | previous_match 60 | # random string2 61 | assert_highlighted "random string2" "result navigation at the top of the pane" 62 | 63 | create_output 64 | display_text "random string01" 65 | display_text "random string02" 66 | tmux_ctrl_t 67 | # random string02 68 | next_match 69 | # random string02 70 | next_match 71 | # random string01 72 | previous_match 73 | # random string02 74 | assert_highlighted "random string02" "result navigation, middle of the pane" 75 | 76 | # 2 matches on the same line 77 | #--------------------------- 78 | new_tmux_pane 79 | display_text "foo random string11 bar random string12 foobar" 80 | tmux_ctrl_t 81 | assert_highlighted "random string11" "2 matches on the same line, first match" 82 | 83 | display_text "foo random string11 bar random string12 foobar" 84 | tmux_ctrl_t 85 | next_match 86 | assert_highlighted "random string12" "2 matches on the same line, second match" 87 | 88 | # no match, first and last match 89 | #------------------------------- 90 | new_tmux_pane 91 | tmux_ctrl_t 92 | assert_on_screen "No results!" "No results is displayed when no results" 93 | 94 | display_text "foo random string21" 95 | tmux_ctrl_t 96 | next_match 97 | next_match 98 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 99 | # exit copycat mode 100 | send "" 101 | 102 | new_tmux_pane 103 | display_text "foo random string22" 104 | tmux_ctrl_t 105 | next_match 106 | previous_match 107 | previous_match 108 | assert_on_screen "First match!" "'First match' is displayed when first match" 109 | # exit copycat mode 110 | send "" 111 | 112 | # irb console searches 113 | #--------------------- 114 | new_tmux_pane 115 | enter_irb 116 | irb_display_text "foo random string31 bar" 117 | tmux_ctrl_t 118 | irb_assert_highlighted "random string31" "irb console, beginning of line" 119 | exit_irb 120 | 121 | enter_irb 122 | irb_display_text "foo random string32 bar" 123 | tmux_ctrl_t 124 | irb_assert_highlighted "random string32" "irb console, not beginning of line" 125 | exit_irb 126 | 127 | enter_irb 128 | irb_generate_output 129 | send "puts random string33" 130 | sleep 5 131 | tmux_ctrl_t 132 | irb_assert_highlighted "random string33" "irb console, pane bottom, not beginning of line" 133 | exit_irb 134 | 135 | # quit 136 | #----- 137 | teardown_and_exit 138 | -------------------------------------------------------------------------------- /test/test_file_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | # searches at the top of the pane 6 | #-------------------------------- 7 | display_text "/top/of_the/pane/file.txt" 8 | sleep 0.5 9 | tmux_ctrl_f 10 | assert_highlighted "/top/of_the/pane/file.txt" "top of the pane absolute path" 11 | 12 | new_tmux_pane 13 | display_text "another/top/of_the/pane/file.txt" 14 | tmux_ctrl_f 15 | assert_highlighted "another/top/of_the/pane/file.txt" "top of the pane relative path" 16 | 17 | # middle of pane searches 18 | #------------------------ 19 | new_tmux_pane 20 | create_output 21 | clear_screen 22 | display_text "/this/is/some/file.txt" 23 | tmux_ctrl_f 24 | assert_highlighted "/this/is/some/file.txt" "middle of the pane, absolute path, beginning of the line" 25 | 26 | display_text "random /this/is/some/file.txt" 27 | tmux_ctrl_f 28 | assert_highlighted " /this/is/some/file.txt" "middle of the pane, absolute path, not beginning of the line" 29 | 30 | display_text "another/file.txt" 31 | tmux_ctrl_f 32 | assert_highlighted "another/file.txt" "middle of the pane, relative path, beginning of the line" 33 | 34 | display_text "some text another/file.txt" 35 | tmux_ctrl_f 36 | assert_highlighted " another/file.txt" "middle of the pane, relative path, not beginning of the line" 37 | 38 | create_output 39 | sleep 0.2 40 | send " some/file.xyz " 41 | sleep 0.2 42 | tmux_ctrl_f 43 | assert_highlighted " some/file.xyz" "middle of the pane relative path, pane bottom" 44 | 45 | # match selection when line contains escaped chars 46 | #------------------------------------------------- 47 | new_tmux_pane 48 | display_text "filename=test.csv\r\nContent-Type: text/csv\r\n" 49 | tmux_ctrl_f 50 | assert_highlighted " text/csv" "match selection when line contains escaped chars" 51 | 52 | # result navigation 53 | #------------------ 54 | new_tmux_pane 55 | display_text "/file/1.txt" 56 | display_text "/file/2.txt" 57 | display_text "/file/3.txt" 58 | tmux_ctrl_f 59 | # /file/3.txt 60 | next_match 61 | # /file/3.txt 62 | next_match 63 | # /file/2.txt 64 | next_match 65 | # /file/2.txt 66 | next_match 67 | # /file/1.txt 68 | previous_match 69 | # /file/2.txt 70 | assert_highlighted " /file/2.txt" "result navigation at the top of the pane" 71 | 72 | create_output 73 | display_text "/file/1.txt" 74 | display_text "/file/2.txt" 75 | tmux_ctrl_f 76 | # /file/2.txt 77 | next_match 78 | # /file/2.txt 79 | next_match 80 | # /file/1.txt 81 | previous_match 82 | # /file/2.txt 83 | assert_highlighted " /file/2.txt" "result navigation, middle of the pane" 84 | 85 | # 2 matches on the same line 86 | #--------------------------- 87 | new_tmux_pane 88 | display_text "/file/1.txt another/file/2.txt" 89 | tmux_ctrl_f 90 | assert_highlighted "/file/1.txt" "2 matches on the same line, first match" 91 | 92 | display_text "/file/1.txt another/file/2.txt" 93 | tmux_ctrl_f 94 | next_match 95 | assert_highlighted " another/file/2.txt" "2 matches on the same line, second match" 96 | 97 | # no match, first and last match 98 | #------------------------------- 99 | new_tmux_pane 100 | tmux_ctrl_f 101 | assert_on_screen "No results!" "No results is displayed when no results" 102 | 103 | display_text "last/match/file.txt" 104 | tmux_ctrl_f 105 | next_match 106 | next_match 107 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 108 | # exit copycat mode 109 | send "" 110 | 111 | new_tmux_pane 112 | display_text "first/match/file.txt" 113 | tmux_ctrl_f 114 | next_match 115 | previous_match 116 | previous_match 117 | assert_on_screen "First match!" "'First match' is displayed when first match" 118 | # exit copycat mode 119 | send "" 120 | 121 | # irb console searches 122 | #--------------------- 123 | new_tmux_pane 124 | enter_irb 125 | irb_display_text "file/within/irb.rb" 126 | tmux_ctrl_f 127 | irb_assert_highlighted "file/within/irb.rb" "irb console relative path, beggining of line" 128 | exit_irb 129 | 130 | enter_irb 131 | irb_display_text "some text file/within/irb.rb" 132 | tmux_ctrl_f 133 | irb_assert_highlighted " file/within/irb.rb" "irb console relative path, not beggining of line" 134 | exit_irb 135 | 136 | enter_irb 137 | irb_generate_output 138 | send "puts /absolute/file/irb.rb" 139 | sleep 5 140 | tmux_ctrl_f 141 | irb_assert_highlighted " /absolute/file/irb.rb" "irb console absolute path, pane bottom, not beggining of line" 142 | exit_irb 143 | 144 | # quit 145 | #----- 146 | teardown_and_exit 147 | -------------------------------------------------------------------------------- /scripts/helpers.sh: -------------------------------------------------------------------------------- 1 | # config options 2 | 3 | default_next_key="n" 4 | tmux_option_next="@copycat_next" 5 | 6 | default_prev_key="N" 7 | tmux_option_prev="@copycat_prev" 8 | 9 | # keeps track of number of panes in copycat mode 10 | tmux_option_counter="@copycat_counter" 11 | 12 | # === awk vs gawk === 13 | command_exists() { 14 | command -v "$@" > /dev/null 2>&1 15 | } 16 | AWK_CMD='awk' 17 | if command_exists gawk; then 18 | AWK_CMD='gawk' 19 | fi 20 | 21 | # === general helpers === 22 | 23 | get_tmux_option() { 24 | local option=$1 25 | local default_value=$2 26 | local option_value=$(tmux show-option -gqv "$option") 27 | if [ -z "$option_value" ]; then 28 | echo "$default_value" 29 | else 30 | echo "$option_value" 31 | fi 32 | } 33 | 34 | set_tmux_option() { 35 | local option=$1 36 | local value=$2 37 | tmux set-option -gq "$option" "$value" 38 | } 39 | 40 | tmux_copy_mode() { 41 | tmux show-option -gwv mode-keys 42 | } 43 | 44 | tmux_copy_mode_string() { 45 | if [ $(tmux_copy_mode) == 'vi' ]; then 46 | echo copy-mode-vi 47 | else 48 | echo copy-mode 49 | fi 50 | } 51 | 52 | # === copycat mode specific helpers === 53 | 54 | set_copycat_mode() { 55 | set_tmux_option "$(_copycat_mode_var)" "true" 56 | } 57 | 58 | unset_copycat_mode() { 59 | set_tmux_option "$(_copycat_mode_var)" "false" 60 | } 61 | 62 | in_copycat_mode() { 63 | local copycat_mode=$(get_tmux_option "$(_copycat_mode_var)" "false") 64 | [ "$copycat_mode" == "true" ] 65 | } 66 | 67 | not_in_copycat_mode() { 68 | if in_copycat_mode; then 69 | return 1 70 | else 71 | return 0 72 | fi 73 | } 74 | 75 | # === copycat mode position === 76 | 77 | get_copycat_position() { 78 | local copycat_position_variable=$(_copycat_position_var) 79 | echo $(get_tmux_option "$copycat_position_variable" "0") 80 | } 81 | 82 | set_copycat_position() { 83 | local position="$1" 84 | local copycat_position_variable=$(_copycat_position_var) 85 | set_tmux_option "$copycat_position_variable" "$position" 86 | } 87 | 88 | reset_copycat_position() { 89 | set_copycat_position "0" 90 | } 91 | 92 | # === scrollback and results position === 93 | 94 | get_scrollback_filename() { 95 | echo "$(_get_tmp_dir)/scrollback-$(_pane_unique_id)" 96 | } 97 | 98 | get_copycat_filename() { 99 | echo "$(_get_tmp_dir)/results-$(_pane_unique_id)" 100 | } 101 | 102 | # Ensures a message is displayed for 5 seconds in tmux prompt. 103 | # Does not override the 'display-time' tmux option. 104 | display_message() { 105 | local message="$1" 106 | 107 | # display_duration defaults to 5 seconds, if not passed as an argument 108 | if [ "$#" -eq 2 ]; then 109 | local display_duration="$2" 110 | else 111 | local display_duration="5000" 112 | fi 113 | 114 | # saves user-set 'display-time' option 115 | local saved_display_time=$(get_tmux_option "display-time" "750") 116 | 117 | # sets message display time to 5 seconds 118 | tmux set-option -gq display-time "$display_duration" 119 | 120 | # displays message 121 | tmux display-message "$message" 122 | 123 | # restores original 'display-time' value 124 | tmux set-option -gq display-time "$saved_display_time" 125 | } 126 | 127 | # === counter functions === 128 | 129 | copycat_increase_counter() { 130 | local count=$(get_tmux_option "$tmux_option_counter" "0") 131 | local new_count="$((count + 1))" 132 | set_tmux_option "$tmux_option_counter" "$new_count" 133 | } 134 | 135 | copycat_decrease_counter() { 136 | local count="$(get_tmux_option "$tmux_option_counter" "0")" 137 | if [ "$count" -gt "0" ]; then 138 | # decreasing the counter only if it won't go below 0 139 | local new_count="$((count - 1))" 140 | set_tmux_option "$tmux_option_counter" "$new_count" 141 | fi 142 | } 143 | 144 | copycat_counter_zero() { 145 | local count="$(get_tmux_option "$tmux_option_counter" "0")" 146 | [ "$count" -eq "0" ] 147 | } 148 | 149 | # === key binding functions === 150 | 151 | copycat_next_key() { 152 | echo "$(get_tmux_option "$tmux_option_next" "$default_next_key")" 153 | } 154 | 155 | copycat_prev_key() { 156 | echo "$(get_tmux_option "$tmux_option_prev" "$default_prev_key")" 157 | } 158 | 159 | # function expected output: 'C-c Enter q' 160 | copycat_quit_copy_mode_keys() { 161 | local commands_that_quit_copy_mode="cancel" 162 | local copy_mode="$(tmux_copy_mode_string)" 163 | tmux list-keys -T "$copy_mode" | 164 | \grep "$commands_that_quit_copy_mode" | 165 | $AWK_CMD '{ print $4 }' | 166 | sort -u | 167 | sed 's/C-j//g' | 168 | xargs echo 169 | } 170 | 171 | # === 'private' functions === 172 | 173 | _copycat_mode_var() { 174 | local pane_id="$(_pane_unique_id)" 175 | echo "@copycat_mode_$pane_id" 176 | } 177 | 178 | _copycat_position_var() { 179 | local pane_id="$(_pane_unique_id)" 180 | echo "@copycat_position_$pane_id" 181 | } 182 | 183 | _get_tmp_dir() { 184 | echo "${TMPDIR:-/tmp}/tmux-$EUID-copycat" 185 | } 186 | 187 | # returns a string unique to current pane 188 | # sed removes `$` sign because `session_id` contains is 189 | _pane_unique_id() { 190 | tmux display-message -p "#{session_id}-#{window_index}-#{pane_index}" | 191 | sed 's/\$//' 192 | } 193 | -------------------------------------------------------------------------------- /test/test_free_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | # searches at the top of the pane 6 | #-------------------------------- 7 | new_tmux_pane 8 | display_text "something search me1" 9 | search "search me1" 10 | assert_highlighted "search me1" "top of the pane, literal search" 11 | 12 | new_tmux_pane 13 | display_text "something search me2" 14 | search "search me\[\[:alnum:]]" 15 | assert_highlighted "search me2" "top of the pane, search with \[\[:alnum:]]" 16 | 17 | new_tmux_pane 18 | display_text "something search me3" 19 | search "searc. me\[\[:digit:]]" 20 | assert_highlighted "search me3" "top of the pane, search with dot and \[\[:digit:]]" 21 | 22 | new_tmux_pane 23 | display_text "something search me4" 24 | search "sear\[^\[:space:]]* me.+$" 25 | assert_highlighted "search me4" "top of the pane, search with matching group, dot-plus and end of line $" 26 | 27 | # middle of pane searches 28 | #------------------------ 29 | new_tmux_pane 30 | create_output 31 | clear_screen 32 | display_text "something search me1" 33 | search "search me1" 34 | assert_highlighted "search me1" "middle of the pane, literal search" 35 | 36 | display_text "something search me2" 37 | search "search me\[\[:alnum:]]" 38 | assert_highlighted "search me2" "middle of the pane, search with \[\[:alnum:]]" 39 | 40 | display_text "something search me3" 41 | search "searc. me\[\[:digit:]]" 42 | assert_highlighted "search me3" "middle of the pane, search with dot and \[\[:digit:]]" 43 | 44 | display_text "something search me4" 45 | search "sear\[^\[:space:]]* me.+$" 46 | assert_highlighted "search me4" "middle of the pane, search with matching group, dot-plus and end of line $" 47 | 48 | # bottom of the buffer searches 49 | #------------------------------ 50 | new_tmux_pane 51 | create_output 52 | sleep 0.2 53 | send "something search me1" 54 | sleep 0.2 55 | search "search me1" 56 | assert_highlighted "search me1" "bottom of the pane, literal search" 57 | 58 | sleep 0.2 59 | send "something search me2" 60 | sleep 0.2 61 | search "search me\[\[:alnum:]]" 62 | assert_highlighted "search me2" "bottom of the pane, search with \[\[:alnum:]]" 63 | 64 | sleep 0.2 65 | send "something search me3" 66 | sleep 0.2 67 | search "searc. me\[\[:digit:]]" 68 | assert_highlighted "search me3" "bottom of the pane, search with dot and \[\[:digit:]]" 69 | 70 | sleep 0.2 71 | send "something search me4" 72 | sleep 0.2 73 | search "sear\[^\[:space:]]* me.+$" 74 | assert_highlighted "search me4" "bottom of the pane, search with matching group, dot-plus and end of line $" 75 | 76 | # match selection when line contains escaped chars 77 | #------------------------------------------------- 78 | new_tmux_pane 79 | display_text "filename=test.csv\r\nContent-Type: text/csv\r\n" 80 | search "text/csv" 81 | assert_highlighted "text/csv" "match selection when line contains escaped chars" 82 | 83 | # result navigation 84 | #------------------ 85 | new_tmux_pane 86 | display_text "match no1" 87 | display_text "match no2" 88 | display_text "match no3" 89 | search "match\[\[:space:]]no\[\[:digit:]]" 90 | # match no3 91 | next_match 92 | # match no3 93 | next_match 94 | # match no2 95 | next_match 96 | # match no2 97 | next_match 98 | # match no1 99 | previous_match 100 | # match no2 101 | assert_highlighted "match no2" "result navigation at the top of the pane" 102 | 103 | create_output 104 | display_text "match no1" 105 | display_text "match no2" 106 | search "match\[\[:space:]]no\[\[:digit:]]" 107 | # match no2 108 | next_match 109 | # match no2 110 | next_match 111 | # match no1 112 | previous_match 113 | # match no2 114 | assert_highlighted "match no2" "result navigation, middle of the pane" 115 | 116 | # 2 matches on the same line 117 | #--------------------------- 118 | new_tmux_pane 119 | display_text "match1 match2" 120 | search "match\[\[:digit:]]" 121 | assert_highlighted "match1" "2 matches on the same line, first match" 122 | 123 | display_text "match1 match2" 124 | search "match\[\[:digit:]]" 125 | next_match 126 | assert_highlighted "match2" "2 matches on the same line, second match" 127 | 128 | # no match, first and last match 129 | #------------------------------- 130 | new_tmux_pane 131 | search "something\[\[:digit:]]" 132 | assert_on_screen "No results!" "No results is displayed when no results" 133 | 134 | new_tmux_pane 135 | display_text "something2" 136 | sleep 5.0 137 | search "something\[\[:digit:]]" 138 | next_match 139 | next_match 140 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 141 | # exit copycat mode 142 | send "" 143 | 144 | new_tmux_pane 145 | display_text "random3" 146 | search "random\[\[:digit:]]" 147 | previous_match 148 | assert_on_screen "First match!" "'First match' is displayed when first match" 149 | # exit copycat mode 150 | send "" 151 | 152 | # irb console searches 153 | #--------------------- 154 | new_tmux_pane 155 | enter_irb 156 | irb_display_text "within irb1" 157 | search "within\[\[:space:]]irb\[\[:digit:]]" 158 | irb_assert_highlighted "within irb1" "irb console" 159 | exit_irb 160 | 161 | # quit 162 | #----- 163 | teardown_and_exit 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tmux copycat 2 | 3 | [![Build Status](https://travis-ci.org/tmux-plugins/tmux-copycat.svg?branch=master)](https://travis-ci.org/tmux-plugins/tmux-copycat) 4 | 5 | **NOTE: [tmux 3.1 adds support for native regex searches](https://raw.githubusercontent.com/tmux/tmux/3.1/CHANGES). 6 | This is great news because it means a big part of 'tmux-copycat' is now 7 | available natively! Use this plugin only if you need its other features.** 8 | 9 | This plugin enables: 10 | 11 | - regex searches (native support added in tmux 3.1) 12 | - search result highlighting 13 | - predefined searches 14 | 15 | Predefined searches are plugin killer feature. It speeds the workflow and 16 | reduces mouse usage with Tmux. 17 | 18 | It works even better when paired with 19 | [tmux yank](https://github.com/tmux-plugins/tmux-yank). Tested and working on 20 | Linux, OSX and Cygwin. 21 | 22 | ### Screencast 23 | 24 | [![screencast screenshot](/video/screencast_img.png)](https://vimeo.com/101867689) 25 | 26 | #### Search 27 | 28 | - `prefix + /` - regex search (strings work too) 29 | 30 | Example search entries: 31 | 32 | - `foo` - searches for string `foo` 33 | - `[0-9]+` - regex search for numbers 34 | 35 | Grep is used for searching.
36 | Searches are case insensitive.
37 | 38 | #### Predefined searches 39 | 40 | - `prefix + ctrl-f` - simple *f*ile search 41 | - `prefix + ctrl-g` - jumping over *g*it status files (best used after `git status` command) 42 | - `prefix + alt-h` - jumping over SHA-1/SHA-256 hashes (best used after `git log` command) 43 | - `prefix + ctrl-u` - *u*rl search (http, ftp and git urls) 44 | - `prefix + ctrl-d` - number search (mnemonic d, as digit) 45 | - `prefix + alt-i` - *i*p address search 46 | 47 | These start "copycat mode" and jump to first match. 48 | 49 | #### "Copycat mode" bindings 50 | 51 | These are enabled when you search with copycat: 52 | 53 | - `n` - jumps to the next match 54 | - `N` - jumps to the previous match 55 | 56 | To copy a highlighted match: 57 | 58 | - `Enter` - if you're using Tmux `vi` mode 59 | - `ctrl-w` or `alt-w` - if you're using Tmux `emacs` mode 60 | 61 | Copying a highlighted match will take you "out" of copycat mode. Paste with 62 | `prefix + ]` (this is Tmux default paste). 63 | 64 | Copying highlighted matches can be enhanced with 65 | [tmux yank](https://github.com/tmux-plugins/tmux-yank). 66 | 67 | ### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) 68 | 69 | Add plugin to the list of TPM plugins in `.tmux.conf`: 70 | 71 | set -g @plugin 'tmux-plugins/tmux-copycat' 72 | 73 | Hit `prefix + I` to fetch the plugin and source it. You should now be able to 74 | use the plugin. 75 | 76 | Optional (but recommended) install `gawk` via your package manager of choice 77 | for better UTF-8 character support. 78 | 79 | ### Manual Installation 80 | 81 | Clone the repo: 82 | 83 | $ git clone https://github.com/tmux-plugins/tmux-copycat ~/clone/path 84 | 85 | Add this line to the bottom of `.tmux.conf`: 86 | 87 | run-shell ~/clone/path/copycat.tmux 88 | 89 | Reload TMUX environment with: `$ tmux source-file ~/.tmux.conf`. You should now 90 | be able to use the plugin. 91 | 92 | Optional (but recommended) install `gawk` via your package manager of choice 93 | for better UTF-8 character support. 94 | 95 | ### Installation for Tmux 2.3 and earlier 96 | 97 | Due to the changes in tmux, the latest version of this plugin doesn't support 98 | tmux 2.3 and earlier. It is recommended you upgrade to tmux version 2.4 or 99 | later. If you must continue using older version, please follow 100 | [these steps for installation](docs/installation_for_tmux_2.3.md). 101 | 102 | ### Limitations 103 | 104 | This plugin has some known limitations. Please read about it 105 | [here](docs/limitations.md). 106 | 107 | ### Docs 108 | 109 | - Most of the behavior of tmux-copycat can be customized via tmux options. 110 | [Check out the full options list](docs/customizations.md). 111 | - To speed up the workflow you can define new bindings in `.tmux.conf` for 112 | searches you use often, more info [here](docs/defining_new_stored_searches.md) 113 | 114 | ### Other goodies 115 | 116 | `tmux-copycat` works great with: 117 | 118 | - [tmux-yank](https://github.com/tmux-plugins/tmux-yank) - enables copying 119 | highlighted text to system clipboard 120 | - [tmux-open](https://github.com/tmux-plugins/tmux-open) - a plugin for quickly 121 | opening a highlighted file or a url 122 | - [tmux-continuum](https://github.com/tmux-plugins/tmux-continuum) - automatic 123 | restoring and continuous saving of tmux env 124 | 125 | ### Test suite 126 | 127 | This plugin has a pretty extensive integration test suite that runs on 128 | [travis](https://travis-ci.org/tmux-plugins/tmux-copycat). 129 | 130 | When run locally, it depends on `vagrant`. Run it with: 131 | 132 | # within project top directory 133 | $ ./run-tests 134 | 135 | ### Contributions and new features 136 | 137 | Bug fixes and contributions are welcome. 138 | 139 | Feel free to suggest new features, via github issues. 140 | 141 | If you have a bigger idea you'd like to work on, please get in touch, also via 142 | github issues. 143 | 144 | ### License 145 | 146 | [MIT](LICENSE.md) 147 | -------------------------------------------------------------------------------- /test/test_url_search.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env expect 2 | 3 | source "./test/helpers/setup.exp" 4 | 5 | # searches at the top of the pane 6 | #-------------------------------- 7 | display_text "http://example1.com" 8 | tmux_ctrl_u 9 | assert_highlighted "http://example1.com" "top of the pane, http" 10 | 11 | new_tmux_pane 12 | display_text "https://example1.com" 13 | tmux_ctrl_u 14 | assert_highlighted "https://example1.com" "top of the pane, https" 15 | 16 | # middle of pane searches 17 | #------------------------ 18 | new_tmux_pane 19 | create_output 20 | clear_screen 21 | display_text "http://example2.com" 22 | tmux_ctrl_u 23 | assert_highlighted "http://example2.com" "middle of the pane, http, beginning of the line" 24 | 25 | display_text "random http://example3.com" 26 | tmux_ctrl_u 27 | assert_highlighted "http://example3.com" "middle of the pane, http, not beginning of the line" 28 | 29 | display_text "https://example4.com" 30 | tmux_ctrl_u 31 | assert_highlighted "https://example4.com" "middle of the pane, https, beginning of the line" 32 | 33 | display_text "some text https://example5.com" 34 | tmux_ctrl_u 35 | assert_highlighted "https://example5.com" "middle of the pane, https, not beginning of the line" 36 | 37 | create_output 38 | sleep 0.2 39 | send " http://example6.com " 40 | sleep 0.2 41 | tmux_ctrl_u 42 | assert_highlighted "http://example6.com" "middle of the pane, http, pane bottom" 43 | 44 | # other url scheme searches 45 | #-------------------------- 46 | new_tmux_pane 47 | create_output 48 | clear_screen 49 | display_text "git@github.com:rails/rails.git" 50 | tmux_ctrl_u 51 | assert_highlighted "git@github.com:rails/rails.git" "middle of the pane, git url" 52 | 53 | display_text "git://github.com/rails/rails.git" 54 | tmux_ctrl_u 55 | assert_highlighted "git://github.com/rails/rails.git" "middle of the pane, another git url" 56 | 57 | display_text "ftp://ftp.foo.bar/baz/lorem/IPSUM/file.txt" 58 | tmux_ctrl_u 59 | assert_highlighted "ftp://ftp.foo.bar/baz/lorem/IPSUM/file.txt" "middle of the pane, ftp url" 60 | 61 | display_text "file:///foo/bar/file.txt" 62 | tmux_ctrl_u 63 | assert_highlighted "file:///foo/bar/file.txt" "middle of the pane, file url" 64 | 65 | # urls with parameters 66 | #--------------------- 67 | new_tmux_pane 68 | display_text "http://example61.com?some=params" 69 | tmux_ctrl_u 70 | assert_highlighted "http://example61.com?some=params" "http, simple params" 71 | 72 | # long links break tests, that's why the below one is shortened 73 | new_tmux_pane 74 | display_text "'https://github.com/H/h/b/g.rb'" 75 | tmux_ctrl_u 76 | assert_highlighted "https://github.com/H/h/b/g.rb" "github link" 77 | 78 | # match selection when line contains escaped chars 79 | #------------------------------------------------- 80 | new_tmux_pane 81 | display_text "filename=test.csv\r\nContent-Type: http://example7.com\r\n" 82 | tmux_ctrl_u 83 | assert_highlighted "http://example7.com" "match selection when line contains escaped chars" 84 | 85 | # result navigation 86 | #------------------ 87 | new_tmux_pane 88 | display_text "http://example81.com" 89 | display_text "http://example82.com" 90 | display_text "http://example83.com" 91 | tmux_ctrl_u 92 | # http://example83.com 93 | next_match 94 | # http://example83.com 95 | next_match 96 | # http://example82.com 97 | next_match 98 | # http://example82.com 99 | next_match 100 | # http://example81.com 101 | previous_match 102 | # http://example82.com 103 | assert_highlighted "http://example82.com" "result navigation at the top of the pane" 104 | 105 | create_output 106 | display_text "http://example91.com" 107 | display_text "http://example92.com" 108 | tmux_ctrl_u 109 | # http://example92.com 110 | next_match 111 | # http://example92.com 112 | next_match 113 | # http://example91.com 114 | previous_match 115 | # http://example92.com 116 | assert_highlighted "http://example92.com" "result navigation, middle of the pane" 117 | 118 | # 2 matches on the same line 119 | #--------------------------- 120 | new_tmux_pane 121 | display_text "http://example101.com http://example102.com" 122 | tmux_ctrl_u 123 | assert_highlighted "http://example101.com" "2 matches on the same line, first match" 124 | 125 | display_text "http://example111.com http://example112.com" 126 | tmux_ctrl_u 127 | next_match 128 | assert_highlighted "http://example112.com" "2 matches on the same line, second match" 129 | 130 | # works ok even with unicode characters in the line (requires gawk to be installed) 131 | #-------------------------------------------------- 132 | new_tmux_pane 133 | display_text "Ξ ~CM_CONF_DIR → curl http://www.google.com" 134 | tmux_ctrl_u 135 | assert_highlighted "http://www.google.com" "match on the line with unicode characters" 136 | 137 | display_text "↑127 ~CM_CONF_DIR → echo http://www.google.com" 138 | tmux_ctrl_u 139 | assert_highlighted "http://www.google.com" "another match on the line with unicode characters" 140 | 141 | # no match, first and last match 142 | #------------------------------- 143 | new_tmux_pane 144 | tmux_ctrl_u 145 | assert_on_screen "No results!" "No results is displayed when no results" 146 | 147 | display_text "http://example12.com" 148 | tmux_ctrl_u 149 | next_match 150 | next_match 151 | assert_on_screen "Last match!" "'Last match' is displayed when last match" 152 | # exit copycat mode 153 | send "" 154 | 155 | new_tmux_pane 156 | display_text "http://example13.com" 157 | tmux_ctrl_u 158 | next_match 159 | previous_match 160 | previous_match 161 | assert_on_screen "First match!" "'First match' is displayed when first match" 162 | # exit copycat mode 163 | send "" 164 | 165 | # irb console searches 166 | #--------------------- 167 | new_tmux_pane 168 | enter_irb 169 | irb_display_text "http://example14.com" 170 | tmux_ctrl_u 171 | irb_assert_highlighted "http://example14.com" "irb console, beginning of line" 172 | exit_irb 173 | 174 | enter_irb 175 | irb_display_text "some text http://example15.com" 176 | tmux_ctrl_u 177 | irb_assert_highlighted "http://example15.com" "irb console, not beginning of line" 178 | exit_irb 179 | 180 | enter_irb 181 | irb_generate_output 182 | send "puts http://example16.com" 183 | sleep 5 184 | tmux_ctrl_u 185 | irb_assert_highlighted "http://example16.com" "irb console, pane bottom, not beginning of line" 186 | exit_irb 187 | 188 | # quit 189 | #----- 190 | teardown_and_exit 191 | -------------------------------------------------------------------------------- /video/script.md: -------------------------------------------------------------------------------- 1 | # Video script 2 | 3 | 1 - Intro: about selecting in copy mode 4 | ======================================= 5 | Actions 6 | ------- 7 | - select a file with the mouse 8 | - paste it in the command line 9 | 10 | Script 11 | ------ 12 | Let's demo Tmux copycat plugin. 13 | 14 | Tmux copycat enables you to perform regex searches and also to store those 15 | searches for fast execution later. 16 | 17 | This is something not possible with vanilla tmux and it can greatly reduce mouse 18 | usage and speed up your workflow. 19 | 20 | Let's jump to an example. We have tmux here, and I'll create some typical output 21 | in the terminal. 22 | 23 | Now, I want to grab that last file you see listed. 24 | The easiest and fastest way to do it is unfortunately with a mouse. 25 | I'll select the file. 26 | Copy and paste it. 27 | 28 | That, however feels wrong. I shouldn't be using mouse while in the command 29 | line. There has to be a way to automate and speed things up with tmux. 30 | 31 | 2 - Selecting files `prefix + ` 32 | ==================================== 33 | Actions 34 | ------- 35 | - select a file with `prefix + ` 36 | - copy the file with `Enter` 37 | - paste with `prefix + ]` 38 | 39 | Script 40 | ------ 41 | Now, let's show a proper way to do it. 42 | 43 | Tmux copycat has a predefined file search, so pressing prefix plus control-f 44 | jumps straight to the last file. 45 | 46 | Notice how the match is already selected. 47 | 48 | I'll copy it with enter, 49 | and paste it with tmux default paste "bajnding": prefix plus right angle bracket. 50 | 51 | 3 - Jumping over searches with `n` and `N` 52 | ========================================== 53 | Actions 54 | ------- 55 | - select a file with `prefix + ` 56 | - another selection with `n` 57 | - another selection with `n` 58 | - previous match with `N` 59 | 60 | Script 61 | ------ 62 | You can also easily jump over all the matches in the pane scrollback. 63 | 64 | I'll enter file search with prefix control-f again. 65 | 66 | I can move to the next match with n. 67 | 68 | And to the previous match with uppercase n. 69 | 70 | This jumping over the results is possible for any tmux copycat search. 71 | 72 | 4 - Selecting git status files `prefix + ` 73 | =============================================== 74 | Actions 75 | ------- 76 | - invoke `git status` 77 | - the output should have: one word file name, and filename with spaces 78 | - jump over all the results with `prefix + `, `n` and `N` 79 | - make sure it's shown that simple files can't be selected 80 | - invoke `prefix + ` 81 | - go up and down to show all the files can be selected 82 | 83 | Script 84 | ------ 85 | File search can be really useful for selecting `git status` files. I'll invoke 86 | `git status` to get the output. 87 | 88 | But as you can see, file search has it's limitations. 89 | In the example on the screen, file search didn't select `files.txt` and 90 | `file with spaces.txt`. It just skiped those and selected the next file. 91 | 92 | Why? Well, file search does not detect simple file names. A string has to have a 93 | forward slash in it to be detected as a file. 94 | 95 | To solve this, there is a git special "bajnding": prefix + control g. I'll 96 | invoke it. 97 | 98 | I can now smoothly jump over all the git status files, including files with 99 | spaces and single word files. 100 | 101 | 5 - Selecting numbers `prefix + ` 102 | ====================================== 103 | Actions 104 | ------- 105 | - create commit 106 | - create pull request 107 | - start assigning the pull request to me 108 | - fetch a pull request number 109 | - assign a pull request to me 110 | 111 | Script 112 | ------ 113 | To show another example I'll create a somewhat realistic scenario. 114 | I'll git add a file, make a commit and push it to a remote repo. 115 | 116 | Next, I'll use a git alias for a program called `hub` to open a pull request 117 | from the command line. 118 | 119 | 120 | Good, here's the pull request url. 121 | 122 | Now, I need to assign that pull request with the program `ghi`. For that I need 123 | a pull request number. 124 | This is another situation where I'd just use the mouse to select that number. 125 | 126 | But since this is copycat demo, let's use prefix plus control d. 127 | 128 | It searches for digits or numbers. 129 | Copy, paste and done without reaching for the mouse. 130 | 131 | 6 - Selecting URLs `prefix + ` 132 | =================================== 133 | Actions 134 | ------- 135 | - select last, pull request url with `prefix + C-u` 136 | - press n, n, then N, N 137 | - yank it with `y` 138 | 139 | Script 140 | ------ 141 | How about checking that pull request on github now? I need to grab a url to do that. 142 | 143 | You might've guessed it: there's a stored search for url's. Invoke it with 144 | prefix plus C-u and the url is selected. 145 | 146 | 7 - Plain old search with `prefix + /` 147 | ===================================== 148 | Actions 149 | ------- 150 | - clear screen 151 | - enter: echo 'search me123' 152 | - then: echo 'search me2345' 153 | - enter search command `prefix + /` 154 | - search for a regex `search me[[:digit:]]\\+` 155 | - scroll accross the results 156 | - search for a regex `search me\\d\\+` 157 | - scroll accress the results 158 | 159 | Script 160 | ------ 161 | Now, I want to show you how to perform a free search using regex. 162 | In fact plain regex search is the base for all other so called "saved searches" 163 | shown in the video so far. 164 | 165 | I'll write a couple lines in the terminal to get some output. 166 | 167 | 168 | Let's say we need to match 'search me' string and all the digits that come 169 | after it. That can't be done using the tmux vanilla search, because it can do only 170 | literal searches. 171 | 172 | I'll invoke copycat regex search by pressing `prefix` + slash. 173 | 174 | I get a prompt at the bottom of the screen where I can enter the search term or a regex. 175 | I'll type 'search me', then a posix matching group for digits. 176 | 177 | Digit can be repeated one or more times and repeating is specified by the 178 | trailing plus. 179 | Note, plus has to be escaped to have special meaning. 180 | In copycat prompt, all escapes are done twice, so there are two backslashes. 181 | 182 | I'll execute a search and as you can see, we're matching the desired string with 183 | variable number of digits at the end. Yaay! 184 | 185 | 8 - Other use examples 186 | ====================== 187 | Actions 188 | ------- 189 | *brew* 190 | - brew info mobile-shell 191 | - select project home page 192 | - open 193 | *gist* 194 | - gist -a 195 | - some example content and a 196 | - highlight gist url 197 | *rspec* 198 | - bundle exec rspec 199 | - have a failing spec 200 | - highlight a failing spec file 201 | - vim 202 | 203 | Script 204 | ------ 205 | *brew* 206 | To conclude this screencast, I'd like to show you a couple more examples where I 207 | find this plugin useful. 208 | 209 | I'm using OS X, and I often check brew package manager packages. 210 | Let's check this project called 'mobile shell'. 211 | 212 | Hmm, that doesn't tell me much about it, but there's the project homepage in the 213 | output. 214 | I'll use url search to fetch and open that url. 215 | 216 | *gist* 217 | If you like to create gists from the command line, there's a similar use case. 218 | I'll quickly create an example gist. 219 | 220 | Again, I'll use url search to fetch the gist url, without using the mouse. 221 | 222 | *rspec* 223 | By far my most common usage of this plugin is when testing. 224 | I'm a ruby developer and I often use rspec testing framework. 225 | I'll run tests for this project. 226 | 227 | Oh-oh, it seems tests fail and I have to fix them. 228 | To open the failing test file, I'll use file search. 229 | 230 | And now, I can open the file in vim. 231 | 232 | That's it for this screencast. I hope you like tmux copycat and that you'll find 233 | it useful. 234 | -------------------------------------------------------------------------------- /scripts/copycat_jump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR/helpers.sh" 6 | 7 | MAXIMUM_PADDING="25" # maximum padding below the result when it can't be centered 8 | 9 | # jump to 'next' or 'prev' match 10 | # global var for this file 11 | NEXT_PREV="$1" 12 | 13 | # 'vi' or 'emacs', this variable used as a global file constant 14 | TMUX_COPY_MODE="$(tmux_copy_mode)" 15 | 16 | _file_number_of_lines() { 17 | local file="$1" 18 | echo "$(wc -l $file | $AWK_CMD '{print $1}')" 19 | } 20 | 21 | _get_result_line() { 22 | local file="$1" 23 | local number="$2" 24 | echo "$(head -"$number" "$file" | tail -1)" 25 | } 26 | 27 | _string_starts_with_digit() { 28 | local string="$1" 29 | echo "$string" | 30 | \grep -q '^[[:digit:]]\+:' 31 | } 32 | 33 | _get_line_number() { 34 | local string="$1" 35 | local copycat_file="$2" # args 2 & 3 used to handle bug in OSX grep 36 | local position_number="$3" 37 | if _string_starts_with_digit "$string"; then 38 | # we have a number! 39 | local grep_line_number="$(echo "$string" | cut -f1 -d:)" 40 | # grep line number index starts from 1, tmux line number index starts from 0 41 | local tmux_line_number="$((grep_line_number - 1))" 42 | else 43 | # no number in the results line This is a bug in OSX grep. 44 | # Fetching a number from a previous line. 45 | local previous_line_num="$((position_number - 1))" 46 | local result_line="$(_get_result_line "$copycat_file" "$previous_line_num")" 47 | # recursively invoke this same function 48 | tmux_line_number="$(_get_line_number "$result_line" "$copycat_file" "$previous_line_num")" 49 | fi 50 | echo "$tmux_line_number" 51 | } 52 | 53 | _get_match() { 54 | local string="$1" 55 | local full_match 56 | if _string_starts_with_digit "$string"; then 57 | full_match="$(echo "$string" | cut -f2- -d:)" 58 | else 59 | # This scenario handles OS X grep bug "no number in the results line". 60 | # When there's no number at the beginning of the line, we're taking the 61 | # whole line as a match. This handles the result line like this: 62 | # `http://www.example.com` (the `http` would otherwise get cut off) 63 | full_match="$string" 64 | fi 65 | echo -n "$full_match" 66 | } 67 | 68 | _escape_backslash() { 69 | local string="$1" 70 | echo "$(echo "$string" | sed 's/\\/\\\\/g')" 71 | } 72 | 73 | _get_match_line_position() { 74 | local file="$1" 75 | local line_number="$2" 76 | local match="$3" 77 | local adjusted_line_num=$((line_number + 1)) 78 | local result_line=$(tail -"$adjusted_line_num" "$file" | head -1) 79 | 80 | # OS X awk cannot have `=` as the first char in the variable (bug in awk). 81 | # If exists, changing the `=` character with `.` to avoid error. 82 | local platform="$(uname)" 83 | if [ "$platform" == "Darwin" ]; then 84 | result_line="$(echo "$result_line" | sed 's/^=/./')" 85 | match="$(echo "$match" | sed 's/^=/./')" 86 | fi 87 | 88 | # awk treats \r, \n, \t etc as single characters and that messes up match 89 | # highlighting. For that reason, we're escaping backslashes so above chars 90 | # are treated literally. 91 | result_line="$(_escape_backslash "$result_line")" 92 | match="$(_escape_backslash "$match")" 93 | 94 | local index=$($AWK_CMD -v a="$result_line" -v b="$match" 'BEGIN{print index(a,b)}') 95 | local zero_index=$((index - 1)) 96 | echo "$zero_index" 97 | } 98 | 99 | _copycat_jump() { 100 | local line_number="$1" 101 | local match_line_position="$2" 102 | local match="$3" 103 | local scrollback_line_number="$4" 104 | _copycat_enter_mode 105 | _copycat_exit_select_mode 106 | _copycat_jump_to_line "$line_number" "$scrollback_line_number" 107 | _copycat_position_to_match_start "$match_line_position" 108 | _copycat_select "$match" 109 | } 110 | 111 | _copycat_enter_mode() { 112 | tmux copy-mode 113 | } 114 | 115 | # clears selection from a previous match 116 | _copycat_exit_select_mode() { 117 | tmux send-keys -X clear-selection 118 | } 119 | 120 | # "manually" go up in the scrollback for a number of lines 121 | _copycat_manually_go_up() { 122 | local line_number="$1" 123 | tmux send-keys -X -N "$line_number" cursor-up 124 | tmux send-keys -X start-of-line 125 | } 126 | 127 | _copycat_create_padding_below_result() { 128 | local number_of_lines="$1" 129 | local maximum_padding="$2" 130 | local padding 131 | 132 | # Padding should not be greater than half pane height 133 | # (it wouldn't be centered then). 134 | if [ "$number_of_lines" -gt "$maximum_padding" ]; then 135 | padding="$maximum_padding" 136 | else 137 | padding="$number_of_lines" 138 | fi 139 | 140 | # cannot create padding, exit function 141 | if [ "$padding" -eq "0" ]; then 142 | return 143 | fi 144 | 145 | tmux send-keys -X -N "$padding" cursor-down 146 | tmux send-keys -X -N "$padding" cursor-up 147 | } 148 | 149 | # performs a jump to go to line 150 | _copycat_go_to_line_with_jump() { 151 | local line_number="$1" 152 | # first jumps to the "bottom" in copy mode so that jumps are consistent 153 | tmux send-keys -X history-bottom 154 | tmux send-keys -X start-of-line 155 | tmux send-keys -X goto-line $line_number 156 | } 157 | 158 | # maximum line number that can be reached via tmux 'jump' 159 | _get_max_jump() { 160 | local scrollback_line_number="$1" 161 | local window_height="$2" 162 | local max_jump=$((scrollback_line_number - $window_height)) 163 | # max jump can't be lower than zero 164 | if [ "$max_jump" -lt "0" ]; then 165 | max_jump="0" 166 | fi 167 | echo "$max_jump" 168 | } 169 | 170 | _copycat_jump_to_line() { 171 | local line_number="$1" 172 | local scrollback_line_number="$2" 173 | local window_height="$(tmux display-message -p '#{pane_height}')" 174 | local correct_line_number 175 | 176 | local max_jump=$(_get_max_jump "$scrollback_line_number" "$window_height") 177 | local correction="0" 178 | 179 | if [ "$line_number" -gt "$max_jump" ]; then 180 | # We need to 'reach' a line number that is not accessible via 'jump'. 181 | # Introducing 'correction' 182 | correct_line_number="$max_jump" 183 | correction=$((line_number - $correct_line_number)) 184 | else 185 | # we can reach the desired line number via 'jump'. Correction not needed. 186 | correct_line_number="$line_number" 187 | fi 188 | 189 | _copycat_go_to_line_with_jump "$correct_line_number" 190 | 191 | if [ "$correction" -gt "0" ]; then 192 | _copycat_manually_go_up "$correction" 193 | fi 194 | 195 | # If no corrections (meaning result is not at the top of scrollback) 196 | # we can then 'center' the result within a pane. 197 | if [ "$correction" -eq "0" ]; then 198 | local half_window_height="$((window_height / 2))" 199 | # creating as much padding as possible, up to half pane height 200 | _copycat_create_padding_below_result "$line_number" "$half_window_height" 201 | fi 202 | } 203 | 204 | _copycat_position_to_match_start() { 205 | local match_line_position="$1" 206 | [ "$match_line_position" -eq "0" ] && return 0 207 | 208 | tmux send-keys -X -N "$match_line_position" cursor-right 209 | } 210 | 211 | _copycat_select() { 212 | local match="$1" 213 | local length="${#match}" 214 | tmux send-keys -X begin-selection 215 | tmux send-keys -X -N "$length" cursor-right 216 | if [ "$TMUX_COPY_MODE" == "vi" ]; then 217 | tmux send-keys -X cursor-left # selection correction for 1 char 218 | fi 219 | } 220 | 221 | # all functions above are "private", called from `do_next_jump` function 222 | 223 | get_new_position_number() { 224 | local copycat_file="$1" 225 | local current_position="$2" 226 | local new_position 227 | 228 | # doing a forward/up jump 229 | if [ "$NEXT_PREV" == "next" ]; then 230 | local number_of_results=$(wc -l "$copycat_file" | $AWK_CMD '{ print $1 }') 231 | if [ "$current_position" -eq "$number_of_results" ]; then 232 | # position can't go beyond the last result 233 | new_position="$current_position" 234 | else 235 | new_position="$((current_position + 1))" 236 | fi 237 | 238 | # doing a backward/down jump 239 | elif [ "$NEXT_PREV" == "prev" ]; then 240 | if [ "$current_position" -eq "1" ]; then 241 | # position can't go below 1 242 | new_position="1" 243 | else 244 | new_position="$((current_position - 1))" 245 | fi 246 | fi 247 | echo "$new_position" 248 | } 249 | 250 | do_next_jump() { 251 | local position_number="$1" 252 | local copycat_file="$2" 253 | local scrollback_file="$3" 254 | 255 | local scrollback_line_number=$(_file_number_of_lines "$scrollback_file") 256 | local result_line="$(_get_result_line "$copycat_file" "$position_number")" 257 | local line_number=$(_get_line_number "$result_line" "$copycat_file" "$position_number") 258 | local match=$(_get_match "$result_line") 259 | local match_line_position=$(_get_match_line_position "$scrollback_file" "$line_number" "$match") 260 | _copycat_jump "$line_number" "$match_line_position" "$match" "$scrollback_line_number" 261 | } 262 | 263 | notify_about_first_last_match() { 264 | local current_position="$1" 265 | local next_position="$2" 266 | local message_duration="1500" 267 | 268 | # if position didn't change, we are either on a 'first' or 'last' match 269 | if [ "$current_position" -eq "$next_position" ]; then 270 | if [ "$NEXT_PREV" == "next" ]; then 271 | display_message "Last match!" "$message_duration" 272 | elif [ "$NEXT_PREV" == "prev" ]; then 273 | display_message "First match!" "$message_duration" 274 | fi 275 | fi 276 | } 277 | 278 | main() { 279 | if in_copycat_mode; then 280 | local copycat_file="$(get_copycat_filename)" 281 | local scrollback_file="$(get_scrollback_filename)" 282 | local current_position="$(get_copycat_position)" 283 | local next_position="$(get_new_position_number "$copycat_file" "$current_position")" 284 | do_next_jump "$next_position" "$copycat_file" "$scrollback_file" 285 | notify_about_first_last_match "$current_position" "$next_position" 286 | set_copycat_position "$next_position" 287 | fi 288 | } 289 | main 290 | --------------------------------------------------------------------------------