├── .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 | 
2 |
3 | 
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 |
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)?(?