├── .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 | [](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 | [](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 |
--------------------------------------------------------------------------------