├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── demo.gif ├── easy_motion.tmux └── scripts ├── common_variables.sh ├── easy_motion.py ├── easy_motion.sh ├── helpers.sh ├── options.sh └── pipe_target_key.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # Universal EditorConfig config file, see https://editorconfig.org for the spec 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # General settings for all files: 7 | # - UTF-8 charset 8 | # - Unix-style newlines with a newline ending every file 9 | # - Indent with 4 spaces 10 | # - Set the maximum line length to 120 characters (not supported by all editors) 11 | # - Trim trailing whitespace 12 | [*] 13 | charset = utf-8 14 | end_of_line = lf 15 | insert_final_newline = true 16 | indent_style = space 17 | indent_size = 4 18 | max_line_length = 120 19 | trim_trailing_whitespace = true 20 | 21 | # Tab indentation for Makefiles 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.pyc 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ingo Meyer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim's easy-motion for tmux 2 | 3 | ## Demo 4 | 5 | ![tmux-easy-motion demo](https://raw.githubusercontent.com/IngoMeyer441/tmux-easy-motion/master/demo.gif) 6 | 7 | ## Introduction 8 | 9 | This plugin brings Vim's [easy-motion](https://github.com/easymotion/vim-easymotion) navigation plugin to tmux. There 10 | are already some other plugins with similar functionality: 11 | 12 | - [tmux-jump](https://github.com/schasse/tmux-jump): Implements the seek operation of easy-motion. 13 | - [tmux-easymotion](https://github.com/ddzero2c/tmux-easymotion): Also implements the seek operation. 14 | - [tmux-fingers](https://github.com/Morantron/tmux-fingers): Copy text by selecting a hint marker. 15 | - [tmux-thumbs](https://github.com/fcsonline/tmux-thumbs): Alternative to tmux-fingers. 16 | 17 | However, none of the already-existing plugins implement other movements than seeking. Therefore, I started my own 18 | implementation which adds much more easy-motion movements. All standard vi motions (`b`, `B`, 19 | `w`, `W`, `e`, `E`, `ge`, `gE`, `j`, `k`, `f`, `F`, `t`, `T`) and these vim-easy-motion movements are supported: 20 | 21 | - `J` (`j` + move to end of line) 22 | - `K` (`k` + move to end of line) 23 | - `bd-w` (`bd-*` -> bidirectional motion) 24 | - `bd-W` 25 | - `bd-e` 26 | - `bd-E` 27 | - `bd-j` 28 | - `bd-J` 29 | - `bd-f` 30 | - `bd-f2` (search for 2 characters) 31 | - `bd-t` 32 | - `bd-T` 33 | - `c` (target camelCase or underscore notations) 34 | 35 | By default, only the standard vim motions, `J`, `K` and `c` are bound to the keyboard. If you would like to use a 36 | bidirectional motions, you need to configure a key binding for it. See the [key-bindings 37 | section](https://github.com/IngoMeyer441/tmux-easy-motion#key-bindings) of this README for more details. 38 | 39 | Special thanks to the authors of the [tmux-fingers](https://github.com/Morantron/tmux-fingers) project. Reading their 40 | source code helped a lot to understand how an easy-motion plugin can be implemented for tmux. 41 | 42 | ## Requirements 43 | 44 | This plugin needs at least tmux 3.1 and Python 2.7 or 3.3+. You can check your installed tmux version with 45 | 46 | ```bash 47 | tmux -V 48 | ``` 49 | 50 | and your installed Python version with 51 | 52 | ```bash 53 | python --version 54 | ``` 55 | 56 | If you are using a quite recent Linux distribution or macOS, an appropriate Python version should already be installed. 57 | 58 | ## Installation 59 | 60 | ### Using tpm 61 | 62 | 1. Add `set -g @plugin 'IngoMeyer441/tmux-easy-motion'` to your `.tmux.conf`. 63 | 64 | 2. Configure a prefix key for easy-motion movements, the default is `Space`: 65 | 66 | ``` 67 | set -g @easy-motion-prefix "Space" 68 | ``` 69 | 70 | By default, the `Space` key changes layouts in tmux (non-copy mode) or sets the beginning of a selection (copy mode). 71 | Therefore, you should configure other keys for these actions if you would like to use `Space` as easy-motion prefix 72 | key, for example: 73 | 74 | ``` 75 | bind-key v next-layout 76 | bind-key -T copy-mode-vi v send-keys -X begin-selection 77 | ``` 78 | 79 | You can also configure another key binding for copy mode by setting `@easy-motion-copy-mode-prefix`. 80 | 81 | ### Manual 82 | 83 | 1. Clone this repository and add 84 | 85 | ``` 86 | run-shell /easy_motion.tmux 87 | ``` 88 | 89 | to `.tmux.conf`. 90 | 91 | 2. Configure prefix keys like explained above. 92 | 93 | ## Usage 94 | 95 | Press the tmux prefix key followed by the configured easy-motion prefix key (by default `Ctrl-b Space`) to enter the 96 | easy-motion mode. Enter a vi motion command and possible jump targets will be highlighted by red and yellow letters. 97 | Press one of the highlighted letters to enter tmux copy-mode and jump to the corresponding position directly. 98 | 99 | This plugin also works in tmux copy-mode. In copy-mode you don't need to press the tmux prefix key. 100 | 101 | ### Grouping 102 | 103 | If more jump targets exist than configured target keys, targets will be grouped and a second key press is needed to 104 | determine the jump target (see the [demo](#demo) for an example). Groups always contain a preview of the next key which 105 | is needed to reach the target position. The grouping works exactly like the grouping mechanism in Vim's easy-motion 106 | plugin. 107 | 108 | The grouping algorithm works recursively, so grouping is repeated if necessary. However, that case should only occur if 109 | a small set of target keys was configured. 110 | 111 | ## Configuration 112 | 113 | ### Key bindings 114 | 115 | You can set a prefix key for tmux-easy-motion with the `@easy-motion-prefix` option. It will be bound on the prefix and 116 | the copy-mode-vi key table. If you don't want to use the same prefix key for both modes, you can set another key binding 117 | for copy mode with the `@easy-motion-copy-mode-prefix` option or you can disable the key bindings by setting 118 | `@easy-motion-prefix-enabled` and/or `@easy-motion-copy-mode-prefix-enabled` to `false` (or `no`, `off`, `disabled`, 119 | `deactivated`, `0`). 120 | 121 | By default, tmux-easy-motion creates default key bindings for all standard vim motions, `J`, `K` and `c`. If you would 122 | like to remove, change or add a single key bindings, change the corresponding option (see the list below). 123 | Alternatively, you can set `@easy-motion-default-key-bindings` to `false` (or `off`, `disabled`, `no`, `deactivated`, 124 | `0`) and configure all easy-motion key binding options yourself. 125 | 126 | Available key binding options: 127 | 128 | - `@easy-motion-binding-b` 129 | - `@easy-motion-binding-B` 130 | - `@easy-motion-binding-ge` 131 | - `@easy-motion-binding-gE` 132 | - `@easy-motion-binding-e` 133 | - `@easy-motion-binding-E` 134 | - `@easy-motion-binding-w` 135 | - `@easy-motion-binding-W` 136 | - `@easy-motion-binding-j` 137 | - `@easy-motion-binding-J` 138 | - `@easy-motion-binding-k` 139 | - `@easy-motion-binding-K` 140 | - `@easy-motion-binding-f` 141 | - `@easy-motion-binding-F` 142 | - `@easy-motion-binding-t` 143 | - `@easy-motion-binding-T` 144 | - `@easy-motion-binding-bd-w` 145 | - `@easy-motion-binding-bd-W` 146 | - `@easy-motion-binding-bd-e` 147 | - `@easy-motion-binding-bd-E` 148 | - `@easy-motion-binding-bd-j` 149 | - `@easy-motion-binding-bd-J` 150 | - `@easy-motion-binding-bd-f` 151 | - `@easy-motion-binding-bd-f2` 152 | - `@easy-motion-binding-bd-t` 153 | - `@easy-motion-binding-bd-T` 154 | - `@easy-motion-binding-c` 155 | 156 | If you only want to use a single easy-motion movement, you can configure it as the default motion which is activated 157 | directly after pressing the easy-motion prefix key and save one key press (`@easy-motion-default-motion`). 158 | 159 | Example: 160 | 161 | ``` 162 | set -g @easy-motion-default-motion "bd-w" 163 | ``` 164 | 165 | This setting will cause the highlight of all word beginnings (bidirectional) after pressing the configured easy-motion 166 | prefix key. 167 | 168 | ### Target keys 169 | 170 | The target keys can be configured with the `@easy-motion-target-keys` option. The default is taken from the 171 | Vim default configuration value (`"asdghklqwertyuiopzxcvbnmfj;"`) 172 | 173 | You can configure as many keys as you want (minimum two keys). 174 | 175 | Example: 176 | 177 | ``` 178 | set -g @easy-motion-target-keys "asdfghjkl;" 179 | ``` 180 | 181 | ### Colors 182 | 183 | The color of dimmed and highlighted text can be configured by setting four style options. These are the default 184 | settings (taken from the easy-motion Vim plugin): 185 | 186 | ``` 187 | set -g @easy-motion-dim-style "fg=colour242" 188 | set -g @easy-motion-highlight-style "fg=colour196,bold" 189 | set -g @easy-motion-highlight-2-first-style "fg=brightyellow,bold" 190 | set -g @easy-motion-highlight-2-second-style "fg=yellow,bold" 191 | ``` 192 | 193 | Possible style values are described in the [tmux man page](https://man7.org/linux/man-pages/man1/tmux.1.html#STYLES). 194 | 195 | These settings were used in the demo: 196 | 197 | ``` 198 | set -g @easy-motion-dim-style "fg=colour242" 199 | set -g @easy-motion-highlight-style "fg=colour196,bold" 200 | set -g @easy-motion-highlight-2-first-style "fg=#ffb400,bold" 201 | set -g @easy-motion-highlight-2-second-style "fg=#b98300,bold" 202 | ``` 203 | 204 | ### Verbose mode 205 | 206 | By setting 207 | 208 | ``` 209 | set -g @easy-motion-verbose "true" 210 | ``` 211 | 212 | tmux-easy-motion operates in verbose mode which displays messages when easy-motion is activated and a motion was 213 | selected. 214 | 215 | 216 | ### Auto selection 217 | 218 | By setting 219 | ``` 220 | set -g @easy-motion-auto-begin-selection "true" 221 | ``` 222 | 223 | you can enable the automatic start of selection 224 | 225 | ## Other plugins 226 | 227 | If you like this plugin and use zsh, please also try my easy-motion port for zsh: 228 | [zsh-easy-motion](https://github.com/IngoMeyer441/zsh-easy-motion). 229 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IngoMeyer441/tmux-easy-motion/3e2edbd0a3d9924cc1df3bd3529edc507bdf5934/demo.gif -------------------------------------------------------------------------------- /easy_motion.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | SCRIPTS_DIR="${CURRENT_DIR}/scripts" 5 | 6 | # shellcheck source=./scripts/helpers.sh 7 | source "${SCRIPTS_DIR}/helpers.sh" 8 | # shellcheck source=./scripts/options.sh 9 | source "${SCRIPTS_DIR}/options.sh" 10 | 11 | 12 | check_version() { 13 | if ! is_tmux_version_greater_or_equal "3.1"; then 14 | display_message "tmux-easy-motion needs tmux version 3.1 or newer." 15 | return 1 16 | fi 17 | } 18 | 19 | setup_bindings() { 20 | local server_pid key_table key target_key tmux_key prefix_for_key_table 21 | 22 | server_pid="$1" 23 | 24 | if [[ -z "${EASY_MOTION_DEFAULT_MOTION}" ]]; then 25 | if (( EASY_MOTION_VERBOSE )); then 26 | if (( EASY_MOTION_PREFIX_ENABLED )); then 27 | tmux source - <<-EOF 28 | bind-key "${EASY_MOTION_PREFIX}" { 29 | switch-client -T easy-motion 30 | display-message "tmux-easy-motion activated, please type a motion command." 31 | } 32 | EOF 33 | fi 34 | if (( EASY_MOTION_COPY_MODE_PREFIX_ENABLED )); then 35 | tmux source - <<-EOF 36 | bind-key -T copy-mode-vi "${EASY_MOTION_COPY_MODE_PREFIX}" { 37 | switch-client -T easy-motion 38 | display-message "tmux-easy-motion activated, please type a motion command." 39 | } 40 | EOF 41 | fi 42 | else 43 | if (( EASY_MOTION_PREFIX_ENABLED )); then 44 | tmux bind-key "${EASY_MOTION_PREFIX}" switch-client -T easy-motion 45 | fi 46 | if (( EASY_MOTION_COPY_MODE_PREFIX_ENABLED )); then 47 | tmux bind-key -T copy-mode-vi "${EASY_MOTION_COPY_MODE_PREFIX}" switch-client -T easy-motion 48 | fi 49 | fi 50 | 51 | tmux bind-key -T easy-motion "g" switch-client -T easy-motion-g 52 | tmux bind-key -T easy-motion "Escape" switch-client -T root 53 | tmux bind-key -T easy-motion-g "Escape" switch-client -T root 54 | 55 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_B}" "b" 56 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_B}" "B" 57 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_GE}" "ge" 58 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_GE}" "gE" 59 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_E}" "e" 60 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_E}" "E" 61 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_W}" "w" 62 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_W}" "W" 63 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_J}" "j" 64 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_J}" "J" # end of line 65 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_K}" "k" 66 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_K}" "K" # end of line 67 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_F}" "f" 68 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_F}" "F" 69 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_T}" "t" 70 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_T}" "T" 71 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_BD_W}" "bd-w" # bd -> bidirectional 72 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_BD_W}" "bd-W" 73 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_BD_E}" "bd-e" 74 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_BD_E}" "bd-E" 75 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_BD_J}" "bd-j" 76 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_BD_J}" "bd-J" 77 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_BD_F}" "bd-f" 78 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_BD_F2}" "bd-f2" 79 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_BD_T}" "bd-t" 80 | setup_single_key_binding_with_argument "${server_pid}" "${EASY_MOTION_BINDING_CAPITAL_BD_T}" "bd-T" 81 | setup_single_key_binding "${server_pid}" "${EASY_MOTION_BINDING_C}" "c" # camelCase or underscore notation 82 | else 83 | case "${EASY_MOTION_DEFAULT_MOTION}" in 84 | b|B|ge|gE|e|E|w|W|j|J|k|K|bd-w|bd-W|bd-e|bd-E|bd-j|bd-J|c) 85 | for key_table in "prefix" "copy-mode-vi"; do 86 | if ! get_prefix_enabled_for_key_table "${key_table}"; then 87 | continue 88 | fi 89 | prefix_for_key_table=$(get_prefix_for_key_table "${key_table}") 90 | tmux bind-key -T "${key_table}" "${prefix_for_key_table}" run-shell -b \ 91 | "${SCRIPTS_DIR}/easy_motion.sh '${server_pid}' '#{session_id}' '#{window_id}' '#{pane_id}' '${EASY_MOTION_DEFAULT_MOTION}'" 92 | done 93 | ;; 94 | f|F|t|T|bd-f|bd-f2|bd-t|bd-T) 95 | for key_table in "prefix" "copy-mode-vi"; do 96 | if ! get_prefix_enabled_for_key_table "${key_table}"; then 97 | continue 98 | fi 99 | prefix_for_key_table=$(get_prefix_for_key_table "${key_table}") 100 | tmux bind-key -T "${key_table}" "${prefix_for_key_table}" run-shell -b \ 101 | "${SCRIPTS_DIR}/easy_motion.sh '${server_pid}' '#{session_id}' '#{window_id}' '#{pane_id}' '${EASY_MOTION_DEFAULT_MOTION}'" 102 | if [[ "${EASY_MOTION_DEFAULT_MOTION}" != "bd-f2" ]]; then 103 | tmux source - <<-EOF 104 | bind-key -T "${key_table}" "${prefix_for_key_table}" command-prompt -1 -p "character:" { 105 | set -g @tmp-easy-motion-argument "%%%" 106 | run-shell -b '${SCRIPTS_DIR}/easy_motion.sh "${server_pid}" "\#{session_id}" "#{window_id}" "#{pane_id}" "${EASY_MOTION_DEFAULT_MOTION}" "#{q:@tmp-easy-motion-argument}"' 107 | } 108 | EOF 109 | else 110 | tmux source - <<-EOF 111 | bind-key -T "${key_table}" "${prefix_for_key_table}" command-prompt -1 -p "character 1:,character 2:" { 112 | set -g @tmp-easy-motion-argument1 "%1" 113 | set -g @tmp-easy-motion-argument2 "%2" 114 | run-shell -b '${SCRIPTS_DIR}/easy_motion.sh "${server_pid}" "\#{session_id}" "#{window_id}" "#{pane_id}" "${EASY_MOTION_DEFAULT_MOTION}" "#{q:@tmp-easy-motion-argument1}#{q:@tmp-easy-motion-argument2}"' 115 | } 116 | EOF 117 | fi 118 | done 119 | ;; 120 | *) 121 | display_message "The motion \"${EASY_MOTION_DEFAULT_MOTION}\" is not a valid motion." 122 | exit 1 123 | ;; 124 | esac 125 | fi 126 | 127 | while read -n1 key; do 128 | case "${key}" in 129 | \;) 130 | tmux_key="\\${key}" 131 | ;; 132 | *) 133 | tmux_key="${key}" 134 | ;; 135 | esac 136 | case "${key}" in 137 | \"|\`) 138 | target_key="\\${key}" 139 | ;; 140 | *) 141 | target_key="${key}" 142 | ;; 143 | esac 144 | # `easy_motion.sh` switches the key table to `easy-motion-target` 145 | tmux bind-key -T easy-motion-target "${tmux_key}" run-shell -b "${SCRIPTS_DIR}/pipe_target_key.sh '${server_pid}' '#{session_id}' '${target_key}'" 146 | done < <(echo -n "${EASY_MOTION_TARGET_KEYS}") 147 | tmux bind-key -T easy-motion-target "Escape" run-shell -b "${SCRIPTS_DIR}/pipe_target_key.sh '${server_pid}' '#{session_id}' 'esc'" 148 | } 149 | 150 | main() { 151 | local server_pid 152 | server_pid="$(get_tmux_server_pid)" 153 | 154 | check_version && \ 155 | read_options && \ 156 | install_target_key_pipe_cleanup_hook "${server_pid}" && \ 157 | setup_bindings "${server_pid}" 158 | } 159 | 160 | main 161 | -------------------------------------------------------------------------------- /scripts/common_variables.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -z "${TMPDIR}" ]]; then 4 | TMPDIR="$(dirname "$(mktemp "tmp.XXXXXXXXXX" -ut)")" 5 | fi 6 | TARGET_KEY_PIPENAME="target_key.pipe" 7 | -------------------------------------------------------------------------------- /scripts/easy_motion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function, unicode_literals 5 | 6 | import codecs 7 | import re 8 | import subprocess 9 | import sys 10 | import termios 11 | import time 12 | 13 | PY2 = sys.version_info.major < 3 # is needed for correct mypy checking 14 | 15 | if PY2: 16 | from itertools import izip_longest as zip_longest 17 | else: 18 | from itertools import zip_longest 19 | 20 | try: 21 | from typing import ( # noqa: F401 # pylint: disable=unused-import 22 | IO, 23 | Any, 24 | AnyStr, 25 | Callable, 26 | Dict, 27 | Generator, 28 | Iterable, 29 | Iterator, 30 | List, 31 | Optional, 32 | Tuple, 33 | Union, 34 | cast, 35 | ) 36 | except ImportError: 37 | cast = lambda t, x: x # type: ignore # noqa: E731 38 | 39 | if PY2: 40 | str = unicode 41 | 42 | VALID_MOTIONS = frozenset( 43 | ( 44 | "b", 45 | "B", 46 | "ge", 47 | "gE", 48 | "e", 49 | "E", 50 | "w", 51 | "W", 52 | "j", 53 | "J", # end of line 54 | "k", 55 | "K", # end of line 56 | "f", 57 | "F", 58 | "t", 59 | "T", 60 | "bd-w", # bd -> bidirectional 61 | "bd-W", 62 | "bd-e", 63 | "bd-E", 64 | "bd-j", 65 | "bd-J", 66 | "bd-f", 67 | "bd-f2", 68 | "bd-t", 69 | "bd-T", 70 | "c", # camelCase or underscore notation 71 | ) 72 | ) 73 | MOTIONS_WITH_ARGUMENT = frozenset(("f", "F", "t", "T", "bd-f", "bd-f2", "bd-t", "bd-T")) 74 | FORWARD_MOTIONS = frozenset( 75 | ( 76 | "e", 77 | "E", 78 | "w", 79 | "W", 80 | "j", 81 | "J", 82 | "f", 83 | "t", 84 | "bd-w", 85 | "bd-W", 86 | "bd-e", 87 | "bd-E", 88 | "bd-j", 89 | "bd-J", 90 | "bd-f", 91 | "bd-f2", 92 | "bd-t", 93 | "bd-T", 94 | "c", 95 | ) 96 | ) 97 | BACKWARD_MOTIONS = frozenset( 98 | ( 99 | "b", 100 | "B", 101 | "ge", 102 | "gE", 103 | "k", 104 | "K", 105 | "F", 106 | "T", 107 | "bd-w", 108 | "bd-W", 109 | "bd-e", 110 | "bd-E", 111 | "bd-j", 112 | "bd-J", 113 | "bd-f", 114 | "bd-f2", 115 | "bd-t", 116 | "bd-T", 117 | "c", 118 | ) 119 | ) 120 | LINEWISE_MOTIONS = frozenset(("j", "J", "k", "K", "bd-j", "bd-J")) 121 | MOTION_TO_REGEX = { 122 | "b": r"\b(\w)", 123 | "B": r"(?:^|\s)(\S)", 124 | "ge": r"(\w)\b", 125 | "gE": r"(\S)(?:\s|$)", 126 | "e": r"(\w)\b", 127 | "E": r"(\S)(?:\s|$)", 128 | "w": r"\b(\w)", 129 | "W": r"(?:^|\s)(\S)", 130 | "j": r"^(?:\s*)(\S)", 131 | "J": r"(\S)(?:\s*)$", 132 | "k": r"^(?:\s*)(\S)", 133 | "K": r"(\S)(?:\s*)$", 134 | "f": r"({})", 135 | "F": r"({})", 136 | "t": r"(.){}", 137 | "T": r"{}(.)", 138 | "bd-w": r"\b(\w)", 139 | "bd-W": r"(?:^|\s)(\S)", 140 | "bd-e": r"(\w)\b", 141 | "bd-E": r"(\S)(?:\s|$)", 142 | "bd-j": r"^(?:\s*)(\S)", 143 | "bd-J": r"(\S)(?:\s*)$", 144 | "bd-f": r"({})", 145 | "bd-f2": r"({})", 146 | "bd-t": r"(.){}", 147 | "bd-T": r"{}(.)", 148 | "c": r"(?:_(\w))|(?:[a-z]([A-Z]))", 149 | } 150 | 151 | 152 | class MissingDimStyleError(Exception): 153 | pass 154 | 155 | 156 | class InvalidDimStyleError(Exception): 157 | pass 158 | 159 | 160 | class MissingHighlightStyleError(Exception): 161 | pass 162 | 163 | 164 | class InvalidHighlightStyleError(Exception): 165 | pass 166 | 167 | 168 | class MissingHighlight2FirstStyleError(Exception): 169 | pass 170 | 171 | 172 | class InvalidHighlight2FirstStyleError(Exception): 173 | pass 174 | 175 | 176 | class MissingHighlight2SecondStyleError(Exception): 177 | pass 178 | 179 | 180 | class InvalidHighlight2SecondStyleError(Exception): 181 | pass 182 | 183 | 184 | class MissingMotionError(Exception): 185 | pass 186 | 187 | 188 | class InvalidMotionError(Exception): 189 | pass 190 | 191 | 192 | class MissingMotionArgumentError(Exception): 193 | pass 194 | 195 | 196 | class MissingTargetKeysError(Exception): 197 | pass 198 | 199 | 200 | class MissingCursorPositionError(Exception): 201 | pass 202 | 203 | 204 | class InvalidCursorPositionError(Exception): 205 | pass 206 | 207 | 208 | class MissingPaneSizeError(Exception): 209 | pass 210 | 211 | 212 | class InvalidPaneSizeError(Exception): 213 | pass 214 | 215 | 216 | class MissingCaptureBufferFilepathError(Exception): 217 | pass 218 | 219 | 220 | class MissingJumpCommandPipeFilepathError(Exception): 221 | pass 222 | 223 | 224 | class MissingTargetKeyPipeFilepathError(Exception): 225 | pass 226 | 227 | 228 | class InvalidTargetError(Exception): 229 | pass 230 | 231 | 232 | class TerminalCodes(object): 233 | class Style(object): 234 | BLINK = "\033[5m" 235 | BOLD = "\033[1m" 236 | DIM = "\033[2m" 237 | ITALIC = "\033[3m" 238 | UNDERLINE = "\033[4m" 239 | REVERSE = "\033[7m" 240 | CONCEAL = "\033[8m" 241 | OVERLINE = "\033[53m" 242 | STRIKE = "\033[9m" 243 | DOUBLE_UNDERLINE = "\033[4:2m" 244 | CURLY_UNDERLINE = "\033[4:3m" 245 | DOTTED_UNDERLINE = "\033[4:4m" 246 | DASHED_UNDERLINE = "\033[4:5m" 247 | RESET = "\033[0m" 248 | 249 | @classmethod 250 | def color16(cls, name, bg=False): 251 | # type: (str, bool) -> str 252 | name_to_index = { 253 | "black": 0, 254 | "red": 1, 255 | "green": 2, 256 | "yellow": 3, 257 | "blue": 4, 258 | "magenta": 5, 259 | "cyan": 6, 260 | "white": 7, 261 | "brightblack": 8, 262 | "brightred": 9, 263 | "brightgreen": 10, 264 | "brightyellow": 11, 265 | "brightblue": 12, 266 | "brightmagenta": 13, 267 | "brightcyan": 14, 268 | "brightwhite": 15, 269 | } # type: Dict[str, int] 270 | if name not in name_to_index: 271 | raise KeyError( 272 | '"{}" is not a valid color name. Valid color names are: "{}"'.format( 273 | name, '", "'.join(name_to_index.keys()) 274 | ) 275 | ) 276 | color_index = name_to_index[name] 277 | ansi_code = 30 + color_index 278 | if color_index > 7: 279 | ansi_code += 60 - 8 280 | if bg: 281 | ansi_code += 10 282 | return "\033[{:d}m".format(ansi_code) 283 | 284 | @classmethod 285 | def color256(cls, color_index, bg=False): 286 | # type: (int, bool) -> str 287 | if not 0 <= color_index < 256: 288 | raise IndexError("The color index must be an integer between 0 and 255.") 289 | return "\033[{:d};5;{:d}m".format(48 if bg else 38, color_index) 290 | 291 | @classmethod 292 | def truecolor(cls, r, g, b, bg=False): 293 | # type: (int, int, int, bool) -> str 294 | if not all(0 <= c < 256 for c in (r, g, b)): 295 | raise IndexError("The color indices must be integers between 0 and 255.") 296 | return "\033[{:d};2;{:d};{:d};{:d}m".format(48 if bg else 38, r, g, b) 297 | 298 | @classmethod 299 | def parse_style(cls, style): 300 | # type: (str) -> str 301 | def color_to_code(color, bg=False): 302 | # type: (str, bool) -> str 303 | color = color.strip().lower() 304 | color256_regex = re.compile(r"^colou?r(\d+)$") 305 | truecolor_regex = re.compile(r"^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$") 306 | color256_match = color256_regex.match(color) 307 | if color256_match: 308 | return cls.color256(int(color256_match.group(1)), bg) 309 | truecolor_match = truecolor_regex.match(color) 310 | if truecolor_match: 311 | return cls.truecolor(*[int(c, base=16) for c in truecolor_match.groups()], bg=bg) 312 | return cls.color16(color, bg) 313 | 314 | style_to_code = { 315 | "none": cls.RESET, 316 | "bold": cls.BOLD, 317 | "bright": cls.BOLD, 318 | "dim": cls.DIM, 319 | "underscore": cls.UNDERLINE, 320 | "blink": cls.BLINK, 321 | "reverse": cls.REVERSE, 322 | "hidden": cls.CONCEAL, 323 | "italics": cls.ITALIC, 324 | "overline": cls.OVERLINE, 325 | "double-underscore": cls.DOUBLE_UNDERLINE, 326 | "curly-underscore": cls.CURLY_UNDERLINE, 327 | "dotted-underscore": cls.DOTTED_UNDERLINE, 328 | "dashed-underscore": cls.DASHED_UNDERLINE, 329 | "fg=": lambda x: color_to_code(x, bg=False), 330 | "bg=": lambda x: color_to_code(x, bg=True), 331 | } # type: Dict[str, Union[str, Callable[[str], str]]] 332 | style_parts = [part for part in re.split(r"(?:\s+)|(?:\s*,\s*)", style.lower()) if part] 333 | style_codes = [] 334 | for style_part in style_parts: 335 | if "=" in style_part: 336 | style_split = style_part.split("=") 337 | style_key = style_split[0] + "=" 338 | style_argument = "=".join(style_split[1:]) 339 | else: 340 | style_key = style_part 341 | style_argument = style_part 342 | style_code = style_to_code[style_key] 343 | if isinstance(style_code, str): 344 | style_codes.append(style_code) 345 | else: 346 | style_codes.append(style_code(style_argument)) 347 | return "".join(style_codes) 348 | 349 | CLEAR_SCREEN = "\033[H\033[J" 350 | POSITION_CURSOR = "\033[{:d};{:d}H" 351 | 352 | 353 | class JumpTarget(object): 354 | DIRECT = 0 355 | GROUP = 1 356 | PREVIEW = 2 357 | 358 | 359 | def display_tmux_message(message): 360 | # type: (str) -> None 361 | subprocess.check_call(["tmux", "display-message", message], universal_newlines=True) 362 | 363 | 364 | def parse_arguments(): 365 | # type: () -> Tuple[str, str, str, str, str, Optional[str], str, Tuple[int, int], Tuple[int, int], str, str, str] 366 | if PY2: 367 | argv = [arg.decode("utf-8") for arg in sys.argv] 368 | else: 369 | argv = list(sys.argv) 370 | # Remove program name from argument vector 371 | argv.pop(0) 372 | # Extract dim style 373 | if not argv: 374 | raise MissingDimStyleError("No dim style given.") 375 | try: 376 | dim_style = argv.pop(0) 377 | dim_style_code = TerminalCodes.Style.parse_style(dim_style) 378 | except (IndexError, KeyError): 379 | raise InvalidDimStyleError('"{}" is not a valid style.'.format(dim_style)) 380 | # Extract highlight style 381 | if not argv: 382 | raise MissingHighlightStyleError("No highlight style given.") 383 | try: 384 | highlight_style = argv.pop(0) 385 | highlight_style_code = TerminalCodes.Style.parse_style(highlight_style) 386 | except (IndexError, KeyError): 387 | raise InvalidHighlightStyleError('"{}" is not a valid style.'.format(highlight_style)) 388 | # Extract highlight 2 first style 389 | if not argv: 390 | raise MissingHighlight2FirstStyleError("No highlight 2 first style given.") 391 | try: 392 | highlight_2_first_style = argv.pop(0) 393 | highlight_2_first_style_code = TerminalCodes.Style.parse_style(highlight_2_first_style) 394 | except (IndexError, KeyError): 395 | raise InvalidHighlight2FirstStyleError('"{}" is not a valid style.'.format(highlight_2_first_style)) 396 | # Extract highlight 2 second style 397 | if not argv: 398 | raise MissingHighlight2SecondStyleError("No highlight 2 second style given.") 399 | try: 400 | highlight_2_second_style = argv.pop(0) 401 | highlight_2_second_style_code = TerminalCodes.Style.parse_style(highlight_2_second_style) 402 | except (IndexError, KeyError): 403 | raise InvalidHighlight2SecondStyleError('"{}" is not a valid style.'.format(highlight_2_second_style)) 404 | # Extract motion 405 | if not argv: 406 | raise MissingMotionError("No motion given.") 407 | if argv[0] not in VALID_MOTIONS: 408 | raise InvalidMotionError('The string "{}" is not in a valid motion.'.format(argv[0])) 409 | motion = argv.pop(0) 410 | # Extract motion argument if needed 411 | motion_argument = None 412 | if not argv: 413 | raise MissingMotionArgumentError("No motion argument given.") 414 | motion_argument = argv[0] if motion in MOTIONS_WITH_ARGUMENT else None 415 | argv.pop(0) 416 | # Extract target keys 417 | if not argv: 418 | raise MissingTargetKeysError("No target keys given.") 419 | target_keys = argv.pop(0) 420 | if len(target_keys) < 2: 421 | raise MissingTargetKeysError("At least two target keys are needed.") 422 | # Extract cursor position 423 | if not argv: 424 | raise MissingCursorPositionError("No cursor position given.") 425 | cursor_pos_match = re.match(r"(\d+):(\d+)", argv[0]) 426 | if not cursor_pos_match: 427 | raise InvalidCursorPositionError('The cursor position "{}" is not in the format ":".'.format(argv[0])) 428 | cursor_position_row_col = (int(cursor_pos_match.group(1)), int(cursor_pos_match.group(2))) 429 | argv.pop(0) 430 | # Extract pane size 431 | if not argv: 432 | raise MissingPaneSizeError("No pane size given.") 433 | pane_size_match = re.match(r"(\d+):(\d+)", argv[0]) 434 | if not pane_size_match: 435 | raise InvalidPaneSizeError('The pane size "{}" is not in the format ":".'.format(argv[0])) 436 | pane_size = (int(pane_size_match.group(1)), int(pane_size_match.group(2))) 437 | argv.pop(0) 438 | # Extract capture buffer filepath 439 | if not argv: 440 | raise MissingCaptureBufferFilepathError("No tmux capture buffer filepath given.") 441 | capture_buffer_filepath = argv.pop(0) 442 | # Extract jump command pipe filepath 443 | if not argv: 444 | raise MissingJumpCommandPipeFilepathError("No jump command pipe filepath given.") 445 | command_pipe_filepath = argv.pop(0) 446 | # Extract target key pipe filepath 447 | if not argv: 448 | raise MissingTargetKeyPipeFilepathError("No target key pipe filepath given.") 449 | target_key_pipe_filepath = argv.pop(0) 450 | return ( 451 | dim_style_code, 452 | highlight_style_code, 453 | highlight_2_first_style_code, 454 | highlight_2_second_style_code, 455 | motion, 456 | motion_argument, 457 | target_keys, 458 | cursor_position_row_col, 459 | pane_size, 460 | capture_buffer_filepath, 461 | command_pipe_filepath, 462 | target_key_pipe_filepath, 463 | ) 464 | 465 | 466 | def convert_row_col_to_text_pos(row, col, text): 467 | # type: (int, int, str) -> int 468 | lines = text.split("\n") 469 | # Limit `row` and `col` to the existing text 470 | row = max(min(row, len(lines) - 1), 0) 471 | row_line = lines[row] 472 | col = max(min(col, len(row_line) - 1), 0) 473 | 474 | cursor_position = sum(len(line) for line in lines[:row]) + col 475 | cursor_position += row # add `row` newline characters 476 | 477 | return cursor_position 478 | 479 | 480 | def convert_text_pos_to_row_col(textpos, text): 481 | # type: (int, str) -> Tuple[int, int] 482 | lines = text.split("\n") 483 | current_textpos = 0 484 | row, col = 0, 0 485 | for line in lines: 486 | line_length = len(line) 487 | if current_textpos + line_length > textpos: 488 | col = textpos - current_textpos 489 | break 490 | row += 1 491 | current_textpos += line_length + 1 492 | else: 493 | raise IndexError('The text position "{:d}" is out of range.'.format(textpos)) 494 | 495 | return (row, col) 496 | 497 | 498 | def find_first_line_end(cursor_position, text): 499 | # type: (int, str) -> int 500 | first_line_end = re.match(r".*($)", text[cursor_position:], flags=re.MULTILINE) 501 | assert first_line_end is not None 502 | return first_line_end.end(1) 503 | 504 | 505 | def find_latest_line_start(cursor_position, text): 506 | # type: (int, str) -> int 507 | latest_line_start = re.match(r"(?:.*)(^)", text[: cursor_position + 1], flags=re.MULTILINE | re.DOTALL) 508 | assert latest_line_start is not None 509 | return latest_line_start.start(1) 510 | 511 | 512 | def adjust_text(cursor_position, text, is_forward_motion, motion): 513 | # type: (int, str, bool, str) -> Tuple[str, int] 514 | indices_offset = 0 515 | if is_forward_motion: 516 | if motion in LINEWISE_MOTIONS: 517 | first_line_end_index = find_first_line_end(cursor_position, text) 518 | text = text[cursor_position + first_line_end_index :] 519 | indices_offset = cursor_position + first_line_end_index 520 | else: 521 | # Take one character more at the start to exclude wrong positives for word beginnings 522 | # Pad one character at the end to be compatible with the handling of word endings (see below) 523 | text = text[cursor_position:] + " " 524 | indices_offset = cursor_position 525 | else: 526 | if motion in LINEWISE_MOTIONS: 527 | latest_line_start_index = find_latest_line_start(cursor_position, text) 528 | text = text[:latest_line_start_index] 529 | else: 530 | # Take one character more at the end to exclude wrong positives for word endings 531 | # Pad one character at the start to be compatible with the handling of word beginnings (see above) 532 | text = " " + text[: cursor_position + 1] 533 | indices_offset = -1 534 | return text, indices_offset 535 | 536 | 537 | def motion_to_indices(cursor_position, text, motion, motion_argument): 538 | # type: (int, str, str, Optional[str]) -> Iterable[int] 539 | indices_offset = 0 540 | if motion in FORWARD_MOTIONS and motion in BACKWARD_MOTIONS: 541 | # Split the motion into the forward and backward motion and handle these recursively 542 | forward_motion_indices = motion_to_indices(cursor_position, text, motion + ">", motion_argument) 543 | backward_motion_indices = motion_to_indices(cursor_position, text, motion + "<", motion_argument) 544 | # Create a generator which yields the indices round-robin 545 | indices = ( 546 | index 547 | for index_pair in zip_longest(forward_motion_indices, backward_motion_indices) 548 | for index in index_pair 549 | if index is not None 550 | ) 551 | else: 552 | is_forward_motion = motion in FORWARD_MOTIONS or motion.endswith(">") 553 | if motion.endswith(">") or motion.endswith("<"): 554 | motion = motion[:-1] 555 | text, indices_offset = adjust_text(cursor_position, text, is_forward_motion, motion) 556 | if motion_argument is None: 557 | regex = re.compile(MOTION_TO_REGEX[motion], flags=re.MULTILINE) 558 | else: 559 | regex = re.compile(MOTION_TO_REGEX[motion].format(re.escape(motion_argument)), flags=re.MULTILINE) 560 | matches = regex.finditer(text) 561 | if not is_forward_motion: 562 | matches = reversed(list(matches)) 563 | is_linewise_motion = motion in LINEWISE_MOTIONS 564 | indices = ( 565 | match_obj.start(i) + indices_offset 566 | for match_obj in matches 567 | for i in range(1, regex.groups + 1) 568 | if match_obj.start(i) >= 0 and (is_linewise_motion or (0 < match_obj.start(i) < len(text) - 1)) 569 | ) 570 | return indices 571 | 572 | 573 | def group_indices(indices, group_length): 574 | # type: (Iterable[int], int) -> Union[List[Any], int] 575 | 576 | def group(indices, group_length): 577 | # type: (Iterable[int], int) -> Union[List[Any], int] 578 | def find_required_slot_sizes(num_indices, group_length): 579 | # type: (int, int) -> List[int] 580 | if num_indices <= group_length: 581 | slot_sizes = num_indices * [1] 582 | else: 583 | slot_sizes = group_length * [1] 584 | next_increase_slot = group_length - 1 585 | while sum(slot_sizes) < num_indices: 586 | slot_sizes[next_increase_slot] *= group_length 587 | next_increase_slot = (next_increase_slot - 1 + group_length) % group_length 588 | previous_increase_slot = (next_increase_slot + 1) % group_length 589 | # Always fill rear slots first 590 | slot_sizes[previous_increase_slot] -= sum(slot_sizes) - num_indices 591 | return slot_sizes 592 | 593 | indices_as_tuple = tuple(indices) 594 | num_indices = len(indices_as_tuple) 595 | if num_indices == 1: 596 | return indices_as_tuple[0] 597 | slot_sizes = find_required_slot_sizes(num_indices, group_length) 598 | slot_start_indices = [0] 599 | for slot_size in slot_sizes[:-1]: 600 | slot_start_indices.append(slot_start_indices[-1] + slot_size) 601 | grouped_indices = [ 602 | group(indices_as_tuple[slot_start_index : slot_start_index + slot_size], group_length) 603 | for slot_start_index, slot_size in zip(slot_start_indices, slot_sizes) 604 | ] 605 | return grouped_indices 606 | 607 | grouped_indices = group(indices, group_length) 608 | return grouped_indices 609 | 610 | 611 | def generate_jump_targets(grouped_indices, target_keys): 612 | # type: (Iterable[Any], str) -> Generator[Tuple[int, int, str], None, None] 613 | def find_leaves(group_or_index): 614 | # type: (Union[Iterable[Any], int]) -> Iterator[int] 615 | if isinstance(group_or_index, int): 616 | yield group_or_index 617 | else: 618 | for sub_group_or_index in group_or_index: 619 | for leave in find_leaves(sub_group_or_index): 620 | yield leave 621 | 622 | for target_key, group_or_index in zip(target_keys, grouped_indices): 623 | if isinstance(group_or_index, int): 624 | yield (JumpTarget.DIRECT, group_or_index, target_key) 625 | else: 626 | for preview_key, sub_group_or_index in zip(target_keys, group_or_index): 627 | for leave in find_leaves(sub_group_or_index): 628 | yield (JumpTarget.GROUP, leave, target_key) 629 | yield (JumpTarget.PREVIEW, leave + 1, preview_key) 630 | 631 | 632 | def position_cursor(row, col): 633 | # type: (int, int) -> None 634 | sys.stdout.write(TerminalCodes.POSITION_CURSOR.format(row + 1, col + 1)) 635 | sys.stdout.flush() 636 | 637 | 638 | def print_text(capture_buffer): 639 | # type: (str) -> None 640 | sys.stdout.write(TerminalCodes.CLEAR_SCREEN) 641 | sys.stdout.write(capture_buffer.rstrip()) 642 | sys.stdout.flush() 643 | 644 | 645 | def print_text_with_targets( 646 | capture_buffer, 647 | grouped_indices, 648 | dim_style_code, 649 | highlight_style_code, 650 | highlight_2_first_style_code, 651 | highlight_2_second_style_code, 652 | target_keys, 653 | terminal_width, 654 | ): 655 | # type: (str, Iterable[Any], str, str, str, str, str, int) -> None 656 | target_type_to_color = { 657 | JumpTarget.DIRECT: highlight_style_code, 658 | JumpTarget.GROUP: highlight_2_first_style_code, 659 | JumpTarget.PREVIEW: highlight_2_second_style_code, 660 | } 661 | # First, sort for the text position. If text positions are equal (can happen with preview keys), sort for the 662 | # target type: direct < group < preview 663 | jump_targets = sorted(generate_jump_targets(grouped_indices, target_keys), key=lambda x: (x[1], x[0])) 664 | out_buffer_parts = [] # type: List[str] 665 | previous_text_pos = -1 666 | for target_type, text_pos, target_key in jump_targets: 667 | append_to_buffer = False 668 | append_extra_newline = False 669 | if capture_buffer[text_pos] != "\n": 670 | append_to_buffer = True 671 | else: 672 | # The (preview) target will be printed in an extra column at the line ending 673 | # -> Check if there is one additional column available, otherwise skip this preview 674 | append_extra_newline = True 675 | previous_newline_index = capture_buffer.rfind("\n", text_pos - terminal_width - 1, text_pos) 676 | if previous_newline_index > text_pos - terminal_width - 1: 677 | append_to_buffer = True 678 | if append_to_buffer: 679 | if text_pos > previous_text_pos + 1: 680 | out_buffer_parts.extend( 681 | [dim_style_code, capture_buffer[previous_text_pos + 1 : text_pos], TerminalCodes.Style.RESET] 682 | ) 683 | if text_pos > previous_text_pos: 684 | # Skip targets if they share the same text position, otherwise the text would be shifted on screen. The 685 | # first target on a text position should always be prefered due to the sorting order of target types 686 | # (see comment in line 651) 687 | out_buffer_parts.extend([target_type_to_color[target_type], target_key, TerminalCodes.Style.RESET]) 688 | if append_extra_newline: 689 | out_buffer_parts.append("\n") 690 | previous_text_pos = text_pos 691 | rest_of_capture_buffer = capture_buffer[previous_text_pos + 1 :].rstrip() 692 | if rest_of_capture_buffer: 693 | out_buffer_parts.extend([dim_style_code, rest_of_capture_buffer, TerminalCodes.Style.RESET]) 694 | sys.stdout.write(TerminalCodes.CLEAR_SCREEN) 695 | sys.stdout.write("".join(out_buffer_parts).rstrip()) 696 | sys.stdout.flush() 697 | 698 | 699 | def print_ready(command_pipe): 700 | # type: (IO[str]) -> None 701 | print("ready", file=command_pipe) 702 | command_pipe.flush() 703 | 704 | 705 | def print_single_target(command_pipe): 706 | # type: (IO[str]) -> None 707 | print("single-target", file=command_pipe) 708 | command_pipe.flush() 709 | 710 | 711 | def print_jump_target(row, col, command_pipe): 712 | # type: (int, int, IO[str]) -> None 713 | print("jump {:d}:{:d}".format(row, col), file=command_pipe) 714 | command_pipe.flush() 715 | 716 | 717 | def handle_user_input( 718 | dim_style_code, 719 | highlight_style_code, 720 | highlight_2_first_style_code, 721 | highlight_2_second_style_code, 722 | motion, 723 | motion_argument, 724 | target_keys, 725 | cursor_position_row_col, 726 | pane_size, 727 | capture_buffer_filepath, 728 | command_pipe_filepath, 729 | target_key_pipe_filepath, 730 | ): 731 | # type: (str, str, str, str, str, Optional[str], str, Tuple[int, int], Tuple[int, int], str, str, str) -> None 732 | fd = sys.stdin.fileno() 733 | 734 | def read_capture_buffer(): 735 | # type: () -> str 736 | with codecs.open(capture_buffer_filepath, "r", encoding="utf-8") as f: 737 | capture_buffer = f.read() 738 | return capture_buffer 739 | 740 | def setup_terminal(): 741 | # type: () -> List[Union[int, List[bytes]]] 742 | old_term_settings = termios.tcgetattr(fd) 743 | new_term_settings = termios.tcgetattr(fd) 744 | new_term_settings[3] = ( 745 | cast(int, new_term_settings[3]) & ~termios.ICANON & ~termios.ECHO 746 | ) # unbuffered and no echo 747 | termios.tcsetattr(fd, termios.TCSADRAIN, new_term_settings) 748 | return old_term_settings 749 | 750 | def reset_terminal(old_term_settings): 751 | # type: (List[Union[int, List[bytes]]]) -> None 752 | termios.tcsetattr(fd, termios.TCSADRAIN, old_term_settings) 753 | 754 | old_term_settings = setup_terminal() 755 | 756 | target = None 757 | grouped_indices = None 758 | first_highlight = True 759 | try: 760 | with codecs.open(command_pipe_filepath, "w", encoding="utf-8") as command_pipe: 761 | capture_buffer = read_capture_buffer() 762 | row, col = cursor_position_row_col 763 | pane_width, pane_height = pane_size 764 | cursor_position = convert_row_col_to_text_pos(row, col, capture_buffer) 765 | while True: 766 | if grouped_indices is None: 767 | indices = motion_to_indices(cursor_position, capture_buffer, motion, motion_argument) 768 | grouped_indices = group_indices(indices, len(target_keys)) 769 | else: 770 | try: 771 | # pylint: disable=unsubscriptable-object 772 | grouped_indices = grouped_indices[target_keys.index(target)] 773 | except IndexError: 774 | raise InvalidTargetError('The key "{}" is no valid target.'.format(target)) 775 | if not isinstance(grouped_indices, int): 776 | if not grouped_indices: # if no targets found 777 | break 778 | print_text_with_targets( 779 | capture_buffer, 780 | grouped_indices, 781 | dim_style_code, 782 | highlight_style_code, 783 | highlight_2_first_style_code, 784 | highlight_2_second_style_code, 785 | target_keys, 786 | pane_width, 787 | ) 788 | position_cursor(row, col) 789 | if first_highlight: 790 | print_ready(command_pipe) 791 | first_highlight = False 792 | else: 793 | # The user selected a leave target, we can break now 794 | if first_highlight: 795 | print_single_target(command_pipe) 796 | found_index = grouped_indices 797 | print_jump_target( 798 | *convert_text_pos_to_row_col(found_index, capture_buffer), command_pipe=command_pipe 799 | ) 800 | break 801 | # Reopen the named pipe each time because the open operation blocks till the sender also reopens 802 | # the pipe 803 | with codecs.open(target_key_pipe_filepath, "r", encoding="utf-8") as target_key_pipe: 804 | next_key = target_key_pipe.readline().rstrip("\n\r") 805 | if next_key == "esc": 806 | break 807 | target = next_key 808 | if target not in target_keys: 809 | raise InvalidTargetError('The key "{}" is no valid target.'.format(target)) 810 | finally: 811 | reset_terminal(old_term_settings) 812 | 813 | 814 | def main(): 815 | # type: () -> None 816 | exit_code = 0 817 | try: 818 | ( 819 | dim_style_code, 820 | highlight_style_code, 821 | highlight_2_first_style_code, 822 | highlight_2_second_style_code, 823 | motion, 824 | motion_argument, 825 | target_keys, 826 | cursor_position_row_col, 827 | pane_size, 828 | capture_buffer_filepath, 829 | command_pipe_filepath, 830 | target_key_pipe_filepath, 831 | ) = parse_arguments() 832 | handle_user_input( 833 | dim_style_code, 834 | highlight_style_code, 835 | highlight_2_first_style_code, 836 | highlight_2_second_style_code, 837 | motion, 838 | motion_argument, 839 | target_keys, 840 | cursor_position_row_col, 841 | pane_size, 842 | capture_buffer_filepath, 843 | command_pipe_filepath, 844 | target_key_pipe_filepath, 845 | ) 846 | except ( 847 | MissingDimStyleError, 848 | InvalidDimStyleError, 849 | MissingHighlightStyleError, 850 | InvalidHighlightStyleError, 851 | MissingHighlight2FirstStyleError, 852 | InvalidHighlight2FirstStyleError, 853 | MissingHighlight2SecondStyleError, 854 | InvalidHighlight2SecondStyleError, 855 | MissingMotionError, 856 | InvalidMotionError, 857 | MissingMotionArgumentError, 858 | MissingTargetKeysError, 859 | MissingCursorPositionError, 860 | InvalidCursorPositionError, 861 | MissingPaneSizeError, 862 | InvalidPaneSizeError, 863 | MissingCaptureBufferFilepathError, 864 | MissingJumpCommandPipeFilepathError, 865 | MissingTargetKeyPipeFilepathError, 866 | InvalidTargetError, 867 | ) as e: 868 | display_tmux_message("Error: {}".format(e)) 869 | exit_code = 1 870 | # Wait a moment before returning to Bash to avoid flicker 871 | time.sleep(1) 872 | sys.exit(exit_code) 873 | 874 | 875 | if __name__ == "__main__": 876 | main() 877 | -------------------------------------------------------------------------------- /scripts/easy_motion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | SCRIPTS_DIR="${CURRENT_DIR}" 5 | 6 | CAPTURE_PANE_FILENAME="capture.out" 7 | JUMP_COMMAND_PIPENAME="jump.pipe" 8 | 9 | # shellcheck source=./common_variables.sh 10 | source "${SCRIPTS_DIR}/common_variables.sh" 11 | # shellcheck source=./helpers.sh 12 | source "${SCRIPTS_DIR}/helpers.sh" 13 | # shellcheck source=./scripts/options.sh 14 | source "${SCRIPTS_DIR}/options.sh" 15 | 16 | 17 | easy_motion_create_work_buffer_and_pipe() { 18 | local pane_id 19 | 20 | pane_id="$1" 21 | 22 | if [[ -z "${CAPTURE_TMP_DIRECTORY}" ]]; then 23 | CAPTURE_TMP_DIRECTORY="$(mktemp -d)" || return 24 | 25 | _capture_tmp_directory_cleanup() { 26 | if [[ -n "${CAPTURE_TMP_DIRECTORY}" ]]; then 27 | rm -rf "${CAPTURE_TMP_DIRECTORY}" || return 28 | fi 29 | } 30 | trap _capture_tmp_directory_cleanup EXIT 31 | fi 32 | capture_pane "${pane_id}" "${CAPTURE_TMP_DIRECTORY}/${CAPTURE_PANE_FILENAME}" && \ 33 | chmod 400 "${CAPTURE_TMP_DIRECTORY}/${CAPTURE_PANE_FILENAME}" && \ 34 | mkfifo "${CAPTURE_TMP_DIRECTORY}/${JUMP_COMMAND_PIPENAME}" 35 | } 36 | 37 | easy_motion_setup() { 38 | local session_id window_id pane_id easy_motion_window_and_pane_ids 39 | 40 | session_id="$1" 41 | window_id="$2" 42 | pane_id="$3" 43 | 44 | tmux copy-mode -t "${pane_id}" && \ 45 | EASY_MOTION_CURSOR_POSITION="$(read_cursor_position "${pane_id}")" && \ 46 | EASY_MOTION_PANE_SIZE="$(get_pane_size "${pane_id}")" && \ 47 | EASY_MOTION_ORIGINAL_SESSION_ID="${session_id}" && \ 48 | EASY_MOTION_ORIGINAL_WINDOW_ID="${window_id}" && \ 49 | EASY_MOTION_ORIGINAL_PANE_ID="${pane_id}" && \ 50 | EASY_MOTION_IS_PANE_ZOOMED="$(is_pane_zoomed "${pane_id}" && echo 1 || echo 0)" && \ 51 | easy_motion_create_work_buffer_and_pipe "${pane_id}" && \ 52 | easy_motion_window_and_pane_ids="$(create_empty_swap_pane "${session_id}" "${window_id}" "${pane_id}" "easy-motion")" 53 | EASY_MOTION_WINDOW_ID=$(cut -d: -f1 <<< "${easy_motion_window_and_pane_ids}") && \ 54 | EASY_MOTION_PANE_ID=$(cut -d: -f2 <<< "${easy_motion_window_and_pane_ids}") 55 | EASY_MOTION_PANE_ACTIVE=0 56 | } 57 | 58 | easy_motion_toggle_pane() { 59 | if (( EASY_MOTION_PANE_ACTIVE )); then 60 | if [[ -n "${EASY_MOTION_ORIGINAL_PANE_ID}" ]]; then 61 | tmux set-window-option -t "${EASY_MOTION_ORIGINAL_PANE_ID}" key-table root && \ 62 | tmux switch-client -t "${EASY_MOTION_ORIGINAL_PANE_ID}" -T root && \ 63 | if (( EASY_MOTION_IS_PANE_ZOOMED )); then 64 | swap_window "${EASY_MOTION_ORIGINAL_WINDOW_ID}" "${EASY_MOTION_WINDOW_ID}" || return 65 | else 66 | swap_pane "${EASY_MOTION_ORIGINAL_PANE_ID}" "${EASY_MOTION_PANE_ID}" || return 67 | fi 68 | EASY_MOTION_PANE_ACTIVE=0 69 | fi 70 | else 71 | if [[ -n "${EASY_MOTION_PANE_ID}" ]]; then 72 | tmux set-window-option -t "${EASY_MOTION_PANE_ID}" key-table easy-motion-target && \ 73 | tmux switch-client -t "${EASY_MOTION_PANE_ID}" -T easy-motion-target && \ 74 | if (( EASY_MOTION_IS_PANE_ZOOMED )); then 75 | swap_window "${EASY_MOTION_WINDOW_ID}" "${EASY_MOTION_ORIGINAL_WINDOW_ID}" || return 76 | else 77 | swap_pane "${EASY_MOTION_PANE_ID}" "${EASY_MOTION_ORIGINAL_PANE_ID}" || return 78 | fi 79 | EASY_MOTION_PANE_ACTIVE=1 80 | fi 81 | fi 82 | } 83 | 84 | easy_motion() { 85 | local server_pid session_id window_id pane_id motion motion_argument 86 | local ready_command jump_command jump_cursor_position 87 | local target_key_pipe_tmp_directory 88 | 89 | server_pid="$1" 90 | session_id="$2" 91 | window_id="$3" 92 | pane_id="$4" 93 | motion="$5" 94 | motion_argument="$6" 95 | 96 | # Undo escaping of motion arguments 97 | if [[ "${motion_argument:0:1}" == "\\" ]]; then 98 | motion_argument="${motion_argument:1}" 99 | fi 100 | ensure_target_key_pipe_exists "${server_pid}" "${session_id}" && \ 101 | target_key_pipe_tmp_directory=$(get_target_key_pipe_tmp_directory "${server_pid}" "${session_id}") && \ 102 | if (( EASY_MOTION_VERBOSE )); then 103 | if [[ -z "${motion_argument}" ]]; then 104 | display_message "Showing targets for motion \"${motion}\"." 105 | else 106 | display_message "Showing targets for motion \"${motion}\", motion argument \"${motion_argument}\"." 107 | fi 108 | fi 109 | pane_exec "${EASY_MOTION_PANE_ID}" \ 110 | "${SCRIPTS_DIR}/easy_motion.py" \ 111 | "${EASY_MOTION_DIM_STYLE}" \ 112 | "${EASY_MOTION_HIGHLIGHT_STYLE}" \ 113 | "${EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE}" \ 114 | "${EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE}" \ 115 | "${motion}" \ 116 | "$(escape_double_quotes_and_backticks "${motion_argument}")" \ 117 | "$(escape_double_quotes_and_backticks "${EASY_MOTION_TARGET_KEYS}")" \ 118 | "${EASY_MOTION_CURSOR_POSITION}" \ 119 | "${EASY_MOTION_PANE_SIZE}" \ 120 | "${CAPTURE_TMP_DIRECTORY}/${CAPTURE_PANE_FILENAME}" \ 121 | "${CAPTURE_TMP_DIRECTORY}/${JUMP_COMMAND_PIPENAME}" \ 122 | "${target_key_pipe_tmp_directory}/${TARGET_KEY_PIPENAME}" && \ 123 | { 124 | read -r ready_command && \ 125 | if [[ "${ready_command}" == "ready" ]]; then 126 | easy_motion_toggle_pane || return 127 | elif [[ "${ready_command}" != "single-target" ]]; then 128 | return 1 129 | fi 130 | read -r jump_command && \ 131 | [[ "$(awk '{ print $1 }' <<< "${jump_command}")" == "jump" ]] || return 132 | jump_cursor_position="$(awk '{ print $2 }' <<< "${jump_command}")" && \ 133 | if [[ "${ready_command}" != "single-target" ]]; then 134 | easy_motion_toggle_pane || return 135 | fi 136 | set_cursor_position "${pane_id}" "${jump_cursor_position}" 137 | 138 | if (( EASY_MOTION_AUTO_BEGIN_SELECTION )); then 139 | tmux if -F "#{?selection_present,0,1}" "send-keys -t ${pane_id} -X begin-selection" 140 | fi 141 | } < "${CAPTURE_TMP_DIRECTORY}/${JUMP_COMMAND_PIPENAME}" 142 | } 143 | 144 | easy_motion_cleanup() { 145 | if (( EASY_MOTION_PANE_ACTIVE )); then 146 | easy_motion_toggle_pane 147 | fi 148 | if [[ -n "${EASY_MOTION_WINDOW_ID}" ]]; then 149 | tmux kill-window -t "${EASY_MOTION_WINDOW_ID}" 150 | fi 151 | } 152 | 153 | main() { 154 | local session_id window_id pane_id 155 | session_id="$2" 156 | window_id="$3" 157 | pane_id="$4" 158 | 159 | read_options && \ 160 | easy_motion_setup "${session_id}" "${window_id}" "${pane_id}" && \ 161 | easy_motion "$@" 162 | easy_motion_cleanup 163 | } 164 | 165 | main "$@" 166 | -------------------------------------------------------------------------------- /scripts/helpers.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 3 | SCRIPTS_DIR="${CURRENT_DIR}" 4 | 5 | # shellcheck source=./common_variables.sh 6 | source "${SCRIPTS_DIR}/common_variables.sh" 7 | 8 | get_prefix_enabled_for_key_table() { 9 | case "${key_table}" in 10 | "prefix") 11 | return $(( ! EASY_MOTION_PREFIX_ENABLED )) 12 | ;; 13 | "copy-mode-vi") 14 | return $(( ! EASY_MOTION_COPY_MODE_PREFIX_ENABLED )) 15 | ;; 16 | esac 17 | } 18 | 19 | get_prefix_for_key_table() { 20 | local key_table 21 | key_table="$1" 22 | 23 | case "${key_table}" in 24 | "prefix") 25 | echo "${EASY_MOTION_PREFIX}" 26 | ;; 27 | "copy-mode-vi") 28 | echo "${EASY_MOTION_COPY_MODE_PREFIX}" 29 | ;; 30 | esac 31 | } 32 | 33 | setup_single_key_binding() { 34 | local server_pid key motion key_table 35 | 36 | server_pid="$1" 37 | key="$2" 38 | motion="$3" 39 | 40 | if [[ "${key:0:1}" != "g" ]]; then 41 | key_table="easy-motion" 42 | else 43 | key_table="easy-motion-g" 44 | key="${key:1}" 45 | fi 46 | 47 | [[ "${key}" != "" ]] || return 48 | 49 | case "${key}" in 50 | \;) 51 | key="\\${key}" 52 | ;; 53 | *) 54 | ;; 55 | esac 56 | 57 | tmux bind-key -T "${key_table}" "${key}" run-shell -b \ 58 | "${SCRIPTS_DIR}/easy_motion.sh '${server_pid}' '#{session_id}' '#{window_id}' '#{pane_id}' '${motion}'" 59 | } 60 | 61 | setup_single_key_binding_with_argument() { 62 | local server_pid key motion key_table 63 | 64 | server_pid="$1" 65 | key="$2" 66 | motion="$3" 67 | 68 | if [[ "${key:0:1}" != "g" ]]; then 69 | key_table="easy-motion" 70 | else 71 | key_table="easy-motion-g" 72 | key="${key:1}" 73 | fi 74 | 75 | [[ "${key}" != "" ]] || return 76 | 77 | case "${key}" in 78 | \;) 79 | key="\\${key}" 80 | ;; 81 | *) 82 | ;; 83 | esac 84 | 85 | if [[ "${motion}" != "bd-f2" ]]; then 86 | tmux source - <<-EOF 87 | bind-key -T "${key_table}" "${key}" command-prompt -1 -p "character:" { 88 | set -g @tmp-easy-motion-argument "%%%" 89 | run-shell -b '${SCRIPTS_DIR}/easy_motion.sh "${server_pid}" "\#{session_id}" "#{window_id}" "#{pane_id}" "${motion}" "#{q:@tmp-easy-motion-argument}"' 90 | } 91 | EOF 92 | else 93 | tmux source - <<-EOF 94 | bind-key -T "${key_table}" "${key}" command-prompt -1 -p "character 1:,character 2:" { 95 | set -g @tmp-easy-motion-argument1 "%1" 96 | set -g @tmp-easy-motion-argument2 "%2" 97 | run-shell -b '${SCRIPTS_DIR}/easy_motion.sh "${server_pid}" "\#{session_id}" "#{window_id}" "#{pane_id}" "${motion}" "#{q:@tmp-easy-motion-argument1}#{q:@tmp-easy-motion-argument2}"' 98 | } 99 | EOF 100 | fi 101 | } 102 | 103 | ensure_target_key_pipe_exists() { 104 | local server_pid session_id target_key_pipe_tmp_directory 105 | 106 | server_pid="$1" 107 | session_id="$2" 108 | 109 | target_key_pipe_tmp_directory=$(get_target_key_pipe_tmp_directory "${server_pid}" "${session_id}" create) && \ 110 | if [[ ! -p "${target_key_pipe_tmp_directory}/${TARGET_KEY_PIPENAME}" ]]; then 111 | mkfifo "${target_key_pipe_tmp_directory}/${TARGET_KEY_PIPENAME}" 112 | fi 113 | } 114 | 115 | install_target_key_pipe_cleanup_hook() { 116 | local server_pid 117 | 118 | server_pid="$1" 119 | 120 | # Check if the hook is already installed 121 | if tmux show-hooks -g | grep -q '^session-closed.*tmux-easy-motion'; then 122 | return 0 123 | fi 124 | 125 | tmux set-hook -ga "session-closed" \ 126 | "run-shell 'session_id=\"\#{hook_session}\"; \ 127 | rm -rf \"$(get_target_key_pipe_parent_directory "${server_pid}")/\${session_id:1}\"; \ 128 | rmdir \"$(get_target_key_pipe_parent_directory "${server_pid}")\" 2>/dev/null; \ 129 | true'" # Ignore error codes from `rmdir` if the directory is not empty 130 | } 131 | 132 | get_target_key_pipe_parent_directory() { 133 | local server_pid 134 | 135 | server_pid="$1" 136 | 137 | echo "${TMPDIR}/tmux-easy-motion-target-key-pipe_$(id -un)_${server_pid}" 138 | } 139 | 140 | get_target_key_pipe_tmp_directory() { 141 | local server_pid session_id create parent_dir target_key_pipe_tmp_directory 142 | 143 | server_pid="$1" 144 | session_id="$2" 145 | if [[ "${session_id}" =~ \$(.*) ]]; then 146 | session_id="${BASH_REMATCH[1]}" 147 | fi 148 | if [[ "$3" == "create" ]]; then 149 | create=1 150 | else 151 | create=0 152 | fi 153 | 154 | parent_dir=$(get_target_key_pipe_parent_directory "${server_pid}") 155 | target_key_pipe_tmp_directory="${parent_dir}/${session_id}" 156 | 157 | if (( create )); then 158 | if [[ ! -d "${parent_dir}" ]]; then 159 | mkdir -m 700 -p "${parent_dir}" || return 160 | fi 161 | if [[ ! -d "${target_key_pipe_tmp_directory}" ]]; then 162 | mkdir -m 700 -p "${target_key_pipe_tmp_directory}" || return 163 | fi 164 | fi 165 | 166 | echo "${target_key_pipe_tmp_directory}" 167 | } 168 | 169 | get_tmux_server_pid() { 170 | [[ "${TMUX}" =~ .*,(.*),.* ]] && echo "${BASH_REMATCH[1]}" 171 | } 172 | 173 | escape_double_quotes() { 174 | local unescaped_string 175 | 176 | unescaped_string="$1" 177 | echo "${unescaped_string//\"/\\\"}" 178 | } 179 | 180 | escape_double_quotes_and_backticks() { 181 | local unescaped_string escaped_double_quotes 182 | 183 | unescaped_string="$1" 184 | escaped_double_quotes="$(escape_double_quotes "${unescaped_string}")" 185 | echo "${escaped_double_quotes//\`/\\\`}" 186 | } 187 | 188 | is_tmux_version_greater_or_equal() { 189 | local version 190 | 191 | version="$1" 192 | [[ -n "${version}" ]] || return 193 | [[ "$(echo "$(tmux -V | sed 's/next-//g' | cut -d" " -f2);${version}" | tr ";" "\n" | sort -g -t "." -k 1,1 -k 2,2 | head -1)" == "${version}" ]] 194 | } 195 | 196 | display_message() { 197 | tmux display-message "$1" 198 | } 199 | 200 | assign_tmux_option() { 201 | local variable option default_value option_value ignore_case 202 | 203 | variable="$1" 204 | option="$2" 205 | default_value="$3" 206 | if [[ "$4" == "ignore_case" ]]; then 207 | ignore_case=1 208 | else 209 | ignore_case=0 210 | fi 211 | 212 | if (( ignore_case )); then 213 | option_value="$(tmux show-option -gqv "${option}" | awk '{ print tolower($0) }')" 214 | else 215 | option_value="$(tmux show-option -gqv "${option}")" 216 | fi 217 | if [[ -z "${option_value}" ]]; then 218 | eval "${variable}=\${default_value}" 219 | else 220 | eval "${variable}=\${option_value}" 221 | fi 222 | } 223 | 224 | assign_tmux_bool_option() { 225 | local variable option default_value bool_option_value 226 | 227 | variable="$1" 228 | option="$2" 229 | default_value="$3" 230 | 231 | assign_tmux_option "bool_option_value" "${option}" "${default_value}" "ignore_case" 232 | case "${bool_option_value}" in 233 | 1|activated|enabled|on|true|yes) 234 | eval "${variable}='1'" 235 | ;; 236 | *) 237 | eval "${variable}='0'" 238 | ;; 239 | esac 240 | } 241 | 242 | capture_pane() { 243 | local pane_id capture_filepath 244 | local current_pane_scroll_start current_pane_scroll_end 245 | 246 | pane_id="$1" 247 | capture_filepath="$2" 248 | 249 | IFS=':' read -r current_pane_scroll_start current_pane_scroll_end <<< \ 250 | "$(get_pane_scroll_range "${pane_id}")" 251 | tmux capture-pane -t "${pane_id}" \ 252 | -p \ 253 | -S "${current_pane_scroll_start}" \ 254 | -E "${current_pane_scroll_end}" \ 255 | > "${capture_filepath}" 256 | } 257 | 258 | get_pane_scroll_range() { 259 | local pane_id 260 | local current_pane_scroll_position current_pane_height 261 | 262 | pane_id="$1" 263 | 264 | IFS=':' read -r current_pane_scroll_position current_pane_height <<< \ 265 | "$(tmux display-message -p -t "${pane_id}" "#{scroll_position}:#{pane_height}")" 266 | 267 | echo "$(( - current_pane_scroll_position )):$(( - current_pane_scroll_position + current_pane_height - 1 ))" 268 | } 269 | 270 | get_pane_size() { 271 | local pane_id 272 | 273 | pane_id="$1" 274 | 275 | tmux display-message -p -t "${pane_id}" "#{pane_width}:#{pane_height}" 276 | } 277 | 278 | get_window_size() { 279 | local window_id 280 | 281 | window_id="$1" 282 | 283 | tmux display-message -p -t "${window_id}" "#{window_width}:#{window_height}" 284 | } 285 | 286 | get_window_name() { 287 | local window_id 288 | 289 | window_id="$1" 290 | 291 | tmux display-message -p -t "${window_id}" "#{window_name}" 292 | } 293 | 294 | is_pane_zoomed() { 295 | local pane_id 296 | 297 | pane_id="$1" 298 | 299 | (( $(tmux display-message -p -t "${pane_id}" "#{window_zoomed_flag}") )) 300 | } 301 | 302 | swap_window() { 303 | local target_window_id source_window_id 304 | local target_window_name source_window_name 305 | 306 | target_window_id="$1" 307 | source_window_id="$2" 308 | 309 | target_window_name="$(get_window_name "${target_window_id}")" 310 | source_window_name="$(get_window_name "${source_window_id}")" 311 | 312 | tmux swap-window -s "${source_window_id}" -t "${target_window_id}" && \ 313 | tmux rename-window -t "${target_window_id}" "${source_window_name}" && \ 314 | tmux rename-window -t "${source_window_id}" "${target_window_name}" 315 | } 316 | 317 | swap_pane() { 318 | local target_pane_id source_pane_id 319 | 320 | target_pane_id="$1" 321 | source_pane_id="$2" 322 | 323 | tmux swap-pane -s "${source_pane_id}" -t "${target_pane_id}" 324 | } 325 | 326 | # Based on https://github.com/Morantron/tmux-fingers/blob/1.0.1/scripts/tmux-fingers.sh#L10 327 | create_empty_swap_pane() { 328 | local session_id window_id pane_id name 329 | local swap_window_and_pane_ids swap_window_id swap_pane_id 330 | local current_pane_width current_pane_height 331 | local current_window_width current_window_height 332 | local split_width split_height 333 | 334 | running_shell() { 335 | grep -o "\w*$" <<< "${SHELL}" 336 | } 337 | 338 | init_pane_cmd() { 339 | local init_bash set_env 340 | 341 | init_bash="bash --norc --noprofile" 342 | if [[ $(running_shell) == "fish" ]]; then 343 | set_env="set -x HISTFILE /dev/null; " 344 | else 345 | set_env="HISTFILE=/dev/null " 346 | fi 347 | 348 | echo "${set_env} ${init_bash}" 349 | } 350 | 351 | session_id="$1" 352 | window_id="$2" 353 | pane_id="$3" 354 | name="$4" 355 | 356 | swap_window_and_pane_ids="$(tmux new-window -t "${session_id}" -F "#{window_id}:#{pane_id}" -P -d -n "[${name}]" "$(init_pane_cmd)")" 357 | IFS=':' read -r swap_window_id swap_pane_id <<< "${swap_window_and_pane_ids}" 358 | IFS=':' read -r current_window_width current_window_height <<< "$(get_window_size "${window_id}")" 359 | if is_pane_zoomed "${pane_id}"; then 360 | current_pane_width="${current_window_width}" 361 | current_pane_height="${current_window_height}" 362 | else 363 | IFS=':' read -r current_pane_width current_pane_height <<< "$(get_pane_size "${pane_id}")" 364 | fi 365 | 366 | split_width="$(( current_window_width - current_pane_width - 1 ))" 367 | split_height="$(( current_window_height - current_pane_height - 1 ))" 368 | 369 | if (( split_width >= 0 )); then 370 | tmux split-window -d -t "${swap_pane_id}" -h -l "${split_width}" "/bin/nop" 371 | fi 372 | if (( split_height >= 0 )); then 373 | tmux split-window -d -t "${swap_pane_id}" -l "${split_height}" "/bin/nop" 374 | fi 375 | 376 | echo "${swap_window_id}:${swap_pane_id}" 377 | } 378 | 379 | pane_exec() { 380 | local pane_id pane_command 381 | 382 | pane_id=$1 383 | shift 384 | pane_command="$1" 385 | shift 386 | # Quote arguments 387 | while [[ -n "$*" ]]; do 388 | pane_command="${pane_command} \"$1\"" 389 | shift 390 | done 391 | 392 | tmux send-keys -t "${pane_id}" "${pane_command[@]}" 393 | tmux send-keys -t "${pane_id}" Enter 394 | } 395 | 396 | is_pane_in_copy_mode() { 397 | local pane_id 398 | 399 | pane_id="$1" 400 | 401 | (( $(tmux display-message -p -t "${pane_id}" "#{pane_in_mode}") )) 402 | } 403 | 404 | read_cursor_position() { 405 | local pane_id 406 | 407 | pane_id="$1" 408 | 409 | local cursor_type 410 | 411 | if is_pane_in_copy_mode "${pane_id}"; then 412 | cursor_type="copy_cursor" 413 | else 414 | cursor_type="cursor" 415 | fi 416 | tmux display-message -p -t "${pane_id}" "#{${cursor_type}_y}:#{${cursor_type}_x}" 417 | } 418 | 419 | set_cursor_position() { 420 | local pane_id row_col 421 | local row col current_row current_col rel_row rel_col 422 | 423 | pane_id="$1" 424 | row_col="$2" 425 | 426 | IFS=':' read -r row col <<< "${row_col}" 427 | IFS=':' read -r current_row current_col <<< "$(read_cursor_position "${pane_id}")" 428 | rel_row="$(( row - current_row ))" 429 | tmux copy-mode -t "${pane_id}" 430 | if (( rel_row < 0 )); then 431 | tmux send-keys -t "${pane_id}" -X -N "$(( -rel_row ))" cursor-up 432 | elif (( rel_row > 0 )); then 433 | tmux send-keys -t "${pane_id}" -X -N "$(( rel_row ))" cursor-down 434 | fi 435 | # Reread the cursor position since the colum can change 436 | # while moving the cursor up or down (like in vim). 437 | IFS=':' read -r current_row current_col <<< "$(read_cursor_position "${pane_id}")" 438 | rel_col="$(( col - current_col ))" 439 | if (( rel_col < 0 )); then 440 | tmux send-keys -t "${pane_id}" -X -N "$(( -rel_col ))" cursor-left 441 | elif (( rel_col > 0 )); then 442 | tmux send-keys -t "${pane_id}" -X -N "$(( rel_col ))" cursor-right 443 | fi 444 | } 445 | -------------------------------------------------------------------------------- /scripts/options.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | SCRIPTS_DIR="${CURRENT_DIR}" 5 | 6 | # shellcheck source=./helpers.sh 7 | source "${SCRIPTS_DIR}/helpers.sh" 8 | 9 | 10 | # Default settings 11 | EASY_MOTION_PREFIX_ENABLED_DEFAULT=1 12 | EASY_MOTION_PREFIX_DEFAULT="Space" 13 | EASY_MOTION_COPY_MODE_PREFIX_ENABLED_DEFAULT=1 14 | EASY_MOTION_DIM_STYLE_DEFAULT="fg=colour242" 15 | EASY_MOTION_HIGHLIGHT_STYLE_DEFAULT="fg=colour196,bold" 16 | EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE_DEFAULT="fg=brightyellow,bold" 17 | EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE_DEFAULT="fg=yellow,bold" 18 | EASY_MOTION_TARGET_KEYS_DEFAULT="asdghklqwertyuiopzxcvbnmfj;" 19 | EASY_MOTION_VERBOSE_DEFAULT=0 20 | EASY_MOTION_DEFAULT_KEY_BINDINGS_DEFAULT=1 21 | EASY_MOTION_DEFAULT_MOTION_DEFAULT="" 22 | EASY_MOTION_AUTO_BEGIN_SELECTION_DEFAULT=0 23 | # --- key bindings 24 | EASY_MOTION_BINDING_B_DEFAULT="b" 25 | EASY_MOTION_BINDING_CAPITAL_B_DEFAULT="B" 26 | EASY_MOTION_BINDING_GE_DEFAULT="ge" 27 | EASY_MOTION_BINDING_CAPITAL_GE_DEFAULT="gE" 28 | EASY_MOTION_BINDING_E_DEFAULT="e" 29 | EASY_MOTION_BINDING_CAPITAL_E_DEFAULT="E" 30 | EASY_MOTION_BINDING_W_DEFAULT="w" 31 | EASY_MOTION_BINDING_CAPITAL_W_DEFAULT="W" 32 | EASY_MOTION_BINDING_J_DEFAULT="j" 33 | EASY_MOTION_BINDING_CAPITAL_J_DEFAULT="J" 34 | EASY_MOTION_BINDING_K_DEFAULT="k" 35 | EASY_MOTION_BINDING_CAPITAL_K_DEFAULT="K" 36 | EASY_MOTION_BINDING_F_DEFAULT="f" 37 | EASY_MOTION_BINDING_CAPITAL_F_DEFAULT="F" 38 | EASY_MOTION_BINDING_T_DEFAULT="t" 39 | EASY_MOTION_BINDING_CAPITAL_T_DEFAULT="T" 40 | EASY_MOTION_BINDING_BD_W_DEFAULT="" 41 | EASY_MOTION_BINDING_CAPITAL_BD_W_DEFAULT="" 42 | EASY_MOTION_BINDING_BD_E_DEFAULT="" 43 | EASY_MOTION_BINDING_CAPITAL_BD_E_DEFAULT="" 44 | EASY_MOTION_BINDING_BD_J_DEFAULT="" 45 | EASY_MOTION_BINDING_CAPITAL_BD_J_DEFAULT="" 46 | EASY_MOTION_BINDING_BD_F_DEFAULT="s" 47 | EASY_MOTION_BINDING_BD_F2_DEFAULT="" 48 | EASY_MOTION_BINDING_BD_T_DEFAULT="" 49 | EASY_MOTION_BINDING_CAPITAL_BD_T_DEFAULT="" 50 | EASY_MOTION_BINDING_C_DEFAULT="c" 51 | 52 | # Option names 53 | EASY_MOTION_PREFIX_ENABLED_OPTION="@easy-motion-prefix-enabled" 54 | EASY_MOTION_PREFIX_OPTION="@easy-motion-prefix" 55 | EASY_MOTION_COPY_MODE_PREFIX_ENABLED_OPTION="@easy-motion-copy-mode-prefix-enabled" 56 | EASY_MOTION_COPY_MODE_PREFIX_OPTION="@easy-motion-copy-mode-prefix" 57 | EASY_MOTION_DIM_STYLE_OPTION="@easy-motion-dim-style" 58 | EASY_MOTION_HIGHLIGHT_STYLE_OPTION="@easy-motion-highlight-style" 59 | EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE_OPTION="@easy-motion-highlight-2-first-style" 60 | EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE_OPTION="@easy-motion-highlight-2-second-style" 61 | EASY_MOTION_TARGET_KEYS_OPTION="@easy-motion-target-keys" 62 | EASY_MOTION_VERBOSE_OPTION="@easy-motion-verbose" 63 | EASY_MOTION_DEFAULT_KEY_BINDINGS_OPTION="@easy-motion-default-key-bindings" 64 | EASY_MOTION_DEFAULT_MOTION_OPTION="@easy-motion-default-motion" 65 | EASY_MOTION_AUTO_BEGIN_SELECTION_OPTION="@easy-motion-auto-begin-selection" 66 | # --- key bindings 67 | EASY_MOTION_BINDING_B_OPTION="@easy-motion-binding-b" 68 | EASY_MOTION_BINDING_CAPITAL_B_OPTION="@easy-motion-binding-B" 69 | EASY_MOTION_BINDING_GE_OPTION="@easy-motion-binding-ge" 70 | EASY_MOTION_BINDING_CAPITAL_GE_OPTION="@easy-motion-binding-gE" 71 | EASY_MOTION_BINDING_E_OPTION="@easy-motion-binding-e" 72 | EASY_MOTION_BINDING_CAPITAL_E_OPTION="@easy-motion-binding-E" 73 | EASY_MOTION_BINDING_W_OPTION="@easy-motion-binding-w" 74 | EASY_MOTION_BINDING_CAPITAL_W_OPTION="@easy-motion-binding-W" 75 | EASY_MOTION_BINDING_J_OPTION="@easy-motion-binding-j" 76 | EASY_MOTION_BINDING_CAPITAL_J_OPTION="@easy-motion-binding-J" # end of line 77 | EASY_MOTION_BINDING_K_OPTION="@easy-motion-binding-k" 78 | EASY_MOTION_BINDING_CAPITAL_K_OPTION="@easy-motion-binding-K" # end of line 79 | EASY_MOTION_BINDING_F_OPTION="@easy-motion-binding-f" 80 | EASY_MOTION_BINDING_CAPITAL_F_OPTION="@easy-motion-binding-F" 81 | EASY_MOTION_BINDING_T_OPTION="@easy-motion-binding-t" 82 | EASY_MOTION_BINDING_CAPITAL_T_OPTION="@easy-motion-binding-T" 83 | EASY_MOTION_BINDING_BD_W_OPTION="@easy-motion-binding-bd-w" # bd -> bidirectional 84 | EASY_MOTION_BINDING_CAPITAL_BD_W_OPTION="@easy-motion-binding-bd-W" 85 | EASY_MOTION_BINDING_BD_E_OPTION="@easy-motion-binding-bd-e" 86 | EASY_MOTION_BINDING_CAPITAL_BD_E_OPTION="@easy-motion-binding-bd-E" 87 | EASY_MOTION_BINDING_BD_J_OPTION="@easy-motion-binding-bd-j" 88 | EASY_MOTION_BINDING_CAPITAL_BD_J_OPTION="@easy-motion-binding-bd-J" 89 | EASY_MOTION_BINDING_BD_F_OPTION="@easy-motion-binding-bd-f" 90 | EASY_MOTION_BINDING_BD_F2_OPTION="@easy-motion-binding-bd-f2" 91 | EASY_MOTION_BINDING_BD_T_OPTION="@easy-motion-binding-bd-t" 92 | EASY_MOTION_BINDING_CAPITAL_BD_T_OPTION="@easy-motion-binding-bd-T" 93 | EASY_MOTION_BINDING_C_OPTION="@easy-motion-binding-c" # camelCase or underscore notation 94 | 95 | 96 | read_options() { 97 | # shellcheck disable=SC2034 98 | assign_tmux_bool_option "EASY_MOTION_PREFIX_ENABLED" \ 99 | "${EASY_MOTION_PREFIX_ENABLED_OPTION}" \ 100 | "${EASY_MOTION_PREFIX_ENABLED_DEFAULT}" && \ 101 | assign_tmux_option "EASY_MOTION_PREFIX" \ 102 | "${EASY_MOTION_PREFIX_OPTION}" \ 103 | "${EASY_MOTION_PREFIX_DEFAULT}" && \ 104 | assign_tmux_bool_option "EASY_MOTION_COPY_MODE_PREFIX_ENABLED" \ 105 | "${EASY_MOTION_COPY_MODE_PREFIX_ENABLED_OPTION}" \ 106 | "${EASY_MOTION_COPY_MODE_PREFIX_ENABLED_DEFAULT}" && \ 107 | assign_tmux_option "EASY_MOTION_COPY_MODE_PREFIX" \ 108 | "${EASY_MOTION_COPY_MODE_PREFIX_OPTION}" \ 109 | "${EASY_MOTION_PREFIX}" && \ 110 | assign_tmux_option "EASY_MOTION_DIM_STYLE" \ 111 | "${EASY_MOTION_DIM_STYLE_OPTION}" \ 112 | "${EASY_MOTION_DIM_STYLE_DEFAULT}" && \ 113 | assign_tmux_option "EASY_MOTION_HIGHLIGHT_STYLE" \ 114 | "${EASY_MOTION_HIGHLIGHT_STYLE_OPTION}" \ 115 | "${EASY_MOTION_HIGHLIGHT_STYLE_DEFAULT}" && \ 116 | assign_tmux_option "EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE" \ 117 | "${EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE_OPTION}" \ 118 | "${EASY_MOTION_HIGHLIGHT_2_FIRST_STYLE_DEFAULT}" && \ 119 | assign_tmux_option "EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE" \ 120 | "${EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE_OPTION}" \ 121 | "${EASY_MOTION_HIGHLIGHT_2_SECOND_STYLE_DEFAULT}" && \ 122 | assign_tmux_option "EASY_MOTION_TARGET_KEYS" \ 123 | "${EASY_MOTION_TARGET_KEYS_OPTION}" \ 124 | "${EASY_MOTION_TARGET_KEYS_DEFAULT}" && \ 125 | assign_tmux_bool_option "EASY_MOTION_VERBOSE" \ 126 | "${EASY_MOTION_VERBOSE_OPTION}" \ 127 | "${EASY_MOTION_VERBOSE_DEFAULT}" && \ 128 | assign_tmux_bool_option "EASY_MOTION_DEFAULT_KEY_BINDINGS" \ 129 | "${EASY_MOTION_DEFAULT_KEY_BINDINGS_OPTION}" \ 130 | "${EASY_MOTION_DEFAULT_KEY_BINDINGS_DEFAULT}" && \ 131 | assign_tmux_option "EASY_MOTION_DEFAULT_MOTION" \ 132 | "${EASY_MOTION_DEFAULT_MOTION_OPTION}" \ 133 | "${EASY_MOTION_DEFAULT_MOTION_DEFAULT}" || return 134 | assign_tmux_bool_option "EASY_MOTION_AUTO_BEGIN_SELECTION" \ 135 | "${EASY_MOTION_AUTO_BEGIN_SELECTION_OPTION}" \ 136 | "${EASY_MOTION_AUTO_BEGIN_SELECTION_DEFAULT}" || return 137 | 138 | # key bindings 139 | # shellcheck disable=SC2034 140 | if [[ -z "${EASY_MOTION_DEFAULT_MOTION}" ]]; then 141 | if (( EASY_MOTION_DEFAULT_KEY_BINDINGS )); then 142 | assign_tmux_option "EASY_MOTION_BINDING_B" \ 143 | "${EASY_MOTION_BINDING_B_OPTION}" \ 144 | "${EASY_MOTION_BINDING_B_DEFAULT}" && \ 145 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_B" \ 146 | "${EASY_MOTION_BINDING_CAPITAL_B_OPTION}" \ 147 | "${EASY_MOTION_BINDING_CAPITAL_B_DEFAULT}" && \ 148 | assign_tmux_option "EASY_MOTION_BINDING_GE" \ 149 | "${EASY_MOTION_BINDING_GE_OPTION}" \ 150 | "${EASY_MOTION_BINDING_GE_DEFAULT}" && \ 151 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_GE" \ 152 | "${EASY_MOTION_BINDING_CAPITAL_GE_OPTION}" \ 153 | "${EASY_MOTION_BINDING_CAPITAL_GE_DEFAULT}" && \ 154 | assign_tmux_option "EASY_MOTION_BINDING_E" \ 155 | "${EASY_MOTION_BINDING_E_OPTION}" \ 156 | "${EASY_MOTION_BINDING_E_DEFAULT}" && \ 157 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_E" \ 158 | "${EASY_MOTION_BINDING_CAPITAL_E_OPTION}" \ 159 | "${EASY_MOTION_BINDING_CAPITAL_E_DEFAULT}" && \ 160 | assign_tmux_option "EASY_MOTION_BINDING_W" \ 161 | "${EASY_MOTION_BINDING_W_OPTION}" \ 162 | "${EASY_MOTION_BINDING_W_DEFAULT}" && \ 163 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_W" \ 164 | "${EASY_MOTION_BINDING_CAPITAL_W_OPTION}" \ 165 | "${EASY_MOTION_BINDING_CAPITAL_W_DEFAULT}" && \ 166 | assign_tmux_option "EASY_MOTION_BINDING_J" \ 167 | "${EASY_MOTION_BINDING_J_OPTION}" \ 168 | "${EASY_MOTION_BINDING_J_DEFAULT}" && \ 169 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_J" \ 170 | "${EASY_MOTION_BINDING_CAPITAL_J_OPTION}" \ 171 | "${EASY_MOTION_BINDING_CAPITAL_J_DEFAULT}" && \ 172 | assign_tmux_option "EASY_MOTION_BINDING_K" \ 173 | "${EASY_MOTION_BINDING_K_OPTION}" \ 174 | "${EASY_MOTION_BINDING_K_DEFAULT}" && \ 175 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_K" \ 176 | "${EASY_MOTION_BINDING_CAPITAL_K_OPTION}" \ 177 | "${EASY_MOTION_BINDING_CAPITAL_K_DEFAULT}" && \ 178 | assign_tmux_option "EASY_MOTION_BINDING_F" \ 179 | "${EASY_MOTION_BINDING_F_OPTION}" \ 180 | "${EASY_MOTION_BINDING_F_DEFAULT}" && \ 181 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_F" \ 182 | "${EASY_MOTION_BINDING_CAPITAL_F_OPTION}" \ 183 | "${EASY_MOTION_BINDING_CAPITAL_F_DEFAULT}" && \ 184 | assign_tmux_option "EASY_MOTION_BINDING_T" \ 185 | "${EASY_MOTION_BINDING_T_OPTION}" \ 186 | "${EASY_MOTION_BINDING_T_DEFAULT}" && \ 187 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_T" \ 188 | "${EASY_MOTION_BINDING_CAPITAL_T_OPTION}" \ 189 | "${EASY_MOTION_BINDING_CAPITAL_T_DEFAULT}" && \ 190 | assign_tmux_option "EASY_MOTION_BINDING_BD_W" \ 191 | "${EASY_MOTION_BINDING_BD_W_OPTION}" \ 192 | "${EASY_MOTION_BINDING_BD_W_DEFAULT}" && \ 193 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_W" \ 194 | "${EASY_MOTION_BINDING_CAPITAL_BD_W_OPTION}" \ 195 | "${EASY_MOTION_BINDING_CAPITAL_BD_W_DEFAULT}" && \ 196 | assign_tmux_option "EASY_MOTION_BINDING_BD_E" \ 197 | "${EASY_MOTION_BINDING_BD_E_OPTION}" \ 198 | "${EASY_MOTION_BINDING_BD_E_DEFAULT}" && \ 199 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_E" \ 200 | "${EASY_MOTION_BINDING_CAPITAL_BD_E_OPTION}" \ 201 | "${EASY_MOTION_BINDING_CAPITAL_BD_E_DEFAULT}" && \ 202 | assign_tmux_option "EASY_MOTION_BINDING_BD_J" \ 203 | "${EASY_MOTION_BINDING_BD_J_OPTION}" \ 204 | "${EASY_MOTION_BINDING_BD_J_DEFAULT}" && \ 205 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_J" \ 206 | "${EASY_MOTION_BINDING_CAPITAL_BD_J_OPTION}" \ 207 | "${EASY_MOTION_BINDING_CAPITAL_BD_J_DEFAULT}" && \ 208 | assign_tmux_option "EASY_MOTION_BINDING_BD_F" \ 209 | "${EASY_MOTION_BINDING_BD_F_OPTION}" \ 210 | "${EASY_MOTION_BINDING_BD_F_DEFAULT}" && \ 211 | assign_tmux_option "EASY_MOTION_BINDING_BD_F2" \ 212 | "${EASY_MOTION_BINDING_BD_F2_OPTION}" \ 213 | "${EASY_MOTION_BINDING_BD_F2_DEFAULT}" && \ 214 | assign_tmux_option "EASY_MOTION_BINDING_BD_T" \ 215 | "${EASY_MOTION_BINDING_BD_T_OPTION}" \ 216 | "${EASY_MOTION_BINDING_BD_T_DEFAULT}" && \ 217 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_T" \ 218 | "${EASY_MOTION_BINDING_CAPITAL_BD_T_OPTION}" \ 219 | "${EASY_MOTION_BINDING_CAPITAL_BD_T_DEFAULT}" && \ 220 | assign_tmux_option "EASY_MOTION_BINDING_C" \ 221 | "${EASY_MOTION_BINDING_C_OPTION}" \ 222 | "${EASY_MOTION_BINDING_C_DEFAULT}" 223 | else 224 | assign_tmux_option "EASY_MOTION_BINDING_B" \ 225 | "${EASY_MOTION_BINDING_B_OPTION}" \ 226 | "" && \ 227 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_B" \ 228 | "${EASY_MOTION_BINDING_CAPITAL_B_OPTION}" \ 229 | "" && \ 230 | assign_tmux_option "EASY_MOTION_BINDING_GE" \ 231 | "${EASY_MOTION_BINDING_GE_OPTION}" \ 232 | "" && \ 233 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_GE" \ 234 | "${EASY_MOTION_BINDING_CAPITAL_GE_OPTION}" \ 235 | "" && \ 236 | assign_tmux_option "EASY_MOTION_BINDING_E" \ 237 | "${EASY_MOTION_BINDING_E_OPTION}" \ 238 | "" && \ 239 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_E" \ 240 | "${EASY_MOTION_BINDING_CAPITAL_E_OPTION}" \ 241 | "" && \ 242 | assign_tmux_option "EASY_MOTION_BINDING_W" \ 243 | "${EASY_MOTION_BINDING_W_OPTION}" \ 244 | "" && \ 245 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_W" \ 246 | "${EASY_MOTION_BINDING_CAPITAL_W_OPTION}" \ 247 | "" && \ 248 | assign_tmux_option "EASY_MOTION_BINDING_J" \ 249 | "${EASY_MOTION_BINDING_J_OPTION}" \ 250 | "" && \ 251 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_J" \ 252 | "${EASY_MOTION_BINDING_CAPITAL_J_OPTION}" \ 253 | "" && \ 254 | assign_tmux_option "EASY_MOTION_BINDING_K" \ 255 | "${EASY_MOTION_BINDING_K_OPTION}" \ 256 | "" && \ 257 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_K" \ 258 | "${EASY_MOTION_BINDING_CAPITAL_K_OPTION}" \ 259 | "" && \ 260 | assign_tmux_option "EASY_MOTION_BINDING_F" \ 261 | "${EASY_MOTION_BINDING_F_OPTION}" \ 262 | "" && \ 263 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_F" \ 264 | "${EASY_MOTION_BINDING_CAPITAL_F_OPTION}" \ 265 | "" && \ 266 | assign_tmux_option "EASY_MOTION_BINDING_T" \ 267 | "${EASY_MOTION_BINDING_T_OPTION}" \ 268 | "" && \ 269 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_T" \ 270 | "${EASY_MOTION_BINDING_CAPITAL_T_OPTION}" \ 271 | "" && \ 272 | assign_tmux_option "EASY_MOTION_BINDING_BD_W" \ 273 | "${EASY_MOTION_BINDING_BD_W_OPTION}" \ 274 | "" && \ 275 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_W" \ 276 | "${EASY_MOTION_BINDING_CAPITAL_BD_W_OPTION}" \ 277 | "" && \ 278 | assign_tmux_option "EASY_MOTION_BINDING_BD_E" \ 279 | "${EASY_MOTION_BINDING_BD_E_OPTION}" \ 280 | "" && \ 281 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_E" \ 282 | "${EASY_MOTION_BINDING_CAPITAL_BD_E_OPTION}" \ 283 | "" && \ 284 | assign_tmux_option "EASY_MOTION_BINDING_BD_J" \ 285 | "${EASY_MOTION_BINDING_BD_J_OPTION}" \ 286 | "" && \ 287 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_J" \ 288 | "${EASY_MOTION_BINDING_CAPITAL_BD_J_OPTION}" \ 289 | "" && \ 290 | assign_tmux_option "EASY_MOTION_BINDING_BD_F" \ 291 | "${EASY_MOTION_BINDING_BD_F_OPTION}" \ 292 | "" && \ 293 | assign_tmux_option "EASY_MOTION_BINDING_BD_F2" \ 294 | "${EASY_MOTION_BINDING_BD_F2_OPTION}" \ 295 | "" && \ 296 | assign_tmux_option "EASY_MOTION_BINDING_BD_T" \ 297 | "${EASY_MOTION_BINDING_BD_T_OPTION}" \ 298 | "" && \ 299 | assign_tmux_option "EASY_MOTION_BINDING_CAPITAL_BD_T" \ 300 | "${EASY_MOTION_BINDING_CAPITAL_BD_T_OPTION}" \ 301 | "" && \ 302 | assign_tmux_option "EASY_MOTION_BINDING_C" \ 303 | "${EASY_MOTION_BINDING_C_OPTION}" \ 304 | "" 305 | fi 306 | fi 307 | } 308 | -------------------------------------------------------------------------------- /scripts/pipe_target_key.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | SCRIPTS_DIR="${CURRENT_DIR}" 5 | 6 | # shellcheck source=./common_variables.sh 7 | source "${SCRIPTS_DIR}/common_variables.sh" 8 | # shellcheck source=./helpers.sh 9 | source "${SCRIPTS_DIR}/helpers.sh" 10 | 11 | write_target_key() { 12 | local server_pid session_id target_key target_key_pipe_tmp_directory 13 | 14 | server_pid="$1" 15 | session_id="$2" 16 | target_key="$3" 17 | target_key_pipe_tmp_directory=$(get_target_key_pipe_tmp_directory "${server_pid}" "${session_id}") 18 | 19 | echo "${target_key}" >> "${target_key_pipe_tmp_directory}/${TARGET_KEY_PIPENAME}" 20 | } 21 | 22 | main() { 23 | local server_pid session_id parent_directory 24 | server_pid="$1" 25 | session_id="$2" 26 | 27 | ensure_target_key_pipe_exists "${server_pid}" "${session_id}" && \ 28 | write_target_key "$@" 29 | } 30 | 31 | main "$@" 32 | --------------------------------------------------------------------------------