├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE └── workflows │ └── deployment.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── dev ├── benchmark.cr └── release.cr ├── docs └── migrating-from-1.md ├── install-wizard.sh ├── kill-windows.sh ├── logo.svg ├── shard.lock ├── shard.yml ├── spec ├── .tmuxomatic_unlock_command_prompt ├── conf │ ├── alt-action.conf │ ├── basic.conf │ ├── benchmark.conf │ ├── ctrl-action.conf │ ├── custom-bindings.conf │ ├── custom-patterns.conf │ ├── dev.conf │ ├── invalid.conf │ └── quotes.conf ├── fill_screen.cr ├── fixtures │ ├── benchmark │ ├── custom-patterns │ ├── grep-output │ ├── ip-output │ ├── line_jump_fixture │ └── quotes ├── install-tmux-versions.sh ├── lib │ ├── fingers │ │ ├── hinter_spec.cr │ │ ├── input_socket_spec.cr │ │ └── match_formatter_spec.cr │ ├── huffman_spec.cr │ ├── patterns_spec.cr │ ├── priority_queue_spec.cr │ ├── tmux_format_printer_spec.rb │ └── tmux_style_printer_spec.cr ├── provisioning │ ├── ci.sh │ ├── osx.sh │ └── ubuntu.sh ├── spec_helper.cr ├── stubs │ └── action-stub.sh ├── tmux_spec.cr └── use-tmux.sh ├── src ├── fingers.cr ├── fingers │ ├── action_runner.cr │ ├── cli.cr │ ├── commands │ │ ├── base.cr │ │ ├── info.cr │ │ ├── load_config.cr │ │ ├── send_input.cr │ │ ├── start.cr │ │ └── version.cr │ ├── config.cr │ ├── dirs.cr │ ├── hinter.cr │ ├── input_socket.cr │ ├── logger.cr │ ├── match_formatter.cr │ ├── state.cr │ ├── types.cr │ └── view.cr ├── huffman.cr ├── priority_queue.cr ├── tmux.cr └── tmux_style_printer.cr └── tmux-fingers.tmux /.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Description 6 | 7 | 8 | 9 | ### tmux-fingers info output 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | dist_linux: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: crystallang/crystal:1.14-alpine 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Update Libs 16 | run: apk add --update --upgrade --no-cache --force-overwrite libxml2-dev yaml-dev yaml-static 17 | - name: Build 18 | run: shards build --production --release --static --no-debug 19 | env: 20 | WIZARD_INSTALLATION_METHOD: download-binary 21 | - name: Upload 22 | uses: actions/upload-release-asset@v1.0.1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | upload_url: ${{ github.event.release.upload_url }} 27 | asset_path: ./bin/tmux-fingers 28 | asset_name: tmux-fingers-${{ github.event.release.tag_name }}-linux-x86_64 29 | asset_content_type: binary/octet-stream 30 | dist_homebrew: 31 | name: Bump Homebrew formula 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: mislav/bump-homebrew-formula-action@v2 35 | with: 36 | formula-name: tmux-fingers 37 | homebrew-tap: morantron/homebrew-tmux-fingers 38 | env: 39 | COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules/ 3 | .vagrant/ 4 | .gawk* 5 | .bundle 6 | vendor/bundle 7 | .cache 8 | .byebug_history 9 | shared 10 | bin/* 11 | /lib 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.4.1 - 18 May 2025 2 | 3 | * Fix active pane restore when passing a format as target. 4 | 5 | ## 2.4.0 - 04 Apr 2025 6 | 7 | * Add support to tmux target-pane tokens, which allows to target other panes 8 | rather than the active one like: adjacent panes, previous pane, etc... ( Fixes #138 ) 9 | 10 | ## 2.3.3 - 06 Feb 2025 11 | 12 | * Fix :paste: action again, this time for real. Working in copy mode and non-copy mode. ( Fixes #137 ) 13 | 14 | ## 2.3.2 - 02 Feb 2025 15 | 16 | * :paste: will now exit copy mode right before pasting. 17 | * Documentation improvements. 18 | * Properly handle single quotes in url pattern. 19 | 20 | ## 2.3.1 - 10 Dec 2024 21 | 22 | * Fix :paste: action. 23 | 24 | ## 2.3.0 - 09 Dec 2024 25 | 26 | * Added --(main|ctrl|alt|shift)-action cli options. 27 | * Add info command to show information. 28 | * Improved git-status builtin pattern 29 | * Fixed root key table not being properly restored ( Fixes #130 ) 30 | * Updated to Crystal 1.14. 31 | * Various perfomance improvements (from 50ms to 40ms on my machine :tm:) 32 | * Improved benchmark and development scripts. 33 | 34 | ## 2.2.2 - 14 Aug 2024 35 | 36 | * Fixed error in multi-user environments. 37 | * Added XDG dirs, logs are now in ~/.local/state/tmux-fingers/fingers.log instead of /tmp/fingers.log. 38 | * Action stderr is now logged to ~/.local/state/tmux-fingers/action-stderr 39 | 40 | ## 2.2.1 - 29 Jul 2024 41 | 42 | * Tweaks to version detection logic ( Fixes #125 ). 43 | 44 | ## 2.2.0 - 21 Jul 2024 45 | 46 | * Add new option @fingers-enabled-builtin-patterns ( Fixes #19 ). 47 | * New way of setting up tmux-fingers bindings, with command line option to allow using only specific patterns (built-in or custom) ( Fixes #117 ) 48 | * Fix issue where zoom panes would be unzoomed when closing ( Fixes #123 ). 49 | * Open tmux-fingers in same path as target pane path ( Fixes #120 ). 50 | 51 | ## 2.1.5 - 02 May 2024 52 | 53 | * User defined patterns now take precedence over built-in ones. 54 | * Fix blank screen under certain circumstances due to incorrect handling of capture groups. 55 | 56 | ## 2.1.4 - 08 Mar 2024 57 | 58 | * Fixed "No last pane" error when using "tmux last-pane" ( Fixes #48 ) 59 | 60 | ## 2.1.3 - 31 Jan 2024 61 | 62 | * Fix "Too many matches" exception ( fixes #112 ). 63 | 64 | ## 2.1.2 - 19 Jan 2024 65 | 66 | * Added termux support. 67 | * Fixes to Fingers::Dirs to remove hardcoded paths. Default log path is now /tmp/fingers.log. 68 | * Updated Crystal version to generate clean ELF executables. 69 | * Improve exception handling when rendering. 70 | 71 | ## 2.1.1 - 16 Nov 2023 72 | 73 | * Fix copy/jump when using special named capture group "match". 74 | 75 | ## 2.1.0 - 10 Nov 2023 76 | 77 | * Added new jump functionality. 78 | * Improved visual feedback by discarding unreachable highlights. 79 | 80 | ## 2.0.6 - 26 Oct 2023 81 | 82 | * Added new option @fingers-show-copied-notification ( fixes #104 ). 83 | * Fix problem expanding paths in Mac OS ( thanks @brttbndr ! ). 84 | * Fix hints using disallowed characters like "q" ( fixes #105 ). 85 | * Improve performance in hint generation. 86 | 87 | ## 2.0.5 - 06 Oct 2023 88 | 89 | * Fix git/binary version mismatch again ( fixes #103 ). 90 | 91 | ## 2.0.4 - 05 Oct 2023 92 | 93 | * Fix issues when using backquote as tmux prefix ( fixes #102 ). 94 | 95 | ## 2.0.3 - 29 Sep 2023 96 | 97 | * Fix git/binary version mismatch by publishing a new version ( fixes #101 ). 98 | 99 | ## 2.0.2 - 28 Sep 2023 100 | 101 | * Fix `prefix2` being lost after exiting fingers mode ( fixes #100 ). 102 | 103 | ## 2.0.1 - 27 Sep 2023 104 | 105 | * Fix brew installation method and display load-config errors. 106 | 107 | ## 2.0.0 - 27 Sep 2023 108 | 109 | * Code rewritten in [Crystal language](https://crystal-lang.org/). 110 | * Greatly improved performance. 111 | * Switched regex syntax from ERE to PCRE. 112 | * Deprecated `@fingers-compact-hints` and all `@fingers-*-format-nocompact` format options. 113 | * Deprecated all `@fingers-*-format` options in favour of their `@fingers-*-style` counterparts. 114 | * Added new `@fingers-backdrop-style` option that allows you to customize all the background text that is not highlighted by the plugin. More info in `docs/migrating-from-1.md`. 115 | * Patterns can now define a named capture to only highlight a part of the match. 116 | * Added new built-in patterns. 117 | 118 | ## 1.1.3 - 27 Sep 2023 119 | 120 | * Removed unused .cache folder creation ( fixes #98 ) 121 | * Clarified regexp syntax in README ( thanks @ilyagr ! ) 122 | 123 | ## 1.1.2 - 05 May 2023 124 | 125 | * Fix escaping issue with upcoming tmux 3.4 ( fixes #95 ) 126 | 127 | ## 1.1.1 - 16 Nov 2020 128 | 129 | * Don't allow patterns matching empty string ( fixes #86 ) 130 | * In health-check, suggest to reload tmux.conf when gawk is not found ( fixes #89 ) 131 | 132 | ## 1.1.0 - 06 Mar 2020 133 | 134 | * Extended default SHA pattern to match up to 128 digits ( fixes #73 ) 135 | 136 | ## 1.0.1 - 05 Jan 2020 137 | 138 | * Fix default open command discovery ( fixes #70 ) 139 | 140 | ## 1.0.0 - 05 Jan 2020 141 | 142 | * Added @fingers-keyboard-layout option which allows to customize which letters are used when highlighting matches. Designed to reduce finger movement IRL :tm:. ( fixes #16 ) 143 | * Added @fingers-ctrl-action, @fingers-shift-action and @fingers-alt-action to allow different actions when holding ctlr/alt/shift. Ctrl + a-z will open links in browser, SHIFT + a-z will automatically paste selected matches. 144 | * Added integration with OS clipboard and file openers. This removes dependency with tmux-yank. 145 | * Added multi mode, which allows to copy multiple matches at the same time. When pressing TAB. ( fixes #66 ) 146 | * Fixed WSL support ( fixes #64 ) 147 | * Fixed accidental window renaming ( fixes #65 ) 148 | * Fixed custom patterns parsing. 149 | * Migrated tests to TravisCI, which allows to test in OSX and multiple tmux versions easily ( and for free $ ). 150 | * Deprecated @fingers-copy-command and @fingers-copy-command uppercase in favour of @fingers-(main|ctrl|shift|alt)-action option set. 151 | 152 | ## 0.10.1 - 02 Jan 2019 153 | 154 | * Fix dangling pane when cancelling fingers-mode. 155 | 156 | ## 0.10.0 - 29 Dec 2018 157 | 158 | * New default pattern for uuids ( thanks @kidd ! ). 159 | * `@fingers-copy-command` ( and uppercase alternative ) can now be configured 160 | to automatically paste copied stuff ( thanks @kidd also ! ). 161 | 162 | ## 0.9.0 - 08 Nov 2018 163 | 164 | * Removed health check from startup, now needs to be run manually. 165 | * Fixed health check handling of tmux rc versions ( thanks @ysf ! ). 166 | * Tweaked hexadecimal default pattern ( thanks @giadomelio ! ). 167 | 168 | ## 0.8.0 - 28 Aug 2018 169 | 170 | * New default pattern for kubernetes resource ( thanks @ryankemper ! ) 171 | * New default pattern for hexadecimal numbers ( thanks @ysf ! ) 172 | * Fixed broken "tmux last-pane" behavior ( Fixes #48 ) 173 | * Fixed broken "tmux attach" behavior ( Fixes #54 ) 174 | * Upgraded to CircleCI 2.0 and started using master/develop branching model. 175 | 176 | ## 0.7.2 - 30 Mar 2018 177 | 178 | * Fix portability issues when copying results. Fixes #47 179 | 180 | ## 0.7.1 - 11 Mar 2018 181 | 182 | * Fixed bug with sed BSD/OSX. 183 | * Fixes in BSD tests. 184 | 185 | ## 0.7.0 - 15 Feb 2018 186 | 187 | * Fixed issue when invoking fingers from an unzoomed pane. Fixes #44 188 | * Fixed issues with `@fingers-copy-command`, now commands like `xdg-open` work. 189 | * Added `@fingers-copy-command-uppercase` option. This command will be called 190 | when holding SHIFT while selecting hint. Fixes #43 191 | 192 | ## 0.6.3 - 08 Oct 2017 193 | 194 | * Fixed more issues with clipboard integration, works now on OSX and Linux. 195 | * Fixed line-jumping with user input 196 | * Improved color defaults, for better readability when no dimmed colours are supported. 197 | * Improved feedback, added checks and fixed issues of system health-check. 198 | 199 | ## 0.6.2 - 24 May 2017 200 | 201 | * Fixed issues with `tmux-yank` in Mac OS ( thanks @john-kurkowski ! ) 202 | 203 | ## 0.6.1 - 17 May 2017 204 | 205 | * Fixed `tmux-yank` integration with tmux 2.4 in backwards compatible way. 206 | 207 | ## 0.6.0 - 02 May 2017 208 | 209 | * Refactored configuration script. Now `.tmux.conf` must be re-sourced for changes to take effect. 210 | * Added custom color support. Included in options `@fingers-hint-format` and `@fingers-highlight-format`. 211 | * Configurable hint position with options `@fingers-hint-position`. 212 | * All options above are available with `-nocompact` suffix to use when `@fingers-compact-hints` is set to 0. 213 | * Fixed issue #26. 214 | 215 | ## 0.5.0 - 20 Apr 2017 216 | 217 | * Added support for tmux of the future ( greater than 2.3 ). Thanks @fcsonline! 218 | * Tests rewritten in bash. Bye bye `expect` tool! 219 | 220 | ## 0.4.1 - 09 Apr 2017 221 | 222 | * Looks like `gawk` should be 4+ for things to go smooth. 223 | * Improved output of system health check. 224 | 225 | ## 0.4.0 - 07 Apr 2017 226 | 227 | * `gawk` is now a required dependency. 228 | * Added a system health check on startup. 229 | 230 | ## 0.3.8 - 14 Feb 2017 231 | 232 | * Fixed support for fish shell. 233 | 234 | ## 0.3.7 - 07 Feb 2017 235 | 236 | * Match SHAs of variable size. ( thanks @jacob-keller ! ) 237 | 238 | ## 0.3.6 - 09 Dec 2016 239 | 240 | * Yep, finally fixed `.bash_history` pollution properly. With coffee and 241 | everything. 242 | 243 | ## 0.3.5 - 03 Dec 2016 244 | 245 | * Reverted wrong commit, it was the `.bash_history` what was broken. Never code 246 | without enough coffee in your veins. 247 | 248 | ## 0.3.4 - 03 Dec 2016 249 | 250 | * Oops, reverted tmp files fix, as it messes up with window name. 251 | 252 | ## 0.3.3 - 03 Dec 2016 253 | 254 | * Fixed `.bash_history` pollution. 255 | * Now all tmp files are properly deleted. 256 | 257 | ## 0.3.2 - 25 Oct 2016 258 | 259 | * Now hints are unique. If a match has several occurrences it will always have 260 | the same hint. 261 | 262 | ## 0.3.1 - 22 Oct 2016 263 | 264 | * Fixed parsing of @fingers-pattern-N option not working for more than one 265 | digit ( thanks @sunaku ! ) 266 | 267 | ## 0.3.0 - 17 Oct 2016 268 | 269 | * Hints now render in a compacter way, avoiding line wraps for better 270 | readability. 271 | * New @fingers-compact-hints option to customize how hints are rendered. 272 | * Added shorcuts while in **[fingers]** mode as well as help screen. 273 | * Signifcantly improved performance by ignoring `.bashrc` and `.bash_profile`. 274 | ( It can't get any faster now! ) 275 | 276 | ## 0.2.0 - 24 Aug 2016 277 | 278 | * Hinter rewritten in awk for improved performance. 279 | 280 | ## 0.1.6 - 04 Aug 2016 281 | 282 | * Preserve zoom state of pane when prompting hints. 283 | * More robust input handling ( holding arrow keys does not output random shite 284 | any more ) 285 | 286 | ## 0.1.5 - 14 Jul 2016 287 | 288 | * Improved rendering of wrapped lines. 289 | * Fixed more than one match per line in BSD/OSX. 290 | * Added automated tests. 291 | 292 | ## 0.1.4 - 06 Jul 2016 293 | 294 | * Fixed tmux-yank integration. 295 | 296 | ## 0.1.3 - 24 May 2016 297 | 298 | * Fixed issues with @fingers-copy-command and xclip not working properly. 299 | 300 | ## 0.1.2 - 23 May 2016 301 | 302 | * Fixed blank screen for certain outputs in BSD/OSX. 303 | 304 | ## 0.1.1 - 16 May 2016 305 | 306 | * Partially fixed for BSD/OSX. 307 | 308 | ## 0.1.0 - 14 May 2016 309 | 310 | * New @fingers-copy-command option. 311 | * Slightly improved performance ( still work to do ). 312 | * Improved rendering of hints. 313 | * Fixed tabs not being preserved when showing results. 314 | * Fixed problem with scrollback history clearing. 315 | * fingers script is now executed silently to prevent shell history pollution. 316 | 317 | ## 0.0.1 - 3 May 2016 318 | 319 | * Initial release. 320 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM crystallang/crystal:1.14-alpine 2 | RUN apk upgrade && apk add bash libevent-dev ncurses-dev ncurses hyperfine bison 3 | COPY ./spec/install-tmux-versions.sh /opt/install-tmux-versions.sh 4 | COPY ./spec/use-tmux.sh /opt/use-tmux.sh 5 | RUN bash /opt/install-tmux-versions.sh 6 | WORKDIR /app 7 | CMD ["/bin/sh"] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jorge Morante Cabrera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | docker build . -t fingers 3 | 4 | shell: default 5 | docker run -it --rm -v $(shell pwd):/app fingers bash 6 | 7 | dev: default 8 | docker run -it --rm -v $(shell pwd):/app fingers bash -c "/opt/use-tmux.sh 3.4; FINGERS_LOG_PATH='/app/fingers.log' shards build; tmux -f spec/conf/dev.conf \; new-session \; split-window 'tail -f /root/.local/state/tmux-fingers/fingers.log' \; last-pane" 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![tmux-fingers](./logo.svg) 2 | 3 | ![demo](https://github.com/Morantron/tmux-fingers/assets/3304507/cafe8877-1c98-41b1-bb65-b72129fea701) 4 | 5 | # Usage 6 | 7 | Press ( prefix + F ) to enter **[fingers]** mode, it will highlight relevant stuff in the current 8 | pane along with letter hints. By pressing those letters, the highlighted match 9 | will be copied to the clipboard. Less keystrokes == profit! 10 | 11 | Here is a list of the stuff highlighted by default. 12 | 13 | * File paths 14 | * SHAs 15 | * numbers ( 4+ digits ) 16 | * hex numbers 17 | * IP addresses 18 | * kubernetes resources 19 | * UUIDs 20 | * git status/diff output 21 | 22 | Checkout [list of built-in patterns](#fingers-enabled-builtin-patterns). 23 | 24 | ## Key shortcuts 25 | 26 | While in **[fingers]** mode, you can use the following shortcuts: 27 | 28 | * a-z: copies selected match to the clipboard 29 | * CTRL + a-z: copies selected match to the clipboard and triggers [@fingers-ctrl-action](#fingers-ctrl-action). By default it triggers `:open:` action, which is useful for opening links in the browser for example. 30 | * SHIFT + a-z: copies selected match to the clipboard and triggers [@fingers-shift-action](#fingers-shift-action). By default it triggers `:paste:` action, which automatically pastes selected matches. 31 | * ALT + a-z: copies selected match to the clipboard and triggers [@fingers-alt-action](#fingers-alt-action). There is no default, configurable by the user. 32 | * TAB: toggle multi mode. First press enters multi mode, which allows to select multiple matches. Second press will exit with the selected matches copied to the clipboard. 33 | * q, ESC or CTRL + c: exit **[fingers]** mode 34 | 35 | # Requirements 36 | 37 | * tmux 3.0 or newer 38 | 39 | # Installation 40 | 41 | ## Using [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) 42 | 43 | Add the following to your list of TPM plugins in `.tmux.conf`: 44 | 45 | ``` 46 | set -g @plugin 'Morantron/tmux-fingers' 47 | ``` 48 | 49 | Hit prefix + I to fetch and source the plugin. The first time you run it you'll be presented with a wizard to complete the installation. Depending on the platform, the wizard will offer the following installation methods: 50 | 51 | - Building from source (requires [crystal](https://crystal-lang.org/install/)). _Available in all platforms_ 52 | - Install through [brew](https://brew.sh). _Mac OS only_. 53 | - Download standalone binary. _Linux x86 only_. 54 | 55 | ## Manual 56 | 57 | Clone the repo: 58 | 59 | ``` 60 | $ git clone https://github.com/Morantron/tmux-fingers ~/.tmux/plugins/tmux-fingers 61 | ``` 62 | 63 | Source it in your `.tmux.conf`: 64 | 65 | ``` 66 | run-shell ~/.tmux/plugins/tmux-fingers/tmux-fingers.tmux 67 | ``` 68 | 69 | Reload TMUX conf by running: 70 | 71 | ``` 72 | $ tmux source-file ~/.tmux.conf 73 | ``` 74 | 75 | The first time you run it you'll be presented with a wizard to complete the installation. 76 | 77 | # Configuration 78 | 79 | NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` file. 80 | 81 | * [@fingers-key](#fingers-key) 82 | * [@fingers-jump-key](#fingers-jump-key) 83 | * [@fingers-pattern-N](#fingers-pattern-n) 84 | * [@fingers-main-action](#fingers-main-action) 85 | * [@fingers-ctrl-action](#fingers-ctrl-action) 86 | * [@fingers-alt-action](#fingers-alt-action) 87 | * [@fingers-shift-action](#fingers-shift-action) 88 | * [@fingers-hint-style](#fingers-hint-style) 89 | * [@fingers-highlight-style](#fingers-highlight-style) 90 | * [@fingers-backdrop-style](#fingers-backdrop-style) 91 | * [@fingers-selected-hint-style](#fingers-selected-hint-style) 92 | * [@fingers-selected-highlight-style](#fingers-selected-highlight-style) 93 | * [@fingers-hint-position](#fingers-hint-position) 94 | * [@fingers-keyboard-layout](#fingers-keyboard-layout) 95 | * [@fingers-show-copied-notification](#fingers-show-copied-notification) 96 | * [@fingers-enabled-builtin-patterns](#fingers-enabled-builtin-patterns) 97 | 98 | Recipes: 99 | 100 | * [Start tmux-fingers without prefix](#start-tmux-fingers-without-prefix) 101 | * [Using only specific patterns](#using-only-specific-patterns) 102 | 103 | ## @fingers-key 104 | 105 | `default: F` 106 | 107 | Customize how to enter fingers mode. Always preceded by prefix: `prefix + @fingers-key`. 108 | 109 | For example: 110 | 111 | ``` 112 | set -g @fingers-key F 113 | ``` 114 | 115 | ## @fingers-jump-key 116 | 117 | `default: J` 118 | 119 | Customize how to enter fingers jump mode. Always preceded by prefix: `prefix + @fingers-jump-key`. 120 | 121 | In jump mode, the cursor will be placed in the position of the match after the hint is selected. 122 | 123 | ## @fingers-pattern-N 124 | 125 | You can also add additional patterns if you want more stuff to be highlighted: 126 | 127 | ``` 128 | # You can define custom patterns like this 129 | set -g @fingers-pattern-0 'git rebase --(abort|continue)' 130 | 131 | # Increment the number and define more patterns 132 | set -g @fingers-pattern-1 'some other pattern' 133 | 134 | # You can use a named capture group like this (?YOUR-REGEX-HERE) 135 | # to only highlight and copy part of the match. 136 | set -g @fingers-pattern-2 'capture (?only this)' 137 | 138 | # Watch out for backslashes! For example the regex \d{50} matches 50 digits. 139 | set -g @fingers-pattern-3 '\d{50}' # No need to escape if you use single quotes 140 | set -g @fingers-pattern-4 "\\d{50}" # If you use double quotes, you'll need to escape backslashes for special characters to work 141 | set -g @fingers-pattern-5 \\d{50} # Escaping also needed if you don't use any quotes 142 | ``` 143 | 144 | Patterns use [PCRE pattern syntax](https://www.pcre.org/original/doc/html/pcrepattern.html). 145 | 146 | If the introduced regex contains an error, an error will be shown when invoking the plugin. 147 | 148 | ## @fingers-main-action 149 | 150 | `default: :copy:` 151 | 152 | By default **tmux-fingers** will copy matches in tmux and system clipboard. 153 | 154 | If you still want to set your own custom command you can do so like this: 155 | 156 | ``` 157 | set -g @fingers-main-action '' 158 | ``` 159 | This command will also receive the following: 160 | 161 | * `MODIFIER`: environment variable set to `ctrl`, `alt`, or `shift` specififying which modifier was used when selecting the match. 162 | * `HINT`: environment variable the selected letter hint itself ( ex: `q`, `as`, etc... ). 163 | * `stdin`: copied text will be piped to `@fingers-copy-command`. 164 | 165 | You can also use the following special values: 166 | 167 | * `:paste:` Copy the the match and paste it automatically. 168 | * `:copy:` Uses built-in system clipboard integration to copy the match. 169 | * `:open:` Uses built-in open file integration to open the file ( opens URLs in default browser, files in OS file navigator, etc ). 170 | 171 | ## @fingers-ctrl-action 172 | 173 | `default: :open:` 174 | 175 | Same as [@fingers-main-action](#fingers-main-action) but only called when match is selected by holding ctrl 176 | 177 | ## @fingers-alt-action 178 | 179 | Same as [@fingers-main-action](#fingers-main-action) but only called when match is selected by holding alt 180 | 181 | ## @fingers-shift-action 182 | 183 | `default: :paste:` 184 | 185 | Same as [@fingers-main-action](#fingers-main-action) but only called when match is selected by holding shift 186 | 187 | ## @fingers-hint-style 188 | 189 | `default: "fg=green,bold` 190 | 191 | With this option you can define the styles for the letter hints. 192 | 193 | You can customize the styles using the same syntax used in `.tmux.conf` for styling the status bar. 194 | 195 | More info in the `STYLES` section of `man tmux`. 196 | 197 | Supported styles are: `bright`, `bold`, `dim`, `underscore`, `italics`. 198 | 199 | ## @fingers-highlight-style 200 | 201 | `default: "fg=yellow"` 202 | 203 | Custom styles for the highlighted match. See [@fingers-hint-style](#fingers-hint-style) for more details. 204 | 205 | ## @fingers-backdrop-style 206 | 207 | `default: ""` 208 | 209 | Custom styles for all the text that is not matched. See [@fingers-hint-style](#fingers-hint-style) for more details. 210 | 211 | ## @fingers-selected-hint-style 212 | 213 | `default: "fg=blue,bold"` 214 | 215 | Format for hints in selected matches in multimode. 216 | 217 | ## @fingers-selected-highlight-style 218 | 219 | `default: "fg=blue"` 220 | 221 | Format for selected matches in multimode. 222 | 223 | ## @fingers-hint-position 224 | 225 | `default: "left"` 226 | 227 | Control the position where the hint is rendered. Possible values are `"left"` 228 | and `"right"`. 229 | 230 | ## @fingers-keyboard-layout 231 | 232 | `default: "qwerty"` 233 | 234 | Hints are generated taking optimal finger movement into account. You can choose between the following: 235 | 236 | * `qwerty`: the default, use all letters 237 | * `qwerty-left-hand`: only use letters easily reachable with left hand 238 | * `qwerty-right-hand`: only use letters easily reachable with right hand 239 | * `qwerty-homerow`: only use letters in the homerow 240 | * `qwertz` 241 | * `qwertz-left-hand` 242 | * `qwertz-right-hand` 243 | * `qwertz-homerow` 244 | * `azerty` 245 | * `azerty-left-hand` 246 | * `azerty-right-hand` 247 | * `azerty-homerow` 248 | * `colemak` 249 | * `colemak-left-hand` 250 | * `colemak-right-hand` 251 | * `colemak-homerow` 252 | * `dvorak` 253 | * `dvorak-left-hand` 254 | * `dvorak-right-hand` 255 | * `dvorak-homerow` 256 | 257 | ## @fingers-show-copied-notification 258 | 259 | `default: 0` 260 | 261 | Show a message using `tmux display-message` notifying about the copied result. 262 | 263 | ## @fingers-enabled-builtin-patterns 264 | 265 | `default: all` 266 | 267 | A list of comma separated pattern names. Built-in patterns are the following: 268 | 269 | | Name | Description | Example | 270 | | ----------------- | --------------------------------------------------------- | ---------------------------------------------- | 271 | | ip | ipv4 addresses | `192.168.0.1` | 272 | | uuid | uuid identifier | `f1b43afb-773c-4da2-9ae5-fef1aa6945ce` | 273 | | sha | sha identifier | `c8b911e2c7e9a6cc57143eaa12cad57c1f0d69df` | 274 | | digit | four or more digits | `1337` | 275 | | url | urls (supported protocols: http/https/git/ssh/file) | `https://asdf.com` | 276 | | path | file paths | `path/to/file` | 277 | | hex | hexidecimal numbers | `0x00FF` | 278 | | kubernetes | kubernetes identifer | `deployment.apps/zookeeper` | 279 | | git-status | will match file paths in the output of git status | `modified: ./path/to/file` | 280 | | git-status-branch | will match branch name in the output of git status | `Your branch is up to date withname-of-branch` | 281 | | diff | will match paths in diff output | `+++ a/path/to/file` | 282 | 283 | ## @fingers-cli 284 | 285 | You can set up key bindings directly to invoke tmux-fingers by using a special global option `@fingers-cli` exposed by the plugin. 286 | 287 | The following options are available: 288 | 289 | ``` 290 | Usage: 291 | tmux-fingers start [options] 292 | 293 | Arguments: 294 | pane_id pane id (also accepts tmux target-pane tokens specified in tmux man pages) (required) 295 | 296 | Options: 297 | --mode can be "jump" or "default" (default: default) 298 | --patterns comma separated list of pattern names 299 | --main-action command to which the output will be piped 300 | --ctrl-action command to which the output will be piped when holding CTRL key 301 | --alt-action command to which the output will be piped when holding ALT key 302 | --shift-action command to which the output will be pipedwhen holding SHIFT key 303 | -h, --help prints help 304 | ``` 305 | 306 | Check some examples in the [Recipes](#Recipes) section below. 307 | 308 | # Recipes 309 | 310 | ## Start tmux-fingers without prefix 311 | 312 | You can start tmux-fingers without having to press tmux prefix by adding bindings like this: 313 | 314 | ``` 315 | # tmux.conf 316 | 317 | # Start tmux fingers by pressing Alt+F 318 | bind -n M-f run -b "#{@fingers-cli} start #{pane_id}" 319 | 320 | # Start tmux fingers in jump mode by pressing Alt+J 321 | bind -n M-j run -b "#{@fingers-cli} start #{pane_id} --mode jump" 322 | 323 | ``` 324 | 325 | ## Using only specific patterns 326 | 327 | You can start tmux-fingers with an specific set of built-in or custom patterns. 328 | 329 | ``` 330 | # match urls with prefix + u 331 | bind u run -b "#{@fingers-cli} start #{pane_id} --patterns url" 332 | 333 | # match hashes with prefix + h 334 | bind h run -b "#{@fingers-cli} start #{pane_id} --patterns sha" 335 | 336 | # match git stuff with prefix + g 337 | bind g run -b "#{@fingers-cli} start #{pane_id} --patterns git-status,git-status-branch" 338 | 339 | # match custom pattern with prefix + y 340 | set -g @fingers-pattern-yolo "yolo.*" 341 | bind y run -b "#{@fingers-cli} start #{pane_id} --patterns yolo" 342 | ``` 343 | 344 | ## Using arbitrary commands 345 | 346 | You can use tmux-fingers with any arbitrary command. 347 | 348 | ``` 349 | # edit file using nvim in a new tmux window with prefix + e 350 | bind e run -b "#{@fingers-cli} start #{pane_id} --patterns path --main-action 'xargs tmux new-window nvim'" 351 | ``` 352 | 353 | ## Target adjacent panes 354 | 355 | You can use tmux target-pane tokens to target adjacent panes. This 356 | configuration uses vim-style ALT+hjkl keys to 357 | directionally adjacent panes. 358 | 359 | Also uses ALT+o to target the last pane. 360 | 361 | ``` 362 | bind -n M-h run -b "#{@fingers-cli} start {left-of}" 363 | bind -n M-j run -b "#{@fingers-cli} start {down-of}" 364 | bind -n M-k run -b "#{@fingers-cli} start {up-of}" 365 | bind -n M-l run -b "#{@fingers-cli} start {right-of}" 366 | bind -n M-o run -b "#{@fingers-cli} start {last}" 367 | ``` 368 | 369 | # Acknowledgements and inspiration 370 | 371 | This plugin is heavily inspired by 372 | [tmux-copycat](https://github.com/tmux-plugins/tmux-copycat) ( **tmux-fingers** 373 | predefined search are *copycatted* :trollface: from 374 | [tmux-copycat](https://github.com/tmux-plugins/tmux-copycat) ). 375 | 376 | Kudos to [bruno-](https://github.com/bruno-) for paving the way to tmux 377 | plugins! :clap: :clap: 378 | 379 | # License 380 | 381 | [MIT](https://github.com/Morantron/tmux-fingers/blob/master/LICENSE) 382 | -------------------------------------------------------------------------------- /dev/benchmark.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | def fix_git_shit 4 | `git config --global --add safe.directory /app` 5 | end 6 | 7 | def cleanup 8 | `tmux kill-server` 9 | `rm -rf /root/.local/state/tmux-fingers` 10 | end 11 | 12 | def run_benchmark 13 | `/opt/use-tmux.sh 3.3a` 14 | `shards build --production` 15 | 16 | puts "running: tmux -f #{Dir.current}/spec/conf/benchmark.conf new-session -d" 17 | `tmux -f #{Dir.current}/spec/conf/benchmark.conf new-session -d` 18 | `tmux resize-window -t '@0' -x 300 -y 300` 19 | `tmux send-keys 'COLUMNS=300 LINES=100 crystal run spec/fill_screen.cr'` 20 | `tmux send-keys Enter` 21 | 22 | sleep 5 23 | 24 | puts "Running benchmarks ..." 25 | 26 | output_file = File.tempfile("benchmark") 27 | 28 | `tmux new-window 'hyperfine --prepare "bash kill-windows.sh" --warmup 5 --runs 100 "bin/tmux-fingers start %0" --export-json #{output_file.path}'` 29 | 30 | while File.size(output_file.path) == 0 31 | puts "Waiting for benchmark results" 32 | sleep 5 33 | end 34 | 35 | cleanup 36 | 37 | JSON.parse(output_file) 38 | end 39 | 40 | def replace_string_in_file(file_path : String, search_string : String, replace_string : String) 41 | content = File.read(file_path) 42 | 43 | updated_content = content.gsub(search_string, replace_string) 44 | 45 | File.write(file_path, updated_content) 46 | end 47 | 48 | def clone_repo_and_cd(version) 49 | repo_path = "/tmp/tmux-fingers-#{version}" 50 | `git clone /app #{repo_path}` 51 | 52 | Dir.cd(repo_path) 53 | 54 | fix_git_shit 55 | 56 | `git checkout #{version}` 57 | 58 | replace_string_in_file( 59 | "#{repo_path}/spec/conf/benchmark.conf", 60 | "/app/tmux-fingers.tmux", 61 | "#{repo_path}/tmux-fingers.tmux" 62 | ) 63 | end 64 | 65 | versions = ARGV 66 | 67 | results = [] of JSON::Any 68 | 69 | versions.each do |version| 70 | clone_repo_and_cd(version) 71 | 72 | results << run_benchmark 73 | end 74 | 75 | versions.each_with_index do |version, index| 76 | result = results[index]? 77 | 78 | next unless result 79 | 80 | puts "#{version}: #{result["results"][0]["mean"]}s ± #{result["results"][0]["stddev"]}s" 81 | end 82 | -------------------------------------------------------------------------------- /dev/release.cr: -------------------------------------------------------------------------------- 1 | require "semantic_version" 2 | require "yaml" 3 | 4 | struct ShardFile 5 | include YAML::Serializable 6 | include YAML::Serializable::Unmapped 7 | 8 | property version : String 9 | end 10 | 11 | current_version = SemanticVersion.parse(`shards version`.chomp) 12 | current_branch = `git symbolic-ref --short HEAD`.chomp 13 | pending_changes = `git status -s`.chomp 14 | 15 | if current_branch != "develop" 16 | puts "This script should be ran from develop branch" 17 | exit 1 18 | end 19 | 20 | if pending_changes != "" 21 | puts "There are uncommited changes" 22 | exit 1 23 | end 24 | 25 | puts "Which component you want to bump? major.minor.patch" 26 | print "> " 27 | 28 | component = gets 29 | 30 | puts "Bumping #{component} in #{current_version}" 31 | 32 | next_version = case component 33 | when "major" 34 | current_version.bump_major 35 | when "minor" 36 | current_version.bump_minor 37 | when "patch" 38 | current_version.bump_patch 39 | else 40 | current_version.bump_patch 41 | end 42 | 43 | shard = ShardFile.from_yaml(File.read("shard.yml")) 44 | shard.version = next_version.to_s 45 | 46 | File.write("shard.yml", shard.to_yaml) 47 | 48 | current_date = Time.local.to_s("%d %b %Y") 49 | 50 | `git add shard.yml` 51 | `git commit -am "bump version in shard.yml"` 52 | 53 | content_to_prepend = "## #{next_version.to_s} - #{current_date}\n\nEDIT THIS:\n\n#{`git log --oneline #{current_version}..@`.chomp}\n\n" 54 | 55 | original_content = File.read("CHANGELOG.md") 56 | File.write("CHANGELOG.md", content_to_prepend + original_content) 57 | 58 | Process.run(ENV["EDITOR"], args: ["CHANGELOG.md"], input: :inherit, output: :inherit, error: :inherit) 59 | 60 | `git add CHANGELOG.md` 61 | `git commit -am 'updated CHANGELOG.md'` 62 | 63 | print "Confirm release? [Y/n]\n >" 64 | answer = gets 65 | 66 | if answer == "n" 67 | puts "Canceling release" 68 | exit 1 69 | end 70 | 71 | `git checkout master` 72 | `git merge develop` 73 | `git tag #{next_version.to_s}` 74 | 75 | puts "Run the following command to push the release" 76 | puts "" 77 | puts "git push && git push --tags" 78 | -------------------------------------------------------------------------------- /docs/migrating-from-1.md: -------------------------------------------------------------------------------- 1 | # Migrating from tmux-fingers 1.x 2 | 3 | Styles and formatting has been reworked a little bit in order to simplify the renderer implementation. Most notably: 4 | 5 | - `@fingers-compact-hints` is deprecated. All rendering will happen now in compact mode. Rendering in no-compact mode changes the length of the lines, and can introduce extra line jumps that make things move around. 6 | - All format related options have now been renamed to style. Interpolation with `%s` is also removed, as this can also introduce line length changes. 7 | 8 | 9 | ## Migrating format options to style 10 | 11 | Here's an example on how to update your formatting for it to work on tmux-fingers 2.x 12 | 13 | ``` 14 | # tmux-fingers 1.x 15 | 16 | set -g @fingers-highlight-format "#[fg=yellow,bold]%s" 17 | 18 | # tmux-fingers 2.x 19 | 20 | set -g @fingers-highlight-style "fg=yellow,bold" 21 | ``` 22 | 23 | Here's the mappings between format and style options. 24 | 25 | | Old tmux-fingers 1.x format option | New tmux-fingers 2.x style option equivalent | 26 | | -------------------------------------------- | -------------------------------------------- | 27 | | @fingers-highlight-format | @fingers-highlight-style | 28 | | @fingers-hint-format | @fingers-hint-style | 29 | | @fingers-selected-highlight-format | @fingers-selected-highlight-style | 30 | | @fingers-selected-hint-format | @fingers-selected-hint-style | 31 | | @fingers-highlight-format-nocompact | _No equivalent_ | 32 | | @fingers-hint-format-nocompact | _No equivalent_ | 33 | | @fingers-selected-highlight-format-nocompact | _No equivalent_ | 34 | | @fingers-selected-hint-format-nocompact | _No equivalent_ | 35 | 36 | That should be it! 37 | 38 | ## Regex syntax 39 | 40 | The regex engine has been changed from ERE to PCRE. You might need to update your custom patterns. 41 | -------------------------------------------------------------------------------- /install-wizard.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | PLATFORM=$(uname -s) 5 | action=$1 6 | 7 | # set up exit trap 8 | function finish { 9 | exit_code=$? 10 | 11 | # only intercept exit code when there is an action defined (download, or 12 | # build from source), otherwise we'll enter an infinte loop of sourcing 13 | # tmux.conf 14 | if [[ -z "$action" ]]; then 15 | exit $exit_code 16 | fi 17 | 18 | if [[ $exit_code -eq 0 ]]; then 19 | echo "Reloading tmux.conf" 20 | tmux source ~/.tmux.conf 21 | exit 0 22 | else 23 | echo "Something went wrong. Please any key to close this window" 24 | read -n 1 25 | exit 1 26 | fi 27 | } 28 | 29 | trap finish EXIT 30 | 31 | function install_from_source() { 32 | echo "Installing from source..." 33 | 34 | # check if shards is installed 35 | if ! command -v shards >/dev/null 2>&1; then 36 | echo "crystal is not installed. Please install it first." 37 | echo "" 38 | echo " https://crystal-lang.org/install/" 39 | echo "" 40 | exit 1 41 | fi 42 | 43 | pushd $CURRENT_DIR > /dev/null 44 | WIZARD_INSTALLATION_METHOD=build-from-source shards build --production 45 | popd > /dev/null 46 | 47 | echo "Build complete!" 48 | exit 0 49 | } 50 | 51 | function install_with_brew() { 52 | echo "Installing with brew..." 53 | brew tap morantron/tmux-fingers 54 | brew install tmux-fingers 55 | 56 | echo "Installation complete!" 57 | exit 0 58 | } 59 | 60 | 61 | function download_binary() { 62 | mkdir -p $CURRENT_DIR/bin 63 | 64 | if [[ ! "$(uname -m)" == "x86_64" ]]; then 65 | echo "tmux-fingers binaries are only provided for x86_64 architecture." 66 | exit 1 67 | fi 68 | 69 | echo "Getting latest release..." 70 | 71 | # TODO use "latest" tag 72 | url=$(curl -s "https://api.github.com/repos/morantron/tmux-fingers/releases" | grep browser_download_url | head -1 | grep -o https://.*x86_64) 73 | 74 | echo "Downloading binary from $url" 75 | 76 | if [[ -z "$url" ]]; then 77 | echo "Could not find a release for tmux-fingers. Please try again later." 78 | exit 1 79 | fi 80 | 81 | # download binary to bin/tmux-fingers 82 | curl -L $url -o $CURRENT_DIR/bin/tmux-fingers 83 | chmod a+x $CURRENT_DIR/bin/tmux-fingers 84 | 85 | echo "Download complete!" 86 | exit 0 87 | } 88 | 89 | if [[ "$1" == "download-binary" ]]; then 90 | download_binary 91 | fi 92 | 93 | if [[ "$1" == "install-with-brew" ]]; then 94 | echo "Installing with brew..." 95 | install_with_brew 96 | exit 1 97 | fi 98 | 99 | if [[ "$1" == "install-from-source" ]]; then 100 | install_from_source 101 | fi 102 | 103 | function binary_or_brew_label() { 104 | if [[ "$PLATFORM" == "Darwin" ]]; then 105 | echo "Install with brew" 106 | else 107 | echo "Download binary" 108 | fi 109 | } 110 | 111 | function binary_or_brew_action() { 112 | if [[ "$PLATFORM" == "Darwin" ]]; then 113 | echo "install-with-brew" 114 | else 115 | echo "download-binary" 116 | fi 117 | } 118 | 119 | function get_message() { 120 | if [[ "$FINGERS_UPDATE" == "1" ]]; then 121 | echo "It looks like tmux-fingers has been updated. We need to rebuild the binary." 122 | else 123 | echo "It looks like it is the first time you are running the plugin. We first need to get tmux-fingers binary for things to work." 124 | fi 125 | 126 | } 127 | 128 | tmux display-menu -T "tmux-fingers" \ 129 | "" \ 130 | "- " "" ""\ 131 | "- #[nodim,bold]Welcome to tmux-fingers! ✌️ " "" ""\ 132 | "- " "" ""\ 133 | "- $(get_message) " "" "" \ 134 | "- " "" ""\ 135 | "" \ 136 | "$(binary_or_brew_label)" b "new-window \"$CURRENT_DIR/install-wizard.sh $(binary_or_brew_action)\"" \ 137 | "Build from source (crystal required)" s "new-window \"$CURRENT_DIR/install-wizard.sh install-from-source\"" \ 138 | "" \ 139 | "Exit" q "" 140 | -------------------------------------------------------------------------------- /kill-windows.sh: -------------------------------------------------------------------------------- 1 | for id in $(tmux list-windows -F "#{window_id}:#{pane_id}:#{window_name}" | grep -v ":%0:" | grep "fingers" | cut -f1 -d:); do 2 | tmux kill-window -t $id 3 | done 4 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | cling: 4 | git: https://github.com/devnote-dev/cling.git 5 | version: 3.0.0 6 | 7 | tablo: 8 | git: https://github.com/hutou/tablo.git 9 | version: 0.10.1 10 | 11 | xdg_base_directory: 12 | git: https://github.com/tijn/xdg_base_directory.git 13 | version: 1.0.5+git.commit.60bf28dc060c221d5af52727927e92b840022eb0 14 | 15 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.4.1 3 | name: fingers 4 | authors: 5 | - Jorge Morante 6 | targets: 7 | tmux-fingers: 8 | main: src/fingers.cr 9 | dependencies: 10 | cling: 11 | github: devnote-dev/cling 12 | version: '>= 3.0.0' 13 | xdg_base_directory: 14 | github: tijn/xdg_base_directory 15 | tablo: 16 | github: hutou/tablo 17 | crystal: 1.14 18 | license: MIT 19 | -------------------------------------------------------------------------------- /spec/.tmuxomatic_unlock_command_prompt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | 5 | require 'fingers' 6 | 7 | Fingers.logger.debug("command-completed") 8 | -------------------------------------------------------------------------------- /spec/conf/alt-action.conf: -------------------------------------------------------------------------------- 1 | set -g prefix C-a 2 | set -g default-terminal 'screen-256color' 3 | set -g @fingers-alt-action 'action-stub.sh' 4 | run 'tmux-fingers.tmux' 5 | -------------------------------------------------------------------------------- /spec/conf/basic.conf: -------------------------------------------------------------------------------- 1 | set -g default-terminal 'screen-256color' 2 | run '/app/tmux-fingers.tmux' 3 | -------------------------------------------------------------------------------- /spec/conf/benchmark.conf: -------------------------------------------------------------------------------- 1 | set -g @fingers-benchmark-mode '1' 2 | set -g @fingers-skip-wizard '1' 3 | set -g default-terminal 'screen-256color' 4 | run '/app/tmux-fingers.tmux' 5 | -------------------------------------------------------------------------------- /spec/conf/ctrl-action.conf: -------------------------------------------------------------------------------- 1 | set -g default-terminal 'screen-256color' 2 | set -g @fingers-ctrl-action 'action-stub.sh' 3 | run 'tmux-fingers.tmux' 4 | -------------------------------------------------------------------------------- /spec/conf/custom-bindings.conf: -------------------------------------------------------------------------------- 1 | set -g prefix C-Space 2 | set -g mode-keys vi 3 | 4 | run 'tmux-fingers.tmux' 5 | -------------------------------------------------------------------------------- /spec/conf/custom-patterns.conf: -------------------------------------------------------------------------------- 1 | set -g prefix C-a 2 | 3 | set -g @fingers-pattern-0 'YOLOYOLOYOLO' 4 | set -g @fingers-pattern-50 'W00TW00TW00T' 5 | run 'tmux-fingers.tmux' 6 | -------------------------------------------------------------------------------- /spec/conf/dev.conf: -------------------------------------------------------------------------------- 1 | set -g default-terminal 'screen-256color' 2 | set -g @fingers-skip-wizard '1' 3 | 4 | bind C-r source-file /app/spec/conf/dev.conf 5 | bind Enter run "cd /app && FINGERS_LOG_PATH='/app/fingers.log' shards build --production" 6 | 7 | run '/app/tmux-fingers.tmux' 8 | -------------------------------------------------------------------------------- /spec/conf/invalid.conf: -------------------------------------------------------------------------------- 1 | set -g prefix C-a 2 | set -g @fingers-lol 'L0L' 3 | 4 | run 'tmux-fingers.tmux' 5 | -------------------------------------------------------------------------------- /spec/conf/quotes.conf: -------------------------------------------------------------------------------- 1 | set -g prefix C-a 2 | set -g default-terminal 'screen-256color' 3 | 4 | set -g @fingers-pattern-0 '"laser"' 5 | set -g @fingers-pattern-1 "'laser'" 6 | 7 | run 'tmux-fingers.tmux' 8 | -------------------------------------------------------------------------------- /spec/fill_screen.cr: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | 3 | SEGMENT_LENGTH = 16 4 | COLUMNS = ENV["COLUMNS"].to_i 5 | LINES = ENV["LINES"].to_i 6 | 7 | def compute_divisions 8 | result = (COLUMNS / SEGMENT_LENGTH).floor.to_i 9 | 10 | loop do 11 | break if (result * SEGMENT_LENGTH + (result - 1)) <= COLUMNS 12 | result = result - 1 13 | end 14 | 15 | result 16 | end 17 | 18 | DIVISIONS = compute_divisions 19 | 20 | LINES.times do 21 | codes = [] of String 22 | 23 | DIVISIONS.times do 24 | codes << UUID.random.to_s.gsub("-", "").to_s[0..SEGMENT_LENGTH - 1] 25 | end 26 | 27 | puts codes.join(" ") 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/benchmark: -------------------------------------------------------------------------------- 1 | 2 | 265982390429758 21362301420008 214882893312190 11554155254778 3 | 93112662828415 1861636117514 24293224364897 11616967922589 4 | 149561703515620 147192438517408 119362656132653 259473161016622 5 | 26182816618282 749425011333 147894782452 6442732125800 6 | 3537130105336 141162223621551 91591152616370 171632212717214 7 | 297263221030670 31198631515200 2000545606028 145532950528175 8 | 18022140422319 177071079827303 110342670114356 185239264440 9 | 222352552627429 198841343024320 284501924918415 319402012730857 10 | 37673530947 1488750949859 231481731431188 13193123525466 11 | 7013796711758 11432920120193 635473127653 3101985454008 12 | 68503003211186 28808147557353 5611755517014 7416147411419 13 | 3129768624280 267571483317777 851621635652 10455258643735 14 | 9061703118615 19579308143142 2148192128021 6173076710102 15 | 35102323818615 320662001031610 181373169332438 21549634016318 16 | 13298366824787 158073252417041 677227996806 4954163968740 17 | 23115783425287 64631276326967 111312908917157 182592183213454 18 | 84301535915650 5743953160 18438947914073 29497177372166 19 | 22316194161386 132972607318923 147362181313176 10183176899020 20 | 4437348216794 6735342623369 13226397113030 2250832389266 21 | 66412833810787 181691605229889 18876812529962 1789342018621 22 | 912106516920 220161710416067 99612181032344 15138301620065 23 | 26851651615927 183051502617127 277012151320289 244882340415011 24 | 240402604016713 303082422917199 4209180631952 3249713933226 25 | 28137793427901 314413187918194 27778518525320 16978203091898 26 | 2419179329323 10115128533863 4448323724928 25533123582200 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /spec/fixtures/custom-patterns: -------------------------------------------------------------------------------- 1 | YOLOYOLOYOLO 2 | beep 3 | 4 | W00TW00TW00T 5 | beep 6 | -------------------------------------------------------------------------------- /spec/fixtures/grep-output: -------------------------------------------------------------------------------- 1 | scripts/debug.sh:# Source this file and call `tail -f fingers.log` when you don't know WTF is 2 | scripts/hints.sh:IFS=$(echo -en "\n\b") # wtf bash? 3 | -------------------------------------------------------------------------------- /spec/fixtures/ip-output: -------------------------------------------------------------------------------- 1 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 2 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 3 | inet 127.0.0.1/8 scope host lo 4 | valid_lft forever preferred_lft forever 5 | inet6 ::1/128 scope host 6 | valid_lft forever preferred_lft forever 7 | 2: wlp3s0: mtu 1500 qdisc fq_codel state UP group default qlen 1000 8 | link/ether 78:31:c1:d5:40:ce brd ff:ff:ff:ff:ff:ff 9 | inet 192.168.1.33/24 brd 192.168.1.255 scope global dynamic wlp3s0 10 | valid_lft 40162sec preferred_lft 40162sec 11 | inet6 fe80::7a31:c1ff:fed5:40ce/64 scope link tentative dadfailed 12 | valid_lft forever preferred_lft forever 13 | 3: br0: mtu 1500 qdisc noqueue state DOWN group default qlen 1000 14 | link/ether 46:06:f6:15:ea:fb brd ff:ff:ff:ff:ff:ff 15 | inet 10.0.3.1/24 brd 10.0.3.255 scope global br0 16 | valid_lft forever preferred_lft forever 17 | 4: vboxnet0: mtu 1500 qdisc fq_codel state UP group default qlen 1000 18 | link/ether 0a:00:27:00:00:00 brd ff:ff:ff:ff:ff:ff 19 | inet 10.0.1.1/24 brd 10.0.1.255 scope global vboxnet0 20 | valid_lft forever preferred_lft forever 21 | inet6 fe80::800:27ff:fe00:0/64 scope link 22 | valid_lft forever preferred_lft forever 23 | 5: vboxnet1: mtu 1500 qdisc noop state DOWN group default qlen 1000 24 | link/ether 0a:00:27:00:00:01 brd ff:ff:ff:ff:ff:ff 25 | 6: vboxnet2: mtu 1500 qdisc noop state DOWN group default qlen 1000 26 | link/ether 0a:00:27:00:00:02 brd ff:ff:ff:ff:ff:ff 27 | -------------------------------------------------------------------------------- /spec/fixtures/line_jump_fixture: -------------------------------------------------------------------------------- 1 | 1 line break 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 2 | -------------------------------------------------------------------------------- /spec/fixtures/quotes: -------------------------------------------------------------------------------- 1 | so, let's test "laser" 2 | so, let's test 'laser' 3 | -------------------------------------------------------------------------------- /spec/install-tmux-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n "$CI_TMUX_VERSION" ]]; then 4 | VERSIONS=("$CI_TMUX_VERSION") 5 | else 6 | VERSIONS=("3.0a" "3.1c" "3.2a" "3.3a" "3.4" "3.5a") 7 | fi 8 | 9 | mkdir -p /opt 10 | chmod a+w /opt 11 | 12 | pushd /tmp 13 | for version in "${VERSIONS[@]}"; 14 | do 15 | if [[ -d "/opt/tmux-${version}" ]]; then 16 | continue 17 | fi 18 | 19 | wget "https://github.com/tmux/tmux/releases/download/${version}/tmux-${version}.tar.gz" 20 | tar pfx "tmux-${version}.tar.gz" -C "/opt/" 21 | 22 | pushd "/opt/tmux-${version}" 23 | ./configure 24 | make 25 | popd 26 | done 27 | popd 28 | -------------------------------------------------------------------------------- /spec/lib/fingers/hinter_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../spec_helper.cr" 3 | require "../../../src/fingers/hinter" 4 | require "../../../src/fingers/state" 5 | require "../../../src/fingers/config" 6 | 7 | record StateDouble, selected_hints : Array(String) 8 | 9 | class TextOutput < ::Fingers::Printer 10 | def initialize 11 | @contents = "" 12 | end 13 | 14 | def print(msg) 15 | self.contents += msg 16 | end 17 | 18 | def flush 19 | end 20 | 21 | property :contents 22 | end 23 | 24 | def generate_lines 25 | input = 50.times.map do 26 | 10.times.map do 27 | rand.to_s.split(".").last[0..15].rjust(16, '0') 28 | end.join(" ") 29 | end.join("\n") 30 | end 31 | 32 | describe Fingers::Hinter do 33 | it "works in a grid of lines" do 34 | width = 100 35 | input = generate_lines 36 | output = TextOutput.new 37 | 38 | patterns = Fingers::Config::BUILTIN_PATTERNS.values.to_a 39 | alphabet = "asdf".split("") 40 | 41 | hinter = Fingers::Hinter.new( 42 | input: input.split("\n"), 43 | width: width, 44 | patterns: patterns, 45 | state: ::Fingers::State.new, 46 | alphabet: alphabet, 47 | output: output, 48 | ) 49 | end 50 | 51 | it "only highlights captured groups" do 52 | width = 100 53 | input = " 54 | On branch ruby-rewrite-more-like-crystal-rewrite-amirite 55 | Your branch is up to date with 'origin/ruby-rewrite-more-like-crystal-rewrite-amirite'. 56 | 57 | Changes to be committed: 58 | (use \"git restore --staged ...\" to unstage) 59 | modified: spec/lib/fingers/match_formatter_spec.cr 60 | 61 | Changes not staged for commit: 62 | (use \"git add ...\" to update what will be committed) 63 | (use \"git restore ...\" to discard changes in working directory) 64 | modified: .gitignore 65 | modified: spec/lib/fingers/hinter_spec.cr 66 | modified: spec/spec_helper.cr 67 | modified: src/fingers/cli.cr 68 | modified: src/fingers/dirs.cr 69 | modified: src/fingers/match_formatter.cr 70 | " 71 | output = TextOutput.new 72 | 73 | patterns = Fingers::Config::BUILTIN_PATTERNS.values.to_a 74 | patterns << "On branch (?.*)" 75 | alphabet = "asdf".split("") 76 | 77 | hinter = Fingers::Hinter.new( 78 | input: input.split("\n"), 79 | width: width, 80 | patterns: patterns, 81 | state: ::Fingers::State.new, 82 | alphabet: alphabet, 83 | output: output, 84 | ) 85 | end 86 | 87 | it "only reuses hints when allow duplicates is false" do 88 | width = 100 89 | output = TextOutput.new 90 | 91 | patterns = Fingers::Config::BUILTIN_PATTERNS.values.to_a 92 | alphabet = "asdf".split("") 93 | 94 | input = " 95 | modified: src/fingers/cli.cr 96 | modified: src/fingers/cli.cr 97 | modified: src/fingers/cli.cr 98 | " 99 | 100 | hinter = Fingers::Hinter.new( 101 | input: input.split("\n"), 102 | width: width, 103 | patterns: patterns, 104 | state: ::Fingers::State.new, 105 | alphabet: alphabet, 106 | output: output, 107 | reuse_hints: false 108 | ) 109 | 110 | hinter.run 111 | end 112 | 113 | it "can rerender when not reusing hints" do 114 | width = 100 115 | output = TextOutput.new 116 | 117 | patterns = Fingers::Config::BUILTIN_PATTERNS.values.to_a 118 | alphabet = "asdf".split("") 119 | 120 | input = " 121 | modified: src/fingers/cli.cr 122 | modified: src/fingers/cli.cr 123 | modified: src/fingers/cli.cr 124 | " 125 | 126 | hinter = Fingers::Hinter.new( 127 | input: input.split("\n"), 128 | width: width, 129 | patterns: patterns, 130 | state: ::Fingers::State.new, 131 | alphabet: alphabet, 132 | output: output, 133 | reuse_hints: false 134 | ) 135 | 136 | hinter.run 137 | hinter.run 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/lib/fingers/input_socket_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../../src/fingers/input_socket" 3 | 4 | describe Fingers::InputSocket do 5 | it "works" do 6 | spawn do 7 | sleep 1 8 | sender = Fingers::InputSocket.new 9 | sender.send_message("hey") 10 | end 11 | 12 | listener = Fingers::InputSocket.new 13 | listener.on_input do |msg| 14 | msg.should eq("hey") 15 | break 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/lib/fingers/match_formatter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | require "../../../src/fingers/dirs" 3 | require "../../../src/fingers/match_formatter" 4 | 5 | def setup( 6 | hint_style : String = "#[fg=yellow,bold]", 7 | highlight_style : String = "#[fg=yellow]", 8 | hint_position : String = "left", 9 | selected_hint_style : String = "#[fg=green,bold]", 10 | selected_highlight_style : String = "#[fg=green]", 11 | selected : Bool = false, 12 | offset : Tuple(Int32, Int32) | Nil = nil, 13 | hint : String = "a", 14 | highlight : String = "yolo" 15 | ) 16 | formatter = Fingers::MatchFormatter.new( 17 | highlight_style: highlight_style, 18 | backdrop_style: "#[bg=black,fg=white]", 19 | hint_style: hint_style, 20 | selected_highlight_style: selected_highlight_style, 21 | selected_hint_style: selected_hint_style, 22 | hint_position: hint_position, 23 | reset_sequence: "#[reset]" 24 | ) 25 | 26 | formatter.format(hint: hint, highlight: highlight, selected: selected, offset: offset) 27 | end 28 | 29 | describe Fingers::MatchFormatter do 30 | context "when hint position" do 31 | context "is set to left" do 32 | it "places the hint on the left side" do 33 | result = setup(hint_position: "left") 34 | result.should eq("#[reset]#[fg=yellow,bold]a#[reset]#[fg=yellow]olo#[reset]#[bg=black,fg=white]") 35 | end 36 | end 37 | 38 | context "is set to right" do 39 | it "places the hint on the right side" do 40 | result = setup(hint_position: "right") 41 | result.should eq("#[reset]#[fg=yellow]yol#[reset]#[fg=yellow,bold]a#[reset]#[bg=black,fg=white]") 42 | end 43 | end 44 | end 45 | 46 | context "when a hint is selected" do 47 | it "selects the correct format" do 48 | result = setup(selected: true) 49 | result.should eq("#[reset]#[fg=green,bold]a#[reset]#[fg=green]olo#[reset]#[bg=black,fg=white]") 50 | end 51 | end 52 | 53 | context "when offset is provided" do 54 | it "only highlights at specified offset" do 55 | result = setup(offset: {1, 5}, highlight: "yoloyoloyolo", hint: "a") 56 | result.should eq("#[reset]#[bg=black,fg=white]y#[fg=yellow,bold]a#[reset]#[fg=yellow]loyo#[reset]#[bg=black,fg=white]loyolo#[bg=black,fg=white]") 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/huffman_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "../../src/huffman" 3 | 4 | expected_5 = [ "s", "d", "f", "aa", "as", ] 5 | expected_50 = ["aaa", "aas", "aad", "aaf", "asa", "ass", "asd", "asf", "ada", "ads", "add", "adf", "afa", "afd", "aff", "saa", "sas", "sad", "saf", "ssa", "sss", "ssd", "ssf", "sda", "sds", "sdd", "sdf", "sfa", "afsa", "afss", "afsd", "afsf", "sfsa", "sfss", "sfsd", "sfsf", "sfda", "sfds", "sfdd", "sfdf", "sffa", "sffs", "sffd", "sfffa", "sfffs", "sfffd", "sffffa", "sffffs", "sffffd", "sfffff"] 6 | alphabet_a = ["a", "s", "d", "f"] 7 | 8 | describe Huffman do 9 | it "should work for 5" do 10 | huffman = Huffman.new 11 | 12 | result = huffman.generate_hints(alphabet = alphabet_a, n = 5) 13 | result.should eq expected_5 14 | end 15 | 16 | it "should work for 50" do 17 | huffman = Huffman.new 18 | 19 | result = huffman.generate_hints(alphabet = alphabet_a, n = 50) 20 | result.should eq expected_50 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/patterns_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "../../src/fingers/config" 3 | require "string_scanner" 4 | 5 | def matches_for(pattern_name, input) 6 | pattern = Regex.new(::Fingers::Config::BUILTIN_PATTERNS[pattern_name]) 7 | input.scan(pattern).map { |m| m["match"]? || m[0] } 8 | end 9 | 10 | describe "builtin patterns" do 11 | describe "ip" do 12 | it "should match ip addresses" do 13 | input = " 14 | foo 15 | 192.168.0.1 16 | 127.0.0.1 17 | foofofo 18 | " 19 | matches_for("ip", input).should eq ["192.168.0.1", "127.0.0.1"] 20 | end 21 | end 22 | 23 | describe "uuid" do 24 | it "should match uuids" do 25 | input = " 26 | foo 27 | d6f4b4ac-4b78-4d79-96a1-eb9ab72f2c59 28 | 7a8e24d1-5a81-4f5a-bc6a-9d7f9818a8c4 29 | e5c3dcf0-9b01-45c2-8327-6d9d4bb8a0c8 30 | 2fa5c6e9-33f9-46b7-ba89-3f17b12e59e5 31 | b882bfc5-6b24-43a7-ae1e-8f9ea14eeff2 32 | bar 33 | " 34 | 35 | expected = ["d6f4b4ac-4b78-4d79-96a1-eb9ab72f2c59", 36 | "7a8e24d1-5a81-4f5a-bc6a-9d7f9818a8c4", 37 | "e5c3dcf0-9b01-45c2-8327-6d9d4bb8a0c8", 38 | "2fa5c6e9-33f9-46b7-ba89-3f17b12e59e5", 39 | "b882bfc5-6b24-43a7-ae1e-8f9ea14eeff2"] 40 | 41 | matches_for("uuid", input).should eq expected 42 | end 43 | end 44 | 45 | describe "sha" do 46 | it "should match shas" do 47 | input = " 48 | foo 49 | fc4fea27210bc0d85b74f40866e12890e3788134 50 | fc4fea2 51 | bar 52 | " 53 | 54 | expected = ["fc4fea27210bc0d85b74f40866e12890e3788134", "fc4fea2"] 55 | 56 | matches_for("sha", input).should eq expected 57 | end 58 | end 59 | 60 | describe "digit" do 61 | it "should match shas" do 62 | input = " 63 | foo 64 | 12345 65 | 67891011 66 | bar 67 | " 68 | 69 | expected = ["12345", "67891011"] 70 | 71 | matches_for("digit", input).should eq expected 72 | end 73 | end 74 | 75 | describe "url" do 76 | it "should match urls" do 77 | input = " 78 | foo 79 | https://geocities.com 80 | bar 81 | " 82 | 83 | expected = ["https://geocities.com"] 84 | 85 | matches_for("url", input).should eq expected 86 | end 87 | end 88 | 89 | describe "path" do 90 | it "should match paths" do 91 | input = " 92 | absolute paths /foo/bar/lol 93 | relative paths ./foo/bar/lol 94 | home paths ~/foo/bar/lol 95 | bar 96 | " 97 | 98 | expected = ["/foo/bar/lol", "./foo/bar/lol", "~/foo/bar/lol"] 99 | 100 | matches_for("path", input).should eq expected 101 | end 102 | end 103 | 104 | describe "hex" do 105 | it "should match hex numbers" do 106 | input = " 107 | hello 0xcafe 108 | 0xcaca 109 | 0xdeadbeef hehehe 0xCACA 110 | " 111 | 112 | expected = ["0xcafe", "0xcaca", "0xdeadbeef", "0xCACA"] 113 | 114 | matches_for("hex", input).should eq expected 115 | end 116 | end 117 | 118 | describe "git status" do 119 | it "should match relevant stuff in git status output" do 120 | input = " 121 | Your branch is up to date with 'origin/crystal-rewrite'. 122 | 123 | Changes to be committed: 124 | (use \"git restore --staged ...\" to unstage) 125 | deleted: CHANGELOG.md 126 | new file: wat 127 | 128 | Changes not staged for commit: 129 | (use \"git add ...\" to update what will be committed) 130 | (use \"git restore ...\" to discard changes in working directory) 131 | modified: Makefile 132 | modified: spec/lib/patterns_spec.cr 133 | modified: src/fingers/config.cr 134 | " 135 | 136 | expected = ["CHANGELOG.md", "wat", "Makefile", "spec/lib/patterns_spec.cr", "src/fingers/config.cr"] 137 | 138 | matches_for("git-status", input).should eq expected 139 | end 140 | end 141 | 142 | describe "git status branch" do 143 | it "should match branch in git status output" do 144 | input = " 145 | Your branch is up to date with 'origin/crystal-rewrite'. 146 | 147 | Changes to be committed: 148 | (use \"git restore --staged ...\" to unstage) 149 | deleted: CHANGELOG.md 150 | new file: wat 151 | 152 | Changes not staged for commit: 153 | (use \"git add ...\" to update what will be committed) 154 | (use \"git restore ...\" to discard changes in working directory) 155 | modified: Makefile 156 | modified: spec/lib/patterns_spec.cr 157 | modified: src/fingers/config.cr 158 | " 159 | 160 | expected = ["origin/crystal-rewrite"] 161 | 162 | matches_for("git-status-branch", input).should eq expected 163 | end 164 | end 165 | 166 | describe "git diff" do 167 | it "should match a/b paths in git diff" do 168 | input = " 169 | diff --git a/spec/lib/patterns_spec.cr b/spec/lib/patterns_spec.cr 170 | index 5281097..6c9c18e 100644 171 | --- a/spec/lib/patterns_spec.cr 172 | +++ b/spec/lib/patterns_spec.cr 173 | " 174 | expected = ["spec/lib/patterns_spec.cr", "spec/lib/patterns_spec.cr"] 175 | matches_for("diff", input).should eq expected 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /spec/lib/priority_queue_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/priority_queue" 3 | 4 | describe PriorityQueue do 5 | it "transforms tmux status line format into escape sequences" do 6 | test = [ 7 | [3, "Clear drains"], 8 | [6, "drink tea"], 9 | [5, "Make tea"], 10 | [4, "Feed cat"], 11 | [7, "eat biscuit"], 12 | [2, "Tax return"], 13 | [1, "Solve RC tasks"], 14 | ] 15 | 16 | results = [] of String 17 | 18 | pq = PriorityQueue(String).new 19 | test.each do |pair| 20 | pr, str = pair 21 | pq.push(pr.to_i, str.to_s) 22 | end 23 | until pq.empty? 24 | results.push(pq.pop) 25 | end 26 | 27 | expected = [ 28 | "eat biscuit", 29 | "drink tea", 30 | "Make tea", 31 | "Feed cat", 32 | "Clear drains", 33 | "Tax return", 34 | "Solve RC tasks", 35 | ] 36 | 37 | results.should eq expected 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/tmux_format_printer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe TmuxFormatPrinter do 4 | let(:printer) do 5 | class FakeShell 6 | def exec(cmd) 7 | "$(#{cmd})" 8 | end 9 | end 10 | 11 | TmuxFormatPrinter.new(shell: FakeShell.new) 12 | end 13 | 14 | it 'transforms tmux status line format into escape sequences' do 15 | result = printer.print('bg=red,fg=yellow,bold', reset_styles_after: true) 16 | expected = '$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)' 17 | 18 | expect(result).to eq(expected) 19 | end 20 | 21 | it 'transforms tmux status line format into escape sequences' do 22 | result = printer.print('bg=red,fg=yellow,bold', reset_styles_after: true) 23 | expected = '$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)' 24 | 25 | expect(result).to eq(expected) 26 | end 27 | 28 | xit 'raises on unknown formats' do 29 | # TODO 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/tmux_style_printer_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../../src/tmux_style_printer" 3 | 4 | class FakeShell < TmuxStylePrinter::Shell 5 | def exec(cmd) 6 | "$(#{cmd})" 7 | end 8 | end 9 | 10 | describe TmuxStylePrinter do 11 | it "transforms tmux status line format into escape sequences" do 12 | printer = TmuxStylePrinter.new(shell = FakeShell.new) 13 | result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true) 14 | expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)" 15 | 16 | result.should eq expected 17 | end 18 | 19 | it "transforms tmux status line format into escape sequences" do 20 | printer = TmuxStylePrinter.new(shell = FakeShell.new) 21 | result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true) 22 | expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)" 23 | 24 | result.should eq expected 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/provisioning/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 6 | source $CURRENT_DIR/osx.sh 7 | else 8 | source $CURRENT_DIR/ubuntu.sh 9 | sudo usermod -a -G travis fishman 10 | fi 11 | 12 | $CURRENT_DIR/../use-tmux.sh "$CI_TMUX_VERSION" 13 | 14 | echo $PATH 15 | echo $(which tmux) 16 | 17 | bundle install 18 | 19 | # remove weird warnings in rb shell commands about world writable folder 20 | sudo chmod go-w -R /opt 21 | -------------------------------------------------------------------------------- /spec/provisioning/osx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | brew install bash gawk reattach-to-user-namespace 6 | 7 | # TODO add fishman user 8 | # http://wiki.freegeek.org/index.php/Mac_OSX_adduser_script 9 | 10 | sudo mkdir -p /opt/vagrant 11 | sudo ln -s "$PWD" /opt/vagrant/shared 12 | 13 | bundle 14 | 15 | $CURRENT_DIR/../install-tmux-versions.sh 16 | -------------------------------------------------------------------------------- /spec/provisioning/ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | sudo apt update 6 | sudo apt install -y fish gawk perl 7 | 8 | sudo useradd -m -p "$(perl -e "print crypt('fishman','sa');")" -s "/usr/bin/fish" fishman 9 | 10 | # remove system tmux and install tmux dependencies 11 | sudo aptitude remove -y tmux xsel 12 | sudo aptitude install -y libevent-dev libncurses5-dev 13 | 14 | # stub xclip globally, to avoid having to use xvfb 15 | if [[ ! -e /usr/bin/xclip ]]; then 16 | sudo ln -s $CURRENT_DIR/stubs/action-stub.sh /usr/bin/xclip 17 | fi 18 | 19 | sudo mkdir -p /opt/vagrant 20 | sudo ln -s "$PWD" /opt/vagrant/shared 21 | 22 | $CURRENT_DIR/../install-tmux-versions.sh 23 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/**" 3 | -------------------------------------------------------------------------------- /spec/stubs/action-stub.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | input=$(cat) 4 | echo "action-stub => $input" > /tmp/fingers-stub-output 5 | -------------------------------------------------------------------------------- /spec/tmux_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | require "../src/tmux" 3 | 4 | describe Tmux do 5 | it "returns a semantic version for versions without letters" do 6 | result = Tmux.tmux_version_to_semver("3.1") 7 | result.major.should eq 3 8 | result.minor.should eq 1 9 | result.patch.should eq 0 10 | end 11 | 12 | it "returns a semantic version for versions with letters" do 13 | result = Tmux.tmux_version_to_semver("3.1b") 14 | result.major.should eq 3 15 | result.minor.should eq 1 16 | result.patch.should eq 2 17 | end 18 | 19 | it "returns a semantic version for versions with letters" do 20 | result = Tmux.tmux_version_to_semver("3.3a") 21 | result.major.should eq 3 22 | result.minor.should eq 3 23 | result.patch.should eq 1 24 | end 25 | 26 | it "returns comparable semversions" do 27 | result = Tmux.tmux_version_to_semver("3.0a") >= Tmux.tmux_version_to_semver("3.1") 28 | 29 | result.should eq false 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/use-tmux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | version="$1" 4 | rm -rf /usr/bin/tmux 5 | rm -rf /usr/local/bin/tmux 6 | ln -s /opt/tmux-${version}/tmux /usr/local/bin/tmux 7 | ln -s /opt/tmux-${version}/tmux /usr/bin/tmux 8 | -------------------------------------------------------------------------------- /src/fingers.cr: -------------------------------------------------------------------------------- 1 | require "./fingers/logger" 2 | require "./fingers/cli" 3 | 4 | module Fingers 5 | VERSION = {{ %(#{`shards version`.chomp}) }} 6 | 7 | cli = Cli.new 8 | cli.run 9 | end 10 | -------------------------------------------------------------------------------- /src/fingers/action_runner.cr: -------------------------------------------------------------------------------- 1 | require "./config" 2 | 3 | module Fingers 4 | class ActionRunner 5 | @final_shell_command : String | Nil 6 | 7 | def initialize( 8 | @modifier : String, 9 | @match : String, 10 | @hint : String, 11 | @original_pane : Tmux::Pane, 12 | @offset : Tuple(Int32, Int32) | Nil, 13 | @mode : String, 14 | @main_action : String | Nil, 15 | @ctrl_action : String | Nil, 16 | @alt_action : String | Nil, 17 | @shift_action : String | Nil, 18 | ) 19 | end 20 | 21 | def run 22 | tmux.set_buffer(match) 23 | 24 | return if final_shell_command.nil? || final_shell_command.not_nil!.empty? 25 | 26 | cmd_path, *args = Process.parse_arguments(final_shell_command.not_nil!) 27 | 28 | cmd = Process.new( 29 | cmd_path, 30 | args, 31 | input: :pipe, 32 | output: :pipe, 33 | error: File.open(::Fingers::Dirs::ROOT / "action-stderr", "a"), 34 | chdir: original_pane.pane_current_path, 35 | env: action_env 36 | ) 37 | 38 | cmd.input.print(expanded_match) 39 | cmd.input.flush 40 | end 41 | 42 | private getter :match, :modifier, :hint, :original_pane, :offset, :mode, :main_action, :ctrl_action, :alt_action, :shift_action 43 | 44 | def final_shell_command 45 | return jump if mode == "jump" 46 | return @final_shell_command if @final_shell_command 47 | 48 | @final_shell_command = action_command 49 | end 50 | 51 | private def action_command 52 | case action 53 | when ":copy:" 54 | copy 55 | when ":open:" 56 | open 57 | when ":paste:" 58 | paste 59 | when nil 60 | # do nothing 61 | else 62 | shell_action 63 | end 64 | end 65 | 66 | def copy 67 | return unless system_copy_command 68 | 69 | system_copy_command 70 | end 71 | 72 | def open 73 | return unless system_open_command 74 | 75 | system_open_command 76 | end 77 | 78 | def jump 79 | return nil if offset.nil? 80 | 81 | `tmux copy-mode -t #{original_pane.pane_id}` 82 | `tmux send-keys -t #{original_pane.pane_id} -X top-line` 83 | `tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![0]} -X cursor-down` 84 | `tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![1]} -X cursor-right` 85 | 86 | nil 87 | end 88 | 89 | def paste 90 | if original_pane.pane_in_mode 91 | "tmux send-keys -t #{original_pane.pane_id} -X cancel \; paste-buffer -t #{original_pane.pane_id}" 92 | else 93 | "tmux paste-buffer -t #{original_pane.pane_id}" 94 | end 95 | end 96 | 97 | def shell_action 98 | action 99 | end 100 | 101 | def action_env 102 | {"MODIFIER" => modifier, "HINT" => hint} 103 | end 104 | 105 | private property action : String | Nil do 106 | case modifier 107 | when "main" 108 | main_action || Fingers.config.main_action 109 | when "shift" 110 | shift_action || Fingers.config.shift_action 111 | when "alt" 112 | alt_action || Fingers.config.alt_action 113 | when "ctrl" 114 | ctrl_action || Fingers.config.ctrl_action 115 | end 116 | end 117 | 118 | def system_copy_command 119 | @system_copy_command ||= if program_exists?("pbcopy") 120 | if program_exists?("reattach-to-user-namespace") 121 | "reattach-to-user-namespace" 122 | else 123 | "pbcopy" 124 | end 125 | elsif program_exists?("clip.exe") 126 | "cat | clip.exe" 127 | elsif program_exists?("wl-copy") 128 | "wl-copy" 129 | elsif program_exists?("xclip") 130 | "xclip -selection clipboard" 131 | elsif program_exists?("xsel") 132 | "xsel -i --clipboard" 133 | elsif program_exists?("putclip") 134 | "putclip" 135 | end 136 | end 137 | 138 | def system_open_command 139 | @system_open_command ||= if program_exists?("cygstart") 140 | "xargs cygstart" 141 | elsif program_exists?("xdg-open") 142 | "xargs xdg-open" 143 | elsif program_exists?("open") 144 | "xargs open" 145 | end 146 | end 147 | 148 | def program_exists?(program) 149 | Process.find_executable(program) 150 | end 151 | 152 | def tmux 153 | Tmux.new(Fingers.config.tmux_version) 154 | end 155 | 156 | # This takes care of some path expansion weirdness when opening paths that start with ~ in MacOS 157 | def expanded_match 158 | return match unless should_expand_match? 159 | 160 | Path[match].expand(base: original_pane.pane_current_path, home: Path.home) 161 | end 162 | 163 | private def should_expand_match? 164 | action == ":open:" && match.starts_with?("~") 165 | end 166 | 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /src/fingers/cli.cr: -------------------------------------------------------------------------------- 1 | require "./commands/*" 2 | require "cling" 3 | 4 | module Fingers 5 | class MainCommand < Cling::Command 6 | def setup : Nil 7 | @description = "description" 8 | @name = "tmux-fingers" 9 | add_command Fingers::Commands::Version.new 10 | add_command Fingers::Commands::LoadConfig.new 11 | add_command Fingers::Commands::SendInput.new 12 | add_command Fingers::Commands::Start.new 13 | add_command Fingers::Commands::Info.new 14 | end 15 | 16 | def run(arguments, options) : Nil 17 | puts help_template 18 | end 19 | end 20 | 21 | class Cli 22 | def run 23 | main = MainCommand.new 24 | 25 | main.execute ARGV 26 | end 27 | end 28 | end 29 | 30 | # fingers load-config 31 | # fingers version 32 | # fingers send-input INPUT 33 | # fingers start --mode default|jump --pane #{pane_id} 34 | -------------------------------------------------------------------------------- /src/fingers/commands/base.cr: -------------------------------------------------------------------------------- 1 | module Fingers::Commands 2 | class Base 3 | @args : Array(String) 4 | 5 | def initialize(args) 6 | @args = args 7 | end 8 | 9 | def run 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/fingers/commands/info.cr: -------------------------------------------------------------------------------- 1 | require "cling" 2 | require "tablo" 3 | 4 | class Fingers::Commands::Info < Cling::Command 5 | WIZARD_INSTALLATION_METHOD = {{ env("WIZARD_INSTALLATION_METHOD") }} 6 | 7 | def setup : Nil 8 | @name = "info" 9 | end 10 | 11 | def run(arguments, options) : Nil 12 | data = [ 13 | ["tmux-fingers", "#{Fingers::VERSION}"], 14 | ["xdg-root-folder", "#{Fingers::Dirs::ROOT}"], 15 | ["log-path", "#{Fingers::Dirs::LOG_PATH}"], 16 | ["installation-method", "#{WIZARD_INSTALLATION_METHOD || "manual"}"], 17 | ["tmux-version", `tmux -V`.chomp], 18 | ["crystal-version", Crystal::VERSION] 19 | ] 20 | 21 | opt_width = data.map { |n| n[0].size }.max 22 | val_width = data.map { |n| n[1].size }.max 23 | 24 | table = Tablo::Table.new(data, header_frequency: nil) do |t| 25 | t.add_column("Option", width: opt_width) { |n| n[0] } 26 | t.add_column("Value", width: val_width) { |n| n[1] } 27 | end 28 | puts table 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/fingers/commands/load_config.cr: -------------------------------------------------------------------------------- 1 | require "cling" 2 | require "file_utils" 3 | require "./base" 4 | require "../dirs" 5 | require "../config" 6 | require "../../tmux" 7 | 8 | class Fingers::Commands::LoadConfig < Cling::Command 9 | @fingers_options_names : Array(String) | Nil 10 | 11 | property config : Fingers::Config = Fingers::Config.new 12 | 13 | DISALLOWED_CHARS = /[cimqn]/ 14 | 15 | PRIVATE_OPTIONS = [ 16 | "skip_wizard", 17 | "cli" 18 | ] 19 | 20 | def setup : Nil 21 | @config = Fingers::Config.new 22 | @name = "load-config" 23 | end 24 | 25 | def run(arguments, options) : Nil 26 | validate_options! 27 | parse_tmux_conf 28 | setup_bindings 29 | end 30 | 31 | # private 32 | 33 | def parse_tmux_conf 34 | options = shell_safe_options 35 | 36 | user_defined_patterns = [] of Tuple(String, String) 37 | 38 | Fingers.reset_config 39 | 40 | config.tmux_version = `tmux -V`.chomp.split(" ").last 41 | 42 | options.each do |option, value| 43 | # TODO generate an enum somehow and use an exhaustive case 44 | case option 45 | when "key" 46 | config.key = value 47 | when "jump_key" 48 | config.jump_key = value 49 | when "keyboard_layout" 50 | config.keyboard_layout = value 51 | when "main_action" 52 | config.main_action = value 53 | when "ctrl_action" 54 | config.ctrl_action = value 55 | when "alt_action" 56 | config.alt_action = value 57 | when "shift_action" 58 | config.shift_action = value 59 | when "benchmark_mode" 60 | config.benchmark_mode = value 61 | when "hint_position" 62 | config.hint_position = value 63 | when "hint_style" 64 | config.hint_style = tmux.parse_style(value) 65 | when "selected_hint_style" 66 | config.selected_hint_style = tmux.parse_style(value) 67 | when "highlight_style" 68 | config.highlight_style = tmux.parse_style(value) 69 | when "backdrop_style" 70 | config.backdrop_style = tmux.parse_style(value) 71 | when "selected_highlight_style" 72 | config.selected_highlight_style = tmux.parse_style(value) 73 | when "show_copied_notification" 74 | config.show_copied_notification = value 75 | when "enabled_builtin_patterns" 76 | config.enabled_builtin_patterns = value 77 | end 78 | 79 | if option.match(/^pattern/) && !value.empty? 80 | check_pattern!(value) 81 | user_defined_patterns.push({ option.gsub(/^pattern_/, ""), value }) 82 | end 83 | end 84 | 85 | add_user_defined_patterns(user_defined_patterns) 86 | add_builtin_patterns 87 | 88 | config.alphabet = ::Fingers::Config::ALPHABET_MAP[Fingers.config.keyboard_layout].split("").reject do |char| 89 | char.match(DISALLOWED_CHARS) 90 | end 91 | 92 | config.save 93 | 94 | Fingers.reset_config 95 | rescue e : TmuxStylePrinter::InvalidFormat 96 | puts "[tmux-fingers] #{e.message}" 97 | exit(1) 98 | end 99 | 100 | def add_user_defined_patterns(patterns : Array(Tuple(String, String))) 101 | patterns.each do |p| 102 | name, pattern = p 103 | 104 | config.patterns[name] = pattern 105 | end 106 | end 107 | 108 | def add_builtin_patterns 109 | pattern_names = [] of String 110 | 111 | if config.enabled_builtin_patterns == "all" 112 | pattern_names = ::Fingers::Config::BUILTIN_PATTERNS.keys 113 | else 114 | pattern_names = config.enabled_builtin_patterns.split(",") 115 | end 116 | 117 | pattern_names.each do |name| 118 | pattern = Fingers::Config::BUILTIN_PATTERNS[name]? 119 | config.patterns[name.to_s] = pattern if pattern 120 | end 121 | end 122 | 123 | def setup_bindings 124 | `tmux bind-key #{Fingers.config.key} run-shell -b "#{cli} start "\#{pane_id}" >>#{Fingers::Dirs::LOG_PATH} 2>&1"` 125 | `tmux bind-key #{Fingers.config.jump_key} run-shell -b "#{cli} start --mode jump "\#{pane_id}" >>#{Fingers::Dirs::LOG_PATH} 2>&1"` 126 | setup_fingers_mode_bindings 127 | `tmux set-option -g @fingers-cli #{cli}` 128 | end 129 | 130 | def setup_fingers_mode_bindings 131 | ("a".."z").to_a.each do |char| 132 | next if char.match(DISALLOWED_CHARS) 133 | 134 | fingers_mode_bind(char, "hint:#{char}:main") 135 | fingers_mode_bind(char.upcase, "hint:#{char}:shift") 136 | fingers_mode_bind("C-#{char}", "hint:#{char}:ctrl") 137 | fingers_mode_bind("M-#{char}", "hint:#{char}:alt") 138 | end 139 | 140 | fingers_mode_bind("Space", "fzf") 141 | fingers_mode_bind("C-c", "exit") 142 | fingers_mode_bind("q", "exit") 143 | fingers_mode_bind("Escape", "exit") 144 | 145 | fingers_mode_bind("?", "toggle-help") 146 | 147 | fingers_mode_bind("Enter", "noop") 148 | fingers_mode_bind("Tab", "toggle-multi-mode") 149 | 150 | fingers_mode_bind("Any", "noop") 151 | end 152 | 153 | def enabled_default_patterns 154 | ::Fingers::Config::BUILTIN_PATTERNS.values 155 | end 156 | 157 | def to_bool(input) 158 | input == "1" 159 | end 160 | 161 | def shell_safe_options 162 | options = {} of String => String 163 | 164 | fingers_options_names.each do |option| 165 | option_method = option_to_method(option) 166 | 167 | options[option_method] = `tmux show-option -gv #{option}`.chomp 168 | end 169 | 170 | options 171 | end 172 | 173 | def valid_option?(option) 174 | option_method = option_to_method(option) 175 | 176 | config.members.includes?(option_method) || option_method.match(/^pattern_+/) || PRIVATE_OPTIONS.includes?(option_method) 177 | end 178 | 179 | def fingers_options_names 180 | @fingers_options_names ||= `tmux show-options -g | grep ^@fingers` 181 | .chomp.split("\n") 182 | .map { |line| line.split(" ")[0] } 183 | .reject { |option| option.empty? } 184 | end 185 | 186 | def unset_tmux_option!(option) 187 | `tmux set-option -ug #{option}` 188 | end 189 | 190 | def check_pattern!(pattern) 191 | begin 192 | Regex.new(pattern) 193 | rescue e: ArgumentError 194 | puts "[tmux-fingers] Invalid pattern: #{pattern}" 195 | puts "[tmux-fingers] #{e.message}" 196 | exit(1) 197 | end 198 | end 199 | 200 | def validate_options! 201 | errors = [] of String 202 | 203 | fingers_options_names.each do |option| 204 | unless valid_option?(option) 205 | errors << "'#{option}' is not a valid option" 206 | unset_tmux_option!(option) 207 | end 208 | end 209 | 210 | return if errors.empty? 211 | 212 | puts "[tmux-fingers] Errors found in tmux.conf:" 213 | errors.each { |error| puts " - #{error}" } 214 | exit(1) 215 | end 216 | 217 | def option_to_method(option) 218 | option.gsub(/^@fingers-/, "").tr("-", "_") 219 | end 220 | 221 | def fingers_mode_bind(key, command) 222 | `tmux bind-key -Tfingers "#{key}" run-shell -b "#{cli} send-input #{command}"` 223 | end 224 | 225 | def cli 226 | Process.executable_path 227 | end 228 | 229 | def tmux 230 | Tmux.new(`tmux -V`.chomp.split(" ").last) 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /src/fingers/commands/send_input.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "cling" 3 | 4 | module Fingers::Commands 5 | class SendInput < Cling::Command 6 | def setup : Nil 7 | @name = "send-input" 8 | @hidden = true 9 | add_argument "input", required: true 10 | end 11 | 12 | def run(arguments, options) : Nil 13 | socket = InputSocket.new 14 | 15 | socket.send_message(arguments.get("input")) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/fingers/commands/start.cr: -------------------------------------------------------------------------------- 1 | require "cling" 2 | require "./base" 3 | require "../hinter" 4 | require "../view" 5 | require "../state" 6 | require "../input_socket" 7 | require "../../tmux" 8 | 9 | module Fingers::Commands 10 | class PanePrinter < Fingers::Printer 11 | @pane_tty : String 12 | @file : File 13 | 14 | def initialize(pane_tty) 15 | @pane_tty = pane_tty 16 | @file = File.open(@pane_tty, "w") 17 | end 18 | 19 | def print(msg) 20 | @file.print(msg) 21 | end 22 | 23 | def flush 24 | @file.flush 25 | end 26 | end 27 | 28 | class Start < Cling::Command 29 | @original_options : Hash(String, String) = {} of String => String 30 | @last_key_table : String = "root" 31 | @last_pane_id : String | Nil 32 | @mode : String = "default" 33 | @pane_id : String = "" 34 | @active_pane : Tmux::Pane | Nil 35 | @patterns : Array(String) = [] of String 36 | @main_action : String | Nil 37 | @ctrl_action : String | Nil 38 | @shift_action : String | Nil 39 | @alt_action : String | Nil 40 | 41 | def setup : Nil 42 | @name = "start" 43 | add_argument "pane_id", 44 | description: "pane id (also accepts tmux target-pane tokens specified in tmux man pages)", 45 | required: true 46 | add_option "mode", 47 | description: "can be \"jump\" or \"default\"", 48 | type: :single, 49 | default: "default" 50 | 51 | add_option "patterns", 52 | description: "comma separated list of pattern names", 53 | type: :single 54 | 55 | add_option "main-action", 56 | description: "command to which the output will be piped", 57 | type: :single 58 | 59 | add_option "ctrl-action", 60 | description: "command to which the output will be piped when holding CTRL key", 61 | type: :single 62 | 63 | add_option "alt-action", 64 | description: "command to which the output will be piped when holding ALT key", 65 | type: :single 66 | 67 | add_option "shift-action", 68 | description: "command to which the output will be pipedwhen holding SHIFT key", 69 | type: :single 70 | 71 | add_option 'h', "help", description: "prints help" 72 | end 73 | 74 | def pre_run(arguments, options) : Bool 75 | if options.has?("help") 76 | puts help_template 77 | false 78 | else 79 | true 80 | end 81 | end 82 | 83 | def run(arguments, options) : Nil 84 | @mode = options.get("mode").as_s 85 | parse_pane_target_format!(arguments.get("pane_id").as_s) 86 | 87 | if options.has?("patterns") 88 | @patterns = patterns_from_options(options.get("patterns").as_s) 89 | else 90 | @patterns = Fingers.config.patterns.values 91 | end 92 | 93 | @main_action = options.get?("main-action").try(&.as_s) 94 | @ctrl_action = options.get?("ctrl-action").try(&.as_s) 95 | @alt_action = options.get?("alt-action").try(&.as_s) 96 | @shift_action = options.get?("shift-action").try(&.as_s) 97 | 98 | track_tmux_state 99 | 100 | show_hints 101 | 102 | if Fingers.config.benchmark_mode == "1" 103 | exit(0) 104 | end 105 | 106 | handle_input 107 | process_result 108 | teardown 109 | end 110 | 111 | private def patterns_from_options(pattern_names_option : String) 112 | pattern_names = pattern_names_option.split(",") 113 | 114 | result = [] of String 115 | 116 | pattern_names.each do |pattern_name| 117 | pattern = Fingers.config.patterns[pattern_name]? 118 | if pattern 119 | result << pattern 120 | else 121 | tmux.display_message("[tmux-fingers] error: Unknown pattern #{pattern_name}", 5000) 122 | exit 0 123 | end 124 | end 125 | 126 | result 127 | end 128 | 129 | private def track_tmux_state 130 | output = tmux.exec("display-message -t '{last}' -p '\#{pane_id};\#{client_key_table};\#{prefix};\#{prefix2}'").chomp 131 | 132 | last_pane_id, last_key_table, prefix, prefix2 = output.split(";") 133 | 134 | @last_pane_id = last_pane_id 135 | @last_key_table = if last_key_table.empty? 136 | "root" 137 | else 138 | last_key_table 139 | end 140 | 141 | @original_options["prefix"] = prefix 142 | @original_options["prefix2"] = prefix2 143 | end 144 | 145 | private def restore_options 146 | @original_options.each do |option, value| 147 | tmux.set_global_option(option, value) 148 | end 149 | end 150 | 151 | private def restore_last_key_table 152 | tmux.set_key_table(@last_key_table) 153 | end 154 | 155 | private def restore_last_pane 156 | tmux.select_pane(@last_pane_id) 157 | select_active_pane 158 | end 159 | 160 | private def options_to_preserve 161 | %w[prefix prefix2] 162 | end 163 | 164 | private def parse_pane_target_format!(pane_target_format) 165 | if pane_target_format.match(/^%[0-9]+$/) 166 | @pane_id = pane_target_format 167 | @active_pane = target_pane 168 | else 169 | @pane_id = tmux.exec("display-message -t #{pane_target_format} -p '\#{pane_id}'").chomp 170 | @active_pane = tmux.list_panes("\#{pane_active}", target_pane.window_id).first 171 | end 172 | end 173 | 174 | private def show_hints 175 | # Attention! It is very important to resize the window at this point to 176 | # match the dimensions of the target pane. Otherwise weird linejumping 177 | # will occur when we have wrapped lines. 178 | tmux.resize_window( 179 | fingers_window.window_id, 180 | target_pane.pane_width, 181 | target_pane.pane_height, 182 | ) if needs_resize? 183 | 184 | view.render 185 | tmux.swap_panes(fingers_window.pane_id, target_pane.pane_id) 186 | end 187 | 188 | private def handle_input 189 | input_socket = InputSocket.new 190 | 191 | tmux.disable_prefix 192 | tmux.set_key_table "fingers" 193 | 194 | input_socket.on_input do |input| 195 | view.process_input(input) 196 | break if state.exiting 197 | end 198 | end 199 | 200 | private def process_result 201 | return unless state.result 202 | 203 | match = hinter.lookup(state.input) 204 | 205 | ActionRunner.new( 206 | hint: state.input, 207 | modifier: state.modifier, 208 | match: state.result, 209 | original_pane: active_pane, 210 | offset: match ? match.not_nil!.offset : nil, 211 | mode: mode, 212 | main_action: @main_action, 213 | ctrl_action: @ctrl_action, 214 | alt_action: @alt_action, 215 | shift_action: @shift_action, 216 | ).run 217 | 218 | tmux.display_message("Copied: #{state.result}", 1000) if should_notify? 219 | end 220 | 221 | private def select_active_pane 222 | tmux.select_pane(active_pane.pane_id) 223 | end 224 | 225 | private def needs_resize? 226 | pane_width = target_pane.pane_width.to_i 227 | pane_contents.any? { |line| line.size > pane_width } 228 | end 229 | 230 | private def teardown 231 | tmux.swap_panes(fingers_pane_id, target_pane.pane_id) 232 | tmux.kill_pane(fingers_pane_id) 233 | 234 | restore_last_pane 235 | restore_last_key_table 236 | restore_options 237 | end 238 | 239 | private getter target_pane : Tmux::Pane do 240 | tmux.find_pane_by_id(@pane_id).not_nil! 241 | end 242 | 243 | private getter active_pane : Tmux::Pane do 244 | @active_pane.not_nil! 245 | end 246 | 247 | private getter mode : String do 248 | @mode.not_nil! 249 | end 250 | 251 | private getter fingers_window : Tmux::Window do 252 | tmux.create_window("[fingers]", "cat", 80, 24) 253 | end 254 | 255 | private getter fingers_pane_id : String do 256 | fingers_window.pane_id 257 | end 258 | 259 | private getter pane_printer : PanePrinter do 260 | PanePrinter.new(fingers_window.pane_tty) 261 | end 262 | 263 | private getter state : Fingers::State do 264 | ::Fingers::State.new 265 | end 266 | 267 | private getter hinter : Hinter do 268 | Fingers::Hinter.new( 269 | input: pane_contents, 270 | patterns: @patterns, 271 | width: target_pane.pane_width.to_i, 272 | state: state, 273 | output: pane_printer, 274 | reuse_hints: mode != "jump", 275 | ) 276 | end 277 | 278 | private getter pane_contents : Array(String) do 279 | tmux.capture_pane(target_pane, join: mode != "jump").split("\n") 280 | end 281 | 282 | private getter view : View do 283 | ::Fingers::View.new( 284 | hinter: hinter, 285 | state: state, 286 | output: pane_printer, 287 | original_pane: target_pane, 288 | tmux: tmux, 289 | mode: mode, 290 | ) 291 | end 292 | 293 | private getter tmux : Tmux do 294 | Tmux.new(Fingers.config.tmux_version) 295 | end 296 | 297 | private def should_notify? 298 | !state.result.empty? && Fingers.config.show_copied_notification == "1" 299 | end 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /src/fingers/commands/version.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | require "cling" 3 | 4 | module Fingers::Commands 5 | class Version < Cling::Command 6 | def setup : Nil 7 | @name = "version" 8 | @description = "Duh." 9 | end 10 | 11 | def run(arguments, options) : Nil 12 | puts "#{Fingers::VERSION}" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/fingers/config.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module Fingers 4 | struct Config 5 | include JSON::Serializable 6 | 7 | property key : String 8 | property jump_key : String 9 | property keyboard_layout : String 10 | property patterns : Hash(String, String) 11 | property alphabet : Array(String) 12 | property benchmark_mode : String 13 | property main_action : String 14 | property ctrl_action : String 15 | property alt_action : String 16 | property shift_action : String 17 | property hint_position : String 18 | property hint_style : String 19 | property selected_hint_style : String 20 | property highlight_style : String 21 | property selected_highlight_style : String 22 | property backdrop_style : String 23 | property tmux_version : String 24 | property show_copied_notification : String 25 | property enabled_builtin_patterns : String 26 | 27 | FORMAT_PRINTER = TmuxStylePrinter.new 28 | 29 | BUILTIN_PATTERNS = { 30 | "ip": "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}", 31 | "uuid": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", 32 | "sha": "[0-9a-f]{7,128}", 33 | "digit": "[0-9]{4,}", 34 | "url": "((https?://|git@|git://|ssh://|ftp://|file:///)[^\\s()\"']+)", 35 | "path": "(([.\\w\\-~\\$@]+)?(/[.\\w\\-@]+)+/?)", 36 | "hex": "(0x[0-9a-fA-F]+)", 37 | "kubernetes": "(deployment.app|binding|componentstatuse|configmap|endpoint|event|limitrange|namespace|node|persistentvolumeclaim|persistentvolume|pod|podtemplate|replicationcontroller|resourcequota|secret|serviceaccount|service|mutatingwebhookconfiguration.admissionregistration.k8s.io|validatingwebhookconfiguration.admissionregistration.k8s.io|customresourcedefinition.apiextension.k8s.io|apiservice.apiregistration.k8s.io|controllerrevision.apps|daemonset.apps|deployment.apps|replicaset.apps|statefulset.apps|tokenreview.authentication.k8s.io|localsubjectaccessreview.authorization.k8s.io|selfsubjectaccessreviews.authorization.k8s.io|selfsubjectrulesreview.authorization.k8s.io|subjectaccessreview.authorization.k8s.io|horizontalpodautoscaler.autoscaling|cronjob.batch|job.batch|certificatesigningrequest.certificates.k8s.io|events.events.k8s.io|daemonset.extensions|deployment.extensions|ingress.extensions|networkpolicies.extensions|podsecuritypolicies.extensions|replicaset.extensions|networkpolicie.networking.k8s.io|poddisruptionbudget.policy|clusterrolebinding.rbac.authorization.k8s.io|clusterrole.rbac.authorization.k8s.io|rolebinding.rbac.authorization.k8s.io|role.rbac.authorization.k8s.io|storageclasse.storage.k8s.io)[[:alnum:]_#$%&+=/@-]+", 38 | "git-status": "(modified|deleted|deleted by us|new file): +(?.+)", 39 | "git-status-branch": "Your branch is up to date with '(?.*)'.", 40 | "diff": "(---|\\+\\+\\+) [ab]/(?.*)", 41 | } 42 | 43 | ALPHABET_MAP = { 44 | "qwerty": "asdfqwerzxcvjklmiuopghtybn", 45 | "qwerty-homerow": "asdfjklgh", 46 | "qwerty-left-hand": "asdfqwerzcxv", 47 | "qwerty-right-hand": "jkluiopmyhn", 48 | "azerty": "qsdfazerwxcvjklmuiopghtybn", 49 | "azerty-homerow": "qsdfjkmgh", 50 | "azerty-left-hand": "qsdfazerwxcv", 51 | "azerty-right-hand": "jklmuiophyn", 52 | "qwertz": "asdfqweryxcvjkluiopmghtzbn", 53 | "qwertz-homerow": "asdfghjkl", 54 | "qwertz-left-hand": "asdfqweryxcv", 55 | "qwertz-right-hand": "jkluiopmhzn", 56 | "dvorak": "aoeuqjkxpyhtnsgcrlmwvzfidb", 57 | "dvorak-homerow": "aoeuhtnsid", 58 | "dvorak-left-hand": "aoeupqjkyix", 59 | "dvorak-right-hand": "htnsgcrlmwvz", 60 | "colemak": "arstqwfpzxcvneioluymdhgjbk", 61 | "colemak-homerow": "arstneiodh", 62 | "colemak-left-hand": "arstqwfpzxcv", 63 | "colemak-right-hand": "neioluymjhk", 64 | } 65 | 66 | def initialize( 67 | @key = "F", 68 | @jump_key = "J", 69 | @keyboard_layout = "qwerty", 70 | @alphabet = [] of String, 71 | @patterns = {} of String => String, 72 | @main_action = ":copy:", 73 | @ctrl_action = ":open:", 74 | @alt_action = "", 75 | @shift_action = ":paste:", 76 | @hint_position = "left", 77 | @hint_style = FORMAT_PRINTER.print("fg=green,bold"), 78 | @highlight_style = FORMAT_PRINTER.print("fg=yellow"), 79 | @selected_hint_style = FORMAT_PRINTER.print("fg=blue,bold"), 80 | @selected_highlight_style = FORMAT_PRINTER.print("fg=blue"), 81 | @backdrop_style = "", 82 | @tmux_version = "3.1", 83 | @show_copied_notification = "0", 84 | @enabled_builtin_patterns = "all", 85 | @benchmark_mode = "0" 86 | ) 87 | end 88 | 89 | def self.load_from_cache 90 | Config.from_json(File.open(::Fingers::Dirs::CONFIG_PATH)) 91 | end 92 | 93 | def save 94 | to_json(File.open(::Fingers::Dirs::CONFIG_PATH, "w")) 95 | end 96 | 97 | def members : Array(String) 98 | JSON.parse(to_json).as_h.keys 99 | end 100 | end 101 | 102 | def self.config 103 | @@config ||= Config.load_from_cache 104 | rescue 105 | @@config ||= Config.new 106 | end 107 | 108 | def self.reset_config 109 | @@config = nil 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /src/fingers/dirs.cr: -------------------------------------------------------------------------------- 1 | require "file_utils" 2 | require "xdg_base_directory" 3 | 4 | module Fingers::Dirs 5 | TMUX_PID = (ENV.fetch("TMUX", ",0000")).split(",")[1] 6 | XDG = XdgBaseDirectory.app_directories("tmux-fingers") 7 | 8 | TMP = Path[File.dirname(File.tempname)] 9 | 10 | ROOT = Path[XDG.state.file_path("tmux-#{TMUX_PID}")] 11 | 12 | {% if env("FINGERS_LOG_PATH") %} 13 | # used in development to read logs outside container more easily 14 | LOG_PATH = {{ env("FINGERS_LOG_PATH") }} 15 | {% else %} 16 | LOG_PATH = Path[XDG.state.file_path("fingers.log")] 17 | {% end %} 18 | 19 | CACHE = ROOT 20 | CONFIG_PATH = CACHE / "config.json" 21 | SOCKET_PATH = CACHE / "fingers.sock" 22 | 23 | def self.ensure_folders! 24 | Fingers::Dirs::XDG.state.mkdir unless Fingers::Dirs::XDG.state.exists? 25 | FileUtils.mkdir_p(Fingers::Dirs::ROOT) unless File.exists?(Fingers::Dirs::ROOT) 26 | end 27 | 28 | Fingers::Dirs.ensure_folders! 29 | end 30 | -------------------------------------------------------------------------------- /src/fingers/hinter.cr: -------------------------------------------------------------------------------- 1 | require "../huffman" 2 | require "./config" 3 | require "./match_formatter" 4 | require "./types" 5 | 6 | module Fingers 7 | struct Target 8 | property text : String 9 | property hint : String 10 | property offset : Tuple(Int32, Int32) 11 | 12 | def initialize(@text, @hint, @offset) 13 | end 14 | end 15 | 16 | class Hinter 17 | @formatter : Formatter 18 | @patterns : Array(String) 19 | @alphabet : Array(String) 20 | @pattern : Regex | Nil 21 | @hints : Array(String) | Nil 22 | @n_matches : Int32 | Nil 23 | @reuse_hints : Bool 24 | 25 | def initialize( 26 | input : Array(String), 27 | width : Int32, 28 | state : Fingers::State, 29 | output : Printer, 30 | patterns = Fingers.config.patterns, 31 | alphabet = Fingers.config.alphabet, 32 | huffman = Huffman.new, 33 | formatter = ::Fingers::MatchFormatter.new, 34 | reuse_hints = false 35 | ) 36 | @lines = input 37 | @width = width 38 | @target_by_hint = {} of String => Target 39 | @target_by_text = {} of String => Target 40 | @state = state 41 | @output = output 42 | @formatter = formatter 43 | @huffman = huffman 44 | @patterns = patterns 45 | @alphabet = alphabet 46 | @reuse_hints = reuse_hints 47 | end 48 | 49 | def run 50 | regenerate_hints! 51 | lines[0..-2].each_with_index { |line, index| process_line(line, index, "\n") } 52 | process_line(lines[-1], lines.size - 1, "") 53 | 54 | output.flush 55 | end 56 | 57 | def lookup(hint) : Target | Nil 58 | target_by_hint.fetch(hint) { nil } 59 | end 60 | 61 | # private 62 | 63 | private getter :hints, 64 | :hints_by_text, 65 | :offsets_by_hint, 66 | :input, 67 | :lookup_table, 68 | :width, 69 | :state, 70 | :formatter, 71 | :huffman, 72 | :output, 73 | :patterns, 74 | :alphabet, 75 | :reuse_hints, 76 | :target_by_hint, 77 | :target_by_text 78 | 79 | def process_line(line, line_index, ending) 80 | result = line.gsub(pattern) { |_m| replace($~, line_index) } 81 | result = Fingers.config.backdrop_style + result 82 | double_width_correction = ((line.bytesize - line.size) / 3).round.to_i 83 | padding_amount = (width - line.size - double_width_correction) 84 | padding = padding_amount > 0 ? " " * padding_amount : "" 85 | output.print(result + padding + ending) 86 | end 87 | 88 | def pattern : Regex 89 | @pattern ||= Regex.new("(#{patterns.join('|')})") 90 | end 91 | 92 | def hints : Array(String) 93 | return @hints.as(Array(String)) if !@hints.nil? 94 | 95 | regenerate_hints! 96 | 97 | @hints.as(Array(String)) 98 | end 99 | 100 | def regenerate_hints! 101 | @hints = huffman.generate_hints(alphabet: alphabet.clone, n: n_matches) 102 | @target_by_hint.clear 103 | @target_by_text.clear 104 | end 105 | 106 | def replace(match, line_index) 107 | text = match[0] 108 | 109 | captured_text = captured_text_for_match(match) 110 | relative_capture_offset = relative_capture_offset_for_match(match, captured_text) 111 | 112 | absolute_offset = { 113 | line_index, 114 | match.begin(0) + (relative_capture_offset ? relative_capture_offset[0] : 0) 115 | } 116 | 117 | hint = hint_for_text(captured_text) 118 | build_target(captured_text, hint, absolute_offset) 119 | 120 | if !state.input.empty? && !hint.starts_with?(state.input) 121 | return text 122 | end 123 | 124 | formatter.format( 125 | hint: hint, 126 | highlight: text, 127 | selected: state.selected_hints.includes?(hint), 128 | offset: relative_capture_offset 129 | ) 130 | end 131 | 132 | def captured_text_for_match(match) 133 | match["match"]? || match[0] 134 | end 135 | 136 | def hint_for_text(text) 137 | return pop_hint! unless reuse_hints 138 | 139 | target = target_by_text[text]? 140 | 141 | if target.nil? 142 | return pop_hint! 143 | end 144 | 145 | target.hint 146 | end 147 | 148 | def pop_hint! : String 149 | hint = hints.pop? 150 | 151 | if hint.nil? 152 | raise "Too many matches" 153 | end 154 | 155 | hint 156 | end 157 | 158 | def relative_capture_offset_for_match(match, captured_text) 159 | return nil unless match["match"]? 160 | 161 | match_start, match_end = {match.begin(0), match.end(0)} 162 | capture_start, capture_end = find_capture_offset(match).not_nil! 163 | {capture_start - match_start, captured_text.size} 164 | end 165 | 166 | def build_target(text, hint, offset) 167 | target = Target.new(text, hint, offset) 168 | 169 | target_by_hint[hint] = target 170 | target_by_text[text] = target 171 | 172 | target 173 | end 174 | 175 | def find_capture_offset(match : Regex::MatchData) : Tuple(Int32, Int32) | Nil 176 | index = capture_indices.find { |i| match[i]? } 177 | 178 | return nil unless index 179 | 180 | {match.begin(index), match.end(index)} 181 | end 182 | 183 | getter capture_indices : Array(Int32) do 184 | pattern.name_table.compact_map { |k, v| v == "match" ? k : nil } 185 | end 186 | 187 | def n_matches : Int32 188 | return @n_matches.as(Int32) if !@n_matches.nil? 189 | 190 | if reuse_hints 191 | @n_matches = count_unique_matches 192 | else 193 | @n_matches = count_matches 194 | end 195 | end 196 | 197 | def count_unique_matches 198 | match_set = Set(String).new 199 | 200 | lines.each do |line| 201 | line.scan(pattern) do |match| 202 | match_set.add(captured_text_for_match(match)) 203 | end 204 | end 205 | 206 | @n_matches = match_set.size 207 | 208 | match_set.size 209 | end 210 | 211 | def count_matches 212 | result = 0 213 | 214 | lines.each do |line| 215 | line.scan(pattern) do |match| 216 | result += 1 217 | end 218 | end 219 | 220 | result 221 | end 222 | 223 | private property lines : Array(String) 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /src/fingers/input_socket.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "./dirs" 3 | 4 | module Fingers 5 | class InputSocket 6 | @path : String 7 | 8 | def initialize(path = Fingers::Dirs::SOCKET_PATH.to_s) 9 | @path = path 10 | end 11 | 12 | def on_input 13 | remove_socket_file 14 | 15 | loop do 16 | socket = server.accept 17 | message = socket.gets 18 | 19 | yield (message || "") 20 | end 21 | end 22 | 23 | def send_message(cmd) 24 | socket = UNIXSocket.new(path) 25 | socket.puts(cmd) 26 | socket.close 27 | end 28 | 29 | def close 30 | server.close 31 | remove_socket_file 32 | end 33 | 34 | private getter :path 35 | 36 | def server 37 | @server ||= UNIXServer.new(path) 38 | end 39 | 40 | def remove_socket_file 41 | `rm -rf #{path}` 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /src/fingers/logger.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | require "../fingers/dirs" 3 | 4 | module Fingers 5 | Log.setup(:debug, Log::IOBackend.new(File.new(Dirs::LOG_PATH, "a+"))) 6 | end 7 | -------------------------------------------------------------------------------- /src/fingers/match_formatter.cr: -------------------------------------------------------------------------------- 1 | require "./config" 2 | require "./types" 3 | 4 | module Fingers 5 | class MatchFormatter < Fingers::Formatter 6 | def initialize( 7 | hint_style : String = Fingers.config.hint_style, 8 | highlight_style : String = Fingers.config.highlight_style, 9 | selected_hint_style : String = Fingers.config.selected_hint_style, 10 | selected_highlight_style : String = Fingers.config.selected_highlight_style, 11 | backdrop_style : String = Fingers.config.backdrop_style, 12 | hint_position : String = Fingers.config.hint_position, 13 | reset_sequence : String = "\e[0m" 14 | ) 15 | @hint_style = hint_style 16 | @highlight_style = highlight_style 17 | @selected_hint_style = selected_hint_style 18 | @selected_highlight_style = selected_highlight_style 19 | @backdrop_style = backdrop_style 20 | @hint_position = hint_position 21 | @reset_sequence = reset_sequence 22 | end 23 | 24 | def format(hint : String, highlight : String, selected : Bool, offset : Tuple(Int32, Int32) | Nil) 25 | reset_sequence + before_offset(offset, highlight) + 26 | format_offset(selected, hint, within_offset(offset, highlight)) + 27 | after_offset(offset, highlight) + backdrop_style 28 | end 29 | 30 | private getter :hint_style, :highlight_style, :selected_hint_style, :selected_highlight_style, :hint_position, :reset_sequence, :backdrop_style 31 | 32 | private def before_offset(offset, highlight) 33 | return "" if offset.nil? 34 | start, _ = offset 35 | backdrop_style + highlight[0..(start - 1)] 36 | end 37 | 38 | private def within_offset(offset, highlight) 39 | return highlight if offset.nil? 40 | start, length = offset 41 | highlight[start..(start + length - 1)] 42 | end 43 | 44 | private def after_offset(offset, highlight) 45 | return "" if offset.nil? 46 | start, length = offset 47 | backdrop_style + highlight[(start + length)..] 48 | end 49 | 50 | private def format_offset(selected, hint, highlight) 51 | chopped_highlight = chop_highlight(hint, highlight) 52 | 53 | hint_pair = (selected ? selected_hint_style : hint_style) + hint 54 | highlight_pair = (selected ? selected_highlight_style : highlight_style) + chopped_highlight 55 | 56 | if hint_position == "right" 57 | highlight_pair + reset_sequence + hint_pair + reset_sequence 58 | else 59 | hint_pair + reset_sequence + highlight_pair + reset_sequence 60 | end 61 | end 62 | 63 | private def chop_highlight(hint, highlight) 64 | if hint_position == "right" 65 | highlight[0..-(hint.size + 1)] || "" 66 | else 67 | highlight[hint.size..-1] || "" 68 | end 69 | rescue 70 | puts "failed for hint '#{hint}' and '#{highlight}'" 71 | "" 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /src/fingers/state.cr: -------------------------------------------------------------------------------- 1 | module Fingers 2 | class State 3 | def initialize 4 | @show_help = false 5 | @multi_mode = false 6 | @input = "" 7 | @modifier = "" 8 | @selected_hints = [] of String 9 | @selected_matches = [] of String 10 | @multi_matches = [] of String 11 | @result = "" 12 | @exiting = false 13 | end 14 | 15 | property :show_help, 16 | :multi_mode, 17 | :input, 18 | :modifier, 19 | :selected_hints, 20 | :selected_matches, 21 | :multi_matches, 22 | :result, 23 | :exiting 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/fingers/types.cr: -------------------------------------------------------------------------------- 1 | module Fingers 2 | abstract class Printer 3 | abstract def print(msg : String) 4 | abstract def flush 5 | end 6 | 7 | abstract class Formatter 8 | abstract def format(hint : String, highlight : String, selected : Bool, offset : Tuple(Int32, Int32) | Nil) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/fingers/view.cr: -------------------------------------------------------------------------------- 1 | require "../tmux" 2 | require "./hinter" 3 | require "./state" 4 | require "./action_runner" 5 | 6 | module Fingers 7 | class View 8 | CLEAR_SEQ = "\e[H\e[J" 9 | HIDE_CURSOR_SEQ = "\e[?25l" 10 | 11 | @hinter : Hinter 12 | @state : State 13 | @output : Printer 14 | @original_pane : Tmux::Pane 15 | @tmux : Tmux 16 | @mode : String 17 | 18 | def initialize( 19 | @hinter, 20 | @output, 21 | @original_pane, 22 | @state, 23 | @tmux, 24 | @mode, 25 | ) 26 | end 27 | 28 | def render 29 | clear_screen 30 | hide_cursor 31 | 32 | begin 33 | hinter.run 34 | rescue e 35 | Log.fatal { e } 36 | request_exit! 37 | end 38 | end 39 | 40 | def process_input(input : String) 41 | command, *args = input.split(":") 42 | 43 | case command 44 | when "hint" 45 | char, modifier = args 46 | process_hint(char, modifier) 47 | when "exit" 48 | request_exit! 49 | when "toggle-help" 50 | when "toggle-multi-mode" 51 | process_multimode 52 | when "fzf" 53 | # soon 54 | end 55 | end 56 | 57 | private def hide_cursor 58 | output.print HIDE_CURSOR_SEQ 59 | end 60 | 61 | private def clear_screen 62 | output.print CLEAR_SEQ 63 | end 64 | 65 | private def process_hint(char, modifier) 66 | state.input += char 67 | state.modifier = modifier 68 | 69 | match = hinter.lookup(state.input) 70 | 71 | if match.nil? 72 | render 73 | else 74 | handle_match(match.not_nil!.text) 75 | end 76 | end 77 | 78 | private def process_multimode 79 | return if mode == "jump" 80 | 81 | prev_state = state.multi_mode 82 | state.multi_mode = !state.multi_mode 83 | current_state = state.multi_mode 84 | 85 | if prev_state == true && current_state == false 86 | state.result = state.multi_matches.join(' ') 87 | request_exit! 88 | end 89 | end 90 | 91 | private getter :output, :hinter, :original_pane, :state, :tmux, :mode 92 | 93 | private def handle_match(match) 94 | if state.multi_mode 95 | state.multi_matches << match 96 | state.selected_hints << state.input 97 | state.input = "" 98 | render 99 | else 100 | state.result = match 101 | request_exit! 102 | end 103 | end 104 | 105 | private def request_exit! 106 | state.exiting = true 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /src/huffman.cr: -------------------------------------------------------------------------------- 1 | require "./priority_queue" 2 | 3 | struct HuffmanNode 4 | def initialize(weight : Int32, children : Array(HuffmanNode)) 5 | @weight = weight 6 | @children = children 7 | end 8 | 9 | getter :children, :weight 10 | end 11 | 12 | class Huffman 13 | @alphabet : Array(String) 14 | @n : Int32 15 | 16 | getter :alphabet, :n, :queue 17 | 18 | def initialize 19 | @n = 0 20 | @queue = PriorityQueue(HuffmanNode).new 21 | @alphabet = [] of String 22 | end 23 | 24 | def generate_hints(alphabet : Array(String), n : Int32) 25 | cached_result = read_from_cache(alphabet, n) 26 | return cached_result unless cached_result.nil? 27 | 28 | if n <= alphabet.size 29 | return alphabet 30 | end 31 | 32 | setup!(alphabet: alphabet, n: n) 33 | 34 | first_node = true 35 | 36 | while queue.size > 1 37 | if first_node 38 | n_branches = initial_number_of_branches 39 | first_node = false 40 | else 41 | n_branches = arity 42 | end 43 | 44 | smallest = get_smallest(n_branches) 45 | new_node = new_node_from(smallest) 46 | 47 | queue.push(new_node.weight, new_node) 48 | end 49 | 50 | result = [] of String 51 | 52 | root = queue.pop 53 | 54 | traverse_tree(root) do |node, path| 55 | result.push(translate_path(path)) if node.children.empty? 56 | end 57 | 58 | final_result = result.sort_by(&.size) 59 | 60 | save_to_cache(alphabet, n, final_result) 61 | 62 | final_result 63 | end 64 | 65 | private def setup!(alphabet, n) 66 | @alphabet = alphabet 67 | @n = n 68 | @queue = build_heap 69 | end 70 | 71 | private def initial_number_of_branches 72 | result = 1 73 | 74 | (1..(n.to_i // arity.to_i + 1)).to_a.each do |t| 75 | result = n - t * (arity - 1) 76 | 77 | break if result >= 2 && result <= arity 78 | 79 | result = arity 80 | end 81 | 82 | result 83 | end 84 | 85 | private def read_from_cache(alphabet, n) : Array(String) | Nil 86 | File.read(cache_key(alphabet, n)).chomp.split(":") 87 | rescue File::NotFoundError 88 | nil 89 | end 90 | 91 | private def save_to_cache(alphabet, n, result) 92 | File.write(cache_key(alphabet, n), result.join(":")) 93 | end 94 | 95 | private def cache_key(alphabet, n) 96 | Fingers::Dirs::CACHE / "#{alphabet.join("")}-#{n}" 97 | end 98 | 99 | private def arity 100 | alphabet.size 101 | end 102 | 103 | private def build_heap 104 | queue = PriorityQueue(HuffmanNode).new 105 | 106 | n.times { |i| queue.push(-i.to_i, HuffmanNode.new(weight: -i, children: [] of HuffmanNode)) } 107 | 108 | queue 109 | end 110 | 111 | private def get_smallest(n : Int32) : Array(HuffmanNode) 112 | result = [] of HuffmanNode 113 | [n, queue.size].min.times.each { result.push(queue.pop) } 114 | result 115 | end 116 | 117 | private def new_node_from(nodes) 118 | weight = nodes.sum do |node| 119 | node.weight 120 | end 121 | 122 | HuffmanNode.new(weight: weight, children: nodes) 123 | end 124 | 125 | private def traverse_tree(node, path = [] of Int32, &block : (HuffmanNode, Array(Int32)) -> Nil) 126 | yield node, path 127 | 128 | node.children.each_with_index do |child, index| 129 | traverse_tree(child, [*path, index], &block) 130 | end 131 | end 132 | 133 | def translate_path(path) 134 | path.map { |i| alphabet[i] }.join("") 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /src/priority_queue.cr: -------------------------------------------------------------------------------- 1 | class PriorityQueue(T) 2 | @q : Hash(Int32, Array(T)) 3 | 4 | def initialize(data = nil) 5 | @q = Hash(Int32, Array(T)).new do |h, k| 6 | h[k] = [] of T 7 | end 8 | data.each { |priority, item| @q[priority] << item } if data 9 | @priorities = @q.keys.sort! 10 | end 11 | 12 | def push(priority : Int32, item : T) 13 | @q[priority].push(item) 14 | @priorities = @q.keys.sort! 15 | end 16 | 17 | def pop 18 | p = @priorities.last 19 | item = @q[p].shift 20 | if @q[p].empty? 21 | @q.delete(p) 22 | @priorities.pop 23 | end 24 | item 25 | end 26 | 27 | def peek 28 | unless empty? 29 | @q[@priorities[0]][0] 30 | end 31 | end 32 | 33 | def empty? 34 | @priorities.empty? 35 | end 36 | 37 | def each 38 | @q.each do |priority, items| 39 | items.each { |item| yield priority, item } 40 | end 41 | end 42 | 43 | def dup 44 | @q.each_with_object(self.class.new) do |(priority, items), obj| 45 | items.each { |item| obj.push(priority, item) } 46 | end 47 | end 48 | 49 | def merge(other) 50 | raise TypeError unless self.class == other.class 51 | pq = dup 52 | other.each { |priority, item| pq.push(priority, item) } 53 | pq # return a new object 54 | end 55 | 56 | def inspect 57 | @q.inspect 58 | end 59 | 60 | def size 61 | @q.values.sum(&.size) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /src/tmux.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "semantic_version" 3 | require "./tmux_style_printer" 4 | 5 | def to_tmux_string(value) 6 | # TODO tmux syntax to escape quotes 7 | "\"\#{#{value}}\"" 8 | end 9 | 10 | def to_tmux_number(value) 11 | "\#{#{value}}" 12 | end 13 | 14 | def to_tmux_nullable_number(value) 15 | "\#{?#{value},\#{#{value}},null}" 16 | end 17 | 18 | def to_tmux_bool(value) 19 | "\#{?#{value},true,false}" 20 | end 21 | 22 | def build_tmux_format(hash) 23 | fields = hash.map do |field, type| 24 | if type == String 25 | "\"#{field}\": #{to_tmux_string(field)}" 26 | elsif type == Int32 27 | "\"#{field}\": #{to_tmux_number(field)}" 28 | elsif type == Int32 | Nil 29 | "\"#{field}\": #{to_tmux_nullable_number(field)}" 30 | elsif type == Bool 31 | "\"#{field}\": #{to_tmux_bool(field)}" 32 | end 33 | end 34 | 35 | "{#{fields.join(",")}}" 36 | end 37 | 38 | # TODO maybe use system everywhere? 39 | 40 | # rubocop:disable Metrics/ClassLength 41 | class Tmux 42 | class Shell 43 | def initialize 44 | @sh = Process.new("/bin/sh", input: :pipe, output: :pipe, error: :close) 45 | end 46 | 47 | def exec(cmd) 48 | ch = Channel(String).new 49 | 50 | spawn do 51 | output = "" 52 | while line = @sh.output.read_line 53 | break if line == "cmd-end" 54 | 55 | output += "#{line}\n" 56 | end 57 | 58 | ch.send(output) 59 | end 60 | 61 | @sh.input.print("#{cmd}; echo cmd-end\n") 62 | @sh.input.flush 63 | output = ch.receive 64 | output 65 | end 66 | end 67 | 68 | struct Pane 69 | include JSON::Serializable 70 | 71 | property pane_id : String 72 | property window_id : String 73 | property pane_width : Int32 74 | property pane_height : Int32 75 | property pane_current_path : String 76 | property pane_in_mode : Bool 77 | property scroll_position : Int32 | Nil 78 | property window_zoomed_flag : Bool 79 | end 80 | 81 | struct Window 82 | include JSON::Serializable 83 | 84 | property window_id : String 85 | property window_width : Int32 86 | property window_height : Int32 87 | property pane_id : String 88 | property pane_tty : String 89 | end 90 | 91 | # TODO make a macro or something 92 | PANE_FORMAT = build_tmux_format({ 93 | pane_id: String, 94 | window_id: String, 95 | pane_width: Int32, 96 | pane_height: Int32, 97 | pane_current_path: String, 98 | pane_in_mode: Bool, 99 | scroll_position: Int32 | Nil, 100 | window_zoomed_flag: Bool, 101 | }) 102 | 103 | WINDOW_FORMAT = build_tmux_format({ 104 | window_id: String, 105 | window_width: Int32, 106 | window_height: Int32, 107 | pane_id: String, 108 | pane_tty: String, 109 | }) 110 | 111 | @panes : Array(Pane) | Nil 112 | @version : SemanticVersion 113 | 114 | def self.tmux_version_to_semver(version_string) 115 | match = version_string.match(/(?[1-9]+[0-9]*)\.(?[0-9]+)(?[a-z]+)?/) 116 | 117 | raise "Invalid tmux version #{version_string}" unless match 118 | 119 | major = match["major"].not_nil! 120 | minor = match["minor"].not_nil! 121 | patch_letter = match["patch_letter"]? 122 | 123 | if patch_letter.nil? 124 | patch = 0 125 | else 126 | patch = patch_letter[0].ord - 'a'.ord + 1 127 | end 128 | 129 | SemanticVersion.parse("#{major}.#{minor}.#{patch}") 130 | end 131 | 132 | def initialize(version_string) 133 | @sh = Shell.new 134 | @version = Tmux.tmux_version_to_semver(version_string) 135 | end 136 | 137 | def list_panes(filters = "", target = nil) : Array(Pane) 138 | args = ["list-panes", "-F", PANE_FORMAT] 139 | 140 | if target.nil? 141 | args << "-a" 142 | else 143 | args.concat(["-t", target]) 144 | end 145 | 146 | if !filters.empty? 147 | args.concat(["-f", filters]) 148 | end 149 | 150 | exec(Process.quote(args)).chomp.split("\n").map do |pane| 151 | Pane.from_json(pane) 152 | end 153 | end 154 | 155 | def find_pane_by_id(id) : Pane | Nil 156 | output = exec("display-message -t '#{id}' -F '#{PANE_FORMAT}' -p").chomp 157 | 158 | return nil if output.empty? 159 | 160 | Pane.from_json(output) 161 | end 162 | 163 | def capture_pane(pane : Pane, join = true) 164 | if pane.pane_in_mode && !pane.scroll_position.nil? 165 | scroll_position = pane.scroll_position.not_nil! 166 | start_line = -scroll_position.to_i 167 | end_line = pane.pane_height.to_i - scroll_position.to_i - 1 168 | 169 | exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}' -S #{start_line} -E #{end_line}").chomp 170 | else 171 | exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}'").chomp 172 | end 173 | end 174 | 175 | def create_window(name, cmd, _pane_width, _pane_height) 176 | output = exec("new-window -c '\#{pane_current_path}' -P -d -n '#{name}' -F '#{WINDOW_FORMAT}' '#{cmd}'").chomp 177 | 178 | Window.from_json(output) 179 | end 180 | 181 | def swap_panes(src_id, dst_id) 182 | args = ["swap-pane", "-d", "-s", src_id, "-t", dst_id] 183 | 184 | if @version >= Tmux.tmux_version_to_semver("3.1") 185 | args << "-Z" 186 | end 187 | 188 | exec(args.join(" ")) 189 | end 190 | 191 | def kill_pane(id) 192 | exec("kill-pane -t #{id}") 193 | end 194 | 195 | def kill_window(id) 196 | exec("kill-window -t #{id}") 197 | end 198 | 199 | # TODO: this command is version dependant D: 200 | def resize_window(window_id, width, height) 201 | exec(["resize-window", "-t", window_id, "-x", width.to_s, "-y", height.to_s].join(' ')) 202 | end 203 | 204 | # TODO: this command is version dependant D: 205 | def resize_pane(pane_id, width, height) 206 | exec(["resize-pane", "-t", pane_id, "-x", width.to_s, "-y", height.to_s].join(' ')) 207 | end 208 | 209 | def set_window_option(name, value) 210 | exec(["set-window-option", name, value].join(' ')) 211 | end 212 | 213 | def set_key_table(table) 214 | exec(["set-window-option", "key-table", table].join(' ')) 215 | exec(["switch-client", "-T", table].join(' ')) 216 | end 217 | 218 | def disable_prefix 219 | set_global_option("prefix", "None") 220 | set_global_option("prefix2", "None") 221 | end 222 | 223 | def set_global_option(name, value) 224 | exec(Process.quote(["set-option", "-g", name, value])) 225 | end 226 | 227 | def get_global_option(name) 228 | exec(["show", "-gqv", name].join(' ')).chomp 229 | end 230 | 231 | def set_buffer(value) 232 | return unless value 233 | 234 | if @version >= Tmux.tmux_version_to_semver("3.2") 235 | args = ["load-buffer", "-w", "-"] 236 | else 237 | args = ["load-buffer", "-"] 238 | end 239 | 240 | # To avoid shell escaping nightmares, we'll use Process and write directly to stdin 241 | cmd = Process.new( 242 | tmux, 243 | args, 244 | input: :pipe, 245 | output: :pipe, 246 | error: :pipe, 247 | ) 248 | 249 | cmd.input.print(value) 250 | cmd.input.flush 251 | 252 | cmd.wait 253 | 254 | nil 255 | end 256 | 257 | def select_pane(id) 258 | args = ["select-pane", "-t", id] 259 | 260 | if @version >= Tmux.tmux_version_to_semver("3.1") 261 | args << "-Z" 262 | end 263 | 264 | exec(args.join(' ')) 265 | end 266 | 267 | def zoom_pane(id) 268 | exec(["resize-pane", "-Z", "-t", id].join(' ')) 269 | end 270 | 271 | # TODO 272 | def parse_style(style) 273 | style_printer.print(style).chomp 274 | end 275 | 276 | def style_printer 277 | @style_printer ||= TmuxStylePrinter.new 278 | end 279 | 280 | def tmux 281 | "tmux" 282 | end 283 | 284 | def build_tmux_output_format(fields) 285 | fields.map { |field| format("\#{%s}", field: field) }.join(";") 286 | end 287 | 288 | def parse_tmux_formatted_output(output) 289 | output.split("\n").map do |line| 290 | fields = line.split(";") 291 | yield fields 292 | end 293 | end 294 | 295 | def socket_flag_value 296 | return ENV["FINGERS_TMUX_SOCKET"] if ENV["FINGERS_TMUX_SOCKET"] 297 | socket 298 | end 299 | 300 | def display_message(msg, delay = 100) 301 | exec(Process.quote(["display-message", "-d", delay.to_s, msg])) 302 | end 303 | 304 | def exec(cmd) 305 | @sh.exec("#{tmux} #{cmd}") 306 | end 307 | end 308 | -------------------------------------------------------------------------------- /src/tmux_style_printer.cr: -------------------------------------------------------------------------------- 1 | class TmuxStylePrinter 2 | 3 | class InvalidFormat < Exception 4 | end 5 | 6 | abstract class Shell 7 | abstract def exec(cmd) 8 | end 9 | 10 | STYLE_SEPARATOR = /[ ,]+/ 11 | 12 | COLOR_MAP = { 13 | black: 0, 14 | red: 1, 15 | green: 2, 16 | yellow: 3, 17 | blue: 4, 18 | magenta: 5, 19 | cyan: 6, 20 | white: 7, 21 | } 22 | 23 | LAYER_MAP = { 24 | bg: "setab", 25 | fg: "setaf", 26 | } 27 | 28 | STYLE_MAP = { 29 | bright: "bold", 30 | bold: "bold", 31 | dim: "dim", 32 | underscore: "smul", 33 | reverse: "rev", 34 | italics: "sitm", 35 | } 36 | 37 | class ShellExec < Shell 38 | def exec(cmd) 39 | `#{cmd}`.chomp 40 | end 41 | end 42 | 43 | @shell : Shell 44 | @applied_styles : Hash(String, String) 45 | @reset_sequence : String | Nil 46 | 47 | def initialize(shell = ShellExec.new) 48 | @shell = shell 49 | @applied_styles = {} of String => String 50 | end 51 | 52 | def print(input, reset_styles_after = false) 53 | @applied_styles = {} of String => String 54 | 55 | output = "" 56 | 57 | input.split(STYLE_SEPARATOR).each do |style| 58 | output += parse_style_definition(style) 59 | end 60 | 61 | output += reset_sequence if reset_styles_after && !@applied_styles.empty? 62 | 63 | output 64 | end 65 | 66 | private def parse_style_definition(style) 67 | if style.match(/^(bg|fg)=/) 68 | parse_color(style) 69 | else 70 | parse_style(style) 71 | end 72 | end 73 | 74 | private def parse_color(style) 75 | match = style.match(/(?bg|fg)=(?(colou?r(?[0-9]+)|.*))/) 76 | 77 | raise InvalidFormat.new("Invalid color definition: #{style}") unless match 78 | 79 | layer = match["layer"] 80 | color = match["color"] 81 | color_code = match["color_code"] if match["color_code"]? 82 | 83 | if match["color"] == "default" 84 | @applied_styles.delete(layer) 85 | return reset_to_applied_styles! 86 | end 87 | 88 | color_to_apply = color_code || COLOR_MAP[color]? 89 | 90 | raise InvalidFormat.new("Invalid color definition: #{style}") if color_to_apply.nil? 91 | 92 | result = shell.exec("tput #{LAYER_MAP[layer]} #{color_to_apply}") 93 | 94 | @applied_styles[layer] = result 95 | 96 | result 97 | end 98 | 99 | private def parse_style(style) 100 | match = style.match(/(?no)?(?