Compare commits

..

183 commits

Author SHA1 Message Date
Jorge Morante fc3c750b8d Merge branch 'develop'
Some checks failed
Pull Request / spec (push) Has been cancelled
Pull Request / benchmark (push) Has been cancelled
2026-06-09 11:00:08 +01:00
Jorge Morante 95df1958aa updated CHANGELOG.md 2026-06-09 10:59:23 +01:00
Jorge Morante abf70872ef bump version in shard.yml 2026-06-09 10:58:30 +01:00
Matthew Wilson 7fd9896491
update install-wizard and readme to show that shards is required (#166)
* update install-wizard and readme to show that shards is required

Signed-off-by: Matthew Wilson <54033231+wilson-matthew@users.noreply.github.com>

* updated CHANGELOG.md

---------

Signed-off-by: Matthew Wilson <54033231+wilson-matthew@users.noreply.github.com>
Co-authored-by: Jorge Morante <jorge@morante.eu>
2026-06-09 10:45:07 +01:00
inzeets 1bcbcaa2af
fix custom patterns being unusable via --patterns (#175)
* updated CHANGELOG.md

* fix custom patterns being unusable via --patterns

---------

Co-authored-by: Jorge Morante <jorge@morante.eu>
2026-06-09 10:43:06 +01:00
Noam Stolero fbc9ed8dd6
Allow comma-separated values in @fingers-enabled-builtin-patterns (#177)
* updated CHANGELOG.md

* allow comma-separated values in @fingers-enabled-builtin-patterns

The option is documented as a comma-separated list and add_builtin_patterns
splits it on ",", but it was declared with define_enum_option, whose parser
validates the whole string as a single member. Any list of more than one
pattern failed load-config, so the subset-selection use case from #19 was
impossible.

Add a MultiEnumParser that splits on "," and validates each token against
the allowed values, reporting any invalid names, and a define_multi_enum_option
macro that uses it. enabled_builtin_patterns now uses that macro.

---------

Co-authored-by: Jorge Morante <jorge@morante.eu>
2026-06-09 10:37:21 +01:00
Jorge Morante 5409aee975 updated CHANGELOG.md 2026-05-31 10:40:27 +01:00
Jorge Morante 02a35b246c updated CHANGELOG.md 2026-05-31 10:34:16 +01:00
Jorge Morante ae26058e6c bump version in shard.yml 2026-05-31 10:30:07 +01:00
Jorge Morante c77287b56d fix tmux installations cache 2026-05-05 14:49:45 +01:00
Jorge Morante 79d0355585 fix benchmark job in develop 2026-05-05 14:26:10 +01:00
Jorge Morante f99ceba76f bump to crystal 1.19 2026-04-17 18:36:25 +01:00
Jorge Morante 336a5c6e90 fix dev/benchmark.cr 2026-04-17 18:27:45 +01:00
Jorge Morante 7308c31c05 add error handling and reporting when running actions 2026-04-17 17:56:06 +01:00
Jorge Morante d52c6c07a4 restore new kubernetes-pod builtin pattern 2026-04-15 10:38:59 +01:00
Jorge Morante 38fe26bdff fix clipboard integration in WSL 2026-04-15 10:25:39 +01:00
Jorge Morante 8433feea47 fix highlight when submatch is at the beginning of match 2026-04-13 09:31:22 +01:00
Jorge Morante 5cf72accd9 dont use cached data when running specs 2026-04-12 11:57:24 +01:00
Jorge Morante 8b56badada fix match_formatter specs 2026-04-12 11:54:39 +01:00
Nazım Can Altınova 3190d698d5
Add macOS arm64 binary to the release/download pipeline (#168)
* Add a macos arm64 binary generation and upload step to the deployment workflow

* Add arm64 macos binary support to the install wizard

* Update README.md to mention macOS arm64 binary
2026-04-11 13:30:56 +01:00
Jorge Morante e4280e1bec fix backdrop style leaking into hint/highlight when capture groups are involved 2026-04-11 12:29:46 +01:00
Jorge Morante a168542a83 fix style parsing/processing 2026-04-11 12:21:36 +01:00
Morantron b43b51ffad
Refactor config options with macros (#170)
* refactor option parsing and validation with crystal macros

* store tmux version in config struct

* fix formatting
2026-04-11 11:04:44 +01:00
Jul 9a0cff4b50
add kubernetes-pod builtin pattern for pod names (#164)
The existing "kubernetes" pattern matches resource type paths
(e.g. deployment.apps/my-app) but not pod names, which are
the most commonly copied k8s identifiers for kubectl logs,
describe, exec, etc.

Pod names like "nginx-deployment-66b6c48dd5-7xb2r" have a
distinctive structure: <name>-<replicaset-hash>-<pod-hash>.
The regex uses the actual k8s hash alphabet (consonants +
digits 2,4-9, no vowels) from apimachinery/pkg/util/rand,
which avoids false positives on normal terminal text.
2026-03-16 11:04:10 +00:00
Morantron dabef3697b
remove tput calls (#161) 2026-03-16 10:56:34 +00:00
Morantron 5927132aee
setting up github actions (#158) 2026-02-22 18:09:42 +00:00
Jorge Morante 29f90165fa updated CHANGELOG.md 2026-02-15 22:43:40 +00:00
Jorge Morante dce0319efd bump version in shard.yml 2026-02-15 22:42:14 +00:00
Jorge Morante 64c35e7893 dont check for tab chars in needs_resize? method 2026-02-15 22:16:59 +00:00
Jorge Morante 99dafe680d fix line jumping with tabs/emojis in zoomed panes 2026-02-15 22:12:31 +00:00
Jorge Morante ccdd8df283 add tab expansion/correction in hinter 2026-02-15 22:08:56 +00:00
Jorge Morante 52fe76cfd1 fix need_resize? method 2026-02-15 20:09:07 +00:00
Jorge Morante f97187cd93 Revert "handle tab characters properly by prefilling line"
This reverts commit 8451588fd4.
2026-02-13 16:42:15 +00:00
Jorge Morante be6ef53751 updated CHANGELOG.md 2026-01-27 10:00:45 +01:00
Jorge Morante 2239925824 bump version in shard.yml 2026-01-27 09:57:01 +01:00
Bob Cotton 56f493f644
Fix: close stdin before waiting for load-buffer command (#157)
The set_buffer method writes to tmux load-buffer via stdin pipe but
only flushes without closing. Since load-buffer reads until EOF,
the command may not complete properly, especially when using the
-w flag for OSC52 clipboard integration through nested tmux/SSH.

Replacing flush with close signals EOF and allows the command to
complete, ensuring the OSC52 escape sequence is properly sent.
2026-01-11 18:30:09 +00:00
Jorge Morante 3dae735cb3 add SHELL variable to info command, and properly handle unset env vars 2026-01-11 19:27:19 +01:00
Jorge Morante 8451588fd4 handle tab characters properly by prefilling line 2026-01-11 19:07:39 +01:00
Jorge Morante eeb9fc3310 add utf locale to Dockerfile 2026-01-11 19:06:50 +01:00
Jorge Morante 0c118266b8 update issue templte 2026-01-11 18:56:31 +01:00
Jorge Morante 2795f8aaa7 add running_in_specs? flag 2026-01-11 18:54:44 +01:00
Jorge Morante 7fa3cc3a6f run tests within container with "make test" 2026-01-11 18:53:52 +01:00
Jorge Morante 56d447282d updated CHANGELOG.md 2026-01-05 23:20:56 +01:00
Jorge Morante ec73be73c4 bump version in shard.yml 2026-01-05 23:18:51 +01:00
Jorge Morante ff9954563b read default-terminal option instead of forcing tmux-256color 2026-01-05 23:17:51 +01:00
Jorge Morante edd2972a82 default to tmux 3.6a in development 2026-01-05 23:17:27 +01:00
Jorge Morante c07e1baf28 handle case where hint is longer than text to highlight 2026-01-05 23:08:15 +01:00
Jorge Morante 6677dd1701 add new @fingers-enable-bindings option 2025-12-27 18:18:10 +01:00
Jorge Morante d05ec8817f add TERM value to fingers info command 2025-12-27 18:03:59 +01:00
Jorge Morante e3561d5dfa add tmux 3.6a version 2025-12-27 18:03:27 +01:00
Jorge Morante a91fd35bf3 fix colors when for tmux 3.6a and systemd 2025-12-27 18:02:25 +01:00
phucisstupid 9eec2a9d23
fix(README.md): better tmux config syntax (#151) 2025-08-29 12:34:21 +02:00
Jorge Morante 1456f54f21 cleanup outdated comments in src/tmux.cr 2025-08-10 12:19:04 +01:00
Jorge Morante 39c561bac3 remove unused files 2025-08-10 12:18:44 +01:00
Jorge Morante 809422b139 updated CHANGELOG.md 2025-08-04 12:06:47 +02:00
Jorge Morante 8c45c60456 bump version in shard.yml 2025-08-04 12:05:44 +02:00
Jorge Morante 7b46f6cd1c update recipes in TOC 2025-08-04 11:57:05 +02:00
Nao 43f792b8a2
Use pane_current_path with presence when chdir (#150)
If pane_current_path is an empty string, chdir will crash.

The tmux(1) manual describes pane_current_path as "Current path if
available", so pane_current_path should be used with presence, which
returns nil if the string is empty.

Link: 6f9bcb7fee/tmux.1 (L6107)

Signed-off-by: UEDA Naoaki <nao.uedder@gmail.com>
2025-08-04 10:54:11 +01:00
Jorge Morante ea0447f05a fix typo 2025-07-18 09:50:50 +02:00
Jorge Morante ae2e1d5cd3 updated CHANGELOG.md 2025-07-17 09:59:45 +02:00
Jorge Morante 5f0423088c bump version in shard.yml 2025-07-17 09:58:06 +02:00
Jorge Morante d8bb10e2a2 tweaks and documentation to new clipboard option 2025-07-17 09:57:17 +02:00
Alexandre GV. bbf320b0c7
Fix typo in README (#144) 2025-07-17 09:38:34 +02:00
purajit 8431420e34
add config to allow preventing :paste: from affecting system clipboard (#145) 2025-07-17 09:34:58 +02:00
Jorge Morante ea979d5e4a updated CHANGELOG.md 2025-05-18 15:44:14 +02:00
Jorge Morante e5f6fb0690 bump version in shard.yml 2025-05-18 15:43:48 +02:00
Jorge Morante 963ca74c9e fix active pane retrieval when passing a format as target 2025-05-18 15:11:28 +02:00
Jorge Morante e5d8f3ce42 add tmux 3.5a version 2025-05-18 11:17:16 +02:00
Jorge Morante e4b4741f5a updated CHANGELOG.md 2025-04-04 19:05:05 +02:00
Jorge Morante 62ac2ec93a bump version in shard.yml 2025-04-04 19:03:18 +02:00
Jorge Morante d67503fde5 update README.md 2025-04-04 19:01:53 +02:00
Jorge Morante 1dcfc4815f properly restore active pane, regardless of pane format 2025-02-18 17:45:24 +01:00
Jorge Morante f5faacd9da properly handling active pane restore when using pane formats 2025-02-17 10:18:30 +01:00
Jorge Morante fa3947d487 updated CHANGELOG.md 2025-02-06 18:11:02 +01:00
Jorge Morante 1e4708a903 bump version in shard.yml 2025-02-06 18:10:12 +01:00
Jorge Morante 1439a8542b fix :paste: action when not in copy mode hehehehe 2025-02-06 18:10:00 +01:00
Jorge Morante e7a589600c update CHANGELOG.md 2025-02-02 14:33:08 +01:00
Jorge Morante abdaa1eee0 updated CHANGELOG.md 2025-02-02 14:28:05 +01:00
Jorge Morante 2697db501b bump version in shard.yml 2025-02-02 14:24:28 +01:00
Jorge Morante c73bed42ee clarify that crystal compiler is needed to build from source 2025-02-02 14:24:04 +01:00
Jorge Morante 6f99edc1a9 remove old docs/health-check.md file 2025-02-02 14:14:42 +01:00
Jorge Morante 41dbe3dba5 handle blank url in install-wizard.sh 2025-02-02 14:12:46 +01:00
Jorge Morante bbbca3cd4d send cancel first and then paste in :paste: action 2025-02-02 14:11:05 +01:00
twio142 a668575a13
Quit copy mode after pasting in :paste:; exclude single quotes from URL pattern (#136)
- improve ':paste:' by quitting copy mode after pasting
- exclude single quotes from url
2025-02-02 14:09:23 +01:00
Morantron d04a1185e8
Merge pull request #134 from step-/patch-1
fix: typos in README.md
2025-01-03 16:33:48 +01:00
step 33df35c406
fix: typos in README.md 2024-12-29 23:47:31 +01:00
Jorge Morante e2283d61e4 updated CHANGELOG.md 2024-12-10 11:15:00 +01:00
Jorge Morante 10deafaa5e bump version in shard.yml 2024-12-10 11:14:43 +01:00
Jorge Morante 645f92e26e fix ActionRunner#paste 2024-12-10 11:14:19 +01:00
Jorge Morante 75f0c7c53f updated CHANGELOG.md 2024-12-09 21:47:14 +01:00
Jorge Morante 316a1dfa9f bump version in shard.yml 2024-12-09 21:40:51 +01:00
Jorge Morante e52717b1af add documentation for @fingers-cli and new action flags 2024-12-09 21:30:58 +01:00
Jorge Morante 6ccfeb7b87 handle edge case in ActionRunner 2024-12-09 21:14:21 +01:00
Jorge Morante c174b1919d change log location in development 2024-12-09 21:13:47 +01:00
Jorge Morante 8698822653 fix teardown 2024-12-09 09:44:19 +01:00
Jorge Morante 09e7c1791e add help flag to start command 2024-11-28 11:52:56 +01:00
Jorge Morante c694c1a8dd add ISSUE_TEMPLATE 2024-11-28 11:31:24 +01:00
Jorge Morante 33e2f0e0e2 add info command 2024-11-28 11:20:45 +01:00
Jorge Morante ab8c4be8c0 use crystal 1.14 2024-11-10 15:21:24 +01:00
Jorge Morante 5ec5977201 remove TODO 2024-11-05 18:09:31 +01:00
Jorge Morante d42466771f remove tput call from match formatter 2024-11-05 17:49:26 +01:00
Jorge Morante dacb192171 hide send-input command 2024-11-05 17:44:45 +01:00
Jorge Morante 4af0d7a48c add make dev 2024-11-05 17:44:29 +01:00
Jorge Morante 9d80b2dd26 add --(main|ctrl|alt|shift)-action cli options 2024-11-05 17:12:51 +01:00
Jorge Morante b4bb3c13e4 optimize option tracking 2024-10-30 10:56:54 +01:00
Morantron c257f9388e
Merge pull request #130 from ndsizeif/keytable
Add methods to track and restore key-table
2024-10-30 10:22:18 +01:00
Jorge Morante 0f2d8b9dcf move benchmark script to crystal 2024-10-30 10:21:38 +01:00
Jorge Morante 834f392d3d improve git-status builtin pattern 2024-10-30 08:09:54 +01:00
ndsizeif 7f6a019042
Update start.cr 2024-10-21 13:48:42 +00:00
Morantron 9d609897a7
Merge pull request #131 from alanoliveira/master
implement shell-command option for cli
2024-10-21 11:06:05 +02:00
Alan Oliveira 7ba9307102 implement shell-command option for cli 2024-10-19 18:16:53 +09:00
Alan Oliveira d7fcf90f8d fix 'specific patterns' session on README
`-n` creates a bind without prefix
2024-10-19 18:16:53 +09:00
ndsizeif 9d29d3467d Add methods to track and restore key-table
On teardown, set key-table to the tracked value if not empty, instead of "root"
2024-10-10 01:21:19 -05:00
Jorge Morante 296cc8e49e parse tmux pane target formats 2024-09-15 17:50:00 +02:00
Jorge Morante b90d7c4b3c updated CHANGELOG.md 2024-08-14 10:44:47 +02:00
Jorge Morante 5cd719e5ce bump version in shard.yml 2024-08-14 10:43:00 +02:00
Jorge Morante 9b5411120f use xdg dirs 2024-08-14 10:29:45 +02:00
Morantron 7a1d183b81
Merge pull request #127 from rudenkornk/master
fix(Dirs): fix multi-user race #126
2024-08-14 10:28:41 +02:00
Jorge Morante 36b0be2002 benchmark fixes 2024-08-14 09:53:21 +02:00
Nikita Rudenko 59b7743293 fix(Dirs): fix multi-user race 2024-08-07 19:06:05 +03:00
Jorge Morante 3e1d0a8d85 updated CHANGELOG.md 2024-07-29 08:02:48 +02:00
Jorge Morante 83bf475b97 bump version in shard.yml 2024-07-29 08:01:06 +02:00
Jorge Morante 5b1b380c99 read CURRENT_GIT_VERSION from shard.yml 2024-07-26 11:52:48 +02:00
Morantron f72330eff9
Merge pull request #124 from priceld/patch-1
doc: update list of builtin patterns
2024-07-25 07:55:29 +02:00
Logan Price 5d52ace04d
doc: update list of builtin patterns
Updates the list of default patterns to include `path` and `hex`
2024-07-24 10:41:08 -04:00
Jorge Morante 1cc13c9ae3 updated CHANGELOG.md 2024-07-21 19:21:46 +02:00
Jorge Morante 1cd1119970 bump version in shard.yml 2024-07-21 19:21:13 +02:00
Jorge Morante f5c8a14560 update README.md with new features 2024-07-21 19:14:32 +02:00
Jorge Morante c8b911e2c7 open fingers window in same path as target pane 2024-07-21 18:42:12 +02:00
Jorge Morante 060b342ee3 tweak Dockerfile and add tmux 3.4 for testing 2024-07-21 18:37:25 +02:00
Jorge Morante ad0ae1e90f pass -Z flag to select-pane to properly handle zoomed panes 2024-07-21 18:36:52 +02:00
Jorge Morante 99c20be5b2 adding support to enable/disable built-in patterns or using specific patterns 2024-07-21 18:33:25 +02:00
Jorge Morante 97d1ddd467 use cling shard to handle arg parsing 2024-07-19 10:24:00 +02:00
Jorge Morante 9b393e5757 updated CHANGELOG.md 2024-05-02 16:52:38 +02:00
Jorge Morante 27c5512bc2 bump version in shard.yml 2024-05-02 16:52:14 +02:00
Jorge Morante 2996ef2644 fix dev/release script 2024-05-02 16:51:50 +02:00
Jorge Morante 3c7c6a8673 shitty dev/release script 2024-05-02 16:48:45 +02:00
Morantron 03f0048468
Merge pull request #119 from Avimitin/master
put user defined configs to the front of the list
2024-05-02 15:25:51 +02:00
Avimitin 7e693d40cd
put user defined configs to the front of the list
Signed-off-by: Avimitin <dev@avimit.in>
2024-04-29 17:00:09 +08:00
Jorge Morante 4d3fa1eb30 fix blank screen under certain circumstances due to incorrect handling of capture groups 2024-04-05 12:04:33 +02:00
Jorge Morante 47d986b8a2 bump version in shard.yml 2024-03-08 13:36:33 +01:00
Jorge Morante f6813b8b16 update CHANGELOG.md 2024-03-08 13:35:57 +01:00
Jorge Morante 4ba2ec3125 properly restore last pane after invoking fingers 2024-03-08 13:31:32 +01:00
Jorge Morante a30f2fb566 update CHANGELOG.md 2024-01-31 12:47:56 +01:00
Jorge Morante 65a3dd2665 bump version in shard.yml 2024-01-31 12:47:45 +01:00
Jorge Morante c186efa73c Fix "Too many matches" exception in some circumstances
regenerate_hints must be called each time hinter runs
2024-01-31 12:41:52 +01:00
Jorge Morante cd3241ad42 update CHANGELOG.md 2024-01-19 12:00:51 +01:00
Jorge Morante 05a9ece2d9 bump version in shard.yml 2024-01-19 12:00:40 +01:00
Jorge Morante 1be65bc1dc add arch check when downloading binary 2024-01-19 11:53:46 +01:00
Jorge Morante 26d1f67aa9 bump to crystal 1.11.2 2024-01-19 11:53:13 +01:00
Jorge Morante 7e408344b2 recover safely from hinter exceptions 2024-01-18 11:28:10 +01:00
Jorge Morante 680d801770 cache dir should be at root, logs at tmp 2024-01-12 10:22:34 +01:00
Jorge Morante 8cfc2bd1ad change log path to /tmp/fingers.log and remove /tmp hardcoded path 2024-01-12 09:44:12 +01:00
Jorge Morante 6718322da9 add @fingers-jump-key to README TOC 2023-11-18 10:31:37 +01:00
Jorge Morante 27ffac083d update CHANGELOG.md 2023-11-16 07:57:43 +01:00
Jorge Morante eb3c21eae6 bump version in shard.yml 2023-11-16 07:57:35 +01:00
Jorge Morante f079539a65 fix copy/jump when there is a match capture group 2023-11-16 07:55:47 +01:00
Jorge Morante cda4b68f8c update CHANGELOG.md 2023-11-10 15:51:56 +01:00
Jorge Morante 016c1fa8da bump version in shard.yml 2023-11-10 15:50:55 +01:00
Jorge Morante d6ae53daac remove unused :jump: reference 2023-11-10 15:49:32 +01:00
Jorge Morante 13c46d71eb document new @fingers-jump-key option 2023-11-10 15:48:55 +01:00
Jorge Morante 65af29eba9 disallow multiple selection in jump mode 2023-11-10 15:47:34 +01:00
Jorge Morante da3cd9f267 handle line wrapping in jump mode 2023-11-10 15:39:46 +01:00
Jorge Morante 5a891fde67 remove ameba for now 2023-11-10 15:20:11 +01:00
Jorge Morante 2bb5bd2d5b tweak benchmark script 2023-11-10 15:20:11 +01:00
Jorge Morante fcc4a3ad6c adding jump mode functionality 2023-11-10 15:20:11 +01:00
Jorge Morante b154d4800f improve input feedback by discarding unreachable hints 2023-11-10 09:26:23 +01:00
Jorge Morante 6ad4c5530e update CHANGELOG.md 2023-10-26 18:59:03 +02:00
Jorge Morante a47a9b2809 bump version in shard.yml 2023-10-26 18:58:52 +02:00
Jorge Morante 55c47402b3 reduce tmux display-message noisiness and add @fingers-show-copied-notification option 2023-10-26 18:55:41 +02:00
Jorge Morante abca481d49 use structs for HuffmanNode 2023-10-26 18:45:34 +02:00
Jorge Morante 3ef6184fde fix DISALLOWED_CHARS regex 2023-10-26 18:42:38 +02:00
Morantron cb64f9b859
Merge pull request #106 from brttbndr/master
expand_path only if the match starts with ~
2023-10-23 13:45:27 +02:00
Brett Bender 193d01601f multiple conditions -> method should_expand_match? 2023-10-22 09:47:10 -07:00
Brett Bender b2f7567d0a expand_path only if the match starts with ~ 2023-10-20 10:10:01 -07:00
Jorge Morante c9c6d8e79b update CHANGELOG.md 2023-10-06 15:29:45 +02:00
Jorge Morante d65a73fb33 bump version in shard.yml 2023-10-06 15:29:33 +02:00
Jorge Morante 485d0fbd8a update CHANGELOG.md 2023-10-05 19:22:47 +02:00
Jorge Morante 4a93da7c0e use Process.quote in Tmux#set_global_option 2023-10-05 19:21:32 +02:00
Jorge Morante 579a823729 update CHANGELOG.md 2023-09-29 15:59:13 +02:00
Jorge Morante 28c46301da update CHANGELOG.md 2023-09-29 15:58:35 +02:00
Jorge Morante f15d9512ea bump version in shard.yml 2023-09-29 15:57:57 +02:00
Jorge Morante 5edb746f8c update CHANGELOG.md 2023-09-28 17:45:49 +02:00
49 changed files with 1853 additions and 570 deletions

12
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,12 @@
<!--
Note: Please search to see if an issue already exists for the bug you encountered.
-->
### Description
<!-- Description of issue you are experiencing -->
### tmux-fingers info output
<!-- Output of the following command: -->
<!-- tmux run "#{@fingers-cli} info" -->

View file

@ -9,13 +9,15 @@ jobs:
dist_linux:
runs-on: ubuntu-latest
container:
image: crystallang/crystal:latest-alpine
image: crystallang/crystal:1.19-alpine
steps:
- uses: actions/checkout@v3
- name: Update Libs
run: apk add --update --upgrade --no-cache --force-overwrite libxml2-dev yaml-dev yaml-static
- name: Build
run: shards build --production --release --static --no-debug
env:
WIZARD_INSTALLATION_METHOD: download-binary
- name: Upload
uses: actions/upload-release-asset@v1.0.1
env:
@ -25,6 +27,36 @@ jobs:
asset_path: ./bin/tmux-fingers
asset_name: tmux-fingers-${{ github.event.release.tag_name }}-linux-x86_64
asset_content_type: binary/octet-stream
dist_macos_arm64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
crystal: "1.19"
- name: Install static lib dependencies
run: brew install bdw-gc pcre2
- name: Prepare static libs
run: |
mkdir -p bin
ln -sf $(brew --prefix bdw-gc)/lib/libgc.a bin/libgc.a
ln -sf $(brew --prefix pcre2)/lib/libpcre2-8.a bin/libpcre2-8.a
- name: Build
run: |
shards build --production --release --no-debug \
--link-flags="-L $(pwd)/bin $(pwd)/bin/libgc.a $(pwd)/bin/libpcre2-8.a"
env:
WIZARD_INSTALLATION_METHOD: download-binary
- name: Upload
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./bin/tmux-fingers
asset_name: tmux-fingers-${{ github.event.release.tag_name }}-macos-arm64
asset_content_type: binary/octet-stream
dist_homebrew:
name: Bump Homebrew formula
runs-on: ubuntu-latest

93
.github/workflows/pull_request.yml vendored Normal file
View file

@ -0,0 +1,93 @@
name: Pull Request
on:
push:
branches: ['develop', 'master']
pull_request:
workflow_dispatch:
jobs:
spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create /app symlink
run: sudo ln -s $GITHUB_WORKSPACE /app
- name: Install dependencies for tmux and benchmarks
run: sudo apt-get update && sudo apt-get install -y libevent-dev libncurses-dev build-essential bison pkg-config hyperfine
- name: Cache tmux installations
uses: actions/cache@v4
with:
path: /opt/tmux-*
key: tmux-versions-${{ hashFiles('spec/install-tmux-versions.sh') }}
- name: Install tmux versions
run: sudo bash spec/install-tmux-versions.sh
- name: Symlink use-tmux.sh
run: sudo ln -s $GITHUB_WORKSPACE/spec/use-tmux.sh /opt/use-tmux.sh
- uses: crystal-lang/install-crystal@v1
with:
crystal: 1.19
- name: Install dependencies
run: shards install
- name: Run tests
run: crystal spec
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create /app symlink
run: sudo ln -s $GITHUB_WORKSPACE /app
- name: Install dependencies for tmux and benchmarks
run: sudo apt-get update && sudo apt-get install -y libevent-dev libncurses-dev build-essential bison pkg-config hyperfine
- name: Cache tmux installations
uses: actions/cache@v4
with:
path: /opt/tmux-*
key: tmux-versions-${{ hashFiles('spec/install-tmux-versions.sh') }}
- name: Install tmux versions
run: sudo bash spec/install-tmux-versions.sh
- name: Symlink use-tmux.sh
run: sudo ln -s $GITHUB_WORKSPACE/spec/use-tmux.sh /opt/use-tmux.sh
- uses: crystal-lang/install-crystal@v1
with:
crystal: 1.19
- name: Install dependencies
run: shards install
- name: Fetch main branches
run: |
git fetch origin master develop
git branch -f master origin/master 2>/dev/null || true
git branch -f develop origin/develop 2>/dev/null || true
- name: Setup tmux version for benchmark
run: |
sudo /opt/use-tmux.sh 3.3a
which tmux
tmux -V
- name: Run benchmark
run: crystal run dev/benchmark.cr -- ${GITHUB_SHA} develop
env:
PATH: /usr/local/bin:/usr/bin:${{ env.PATH }}
BENCHMARK_RUNS: 250

View file

@ -1,4 +1,146 @@
## 2.0.2 - 27 Sep 2023
## 2.7.1 - 09 Jun 2026
* fix parsing of @fingers-enabled-builtin-patterns ( Fixes #176 )
* update install-wizard and readme to show that shards is required
* relax parsing of --patterns flag in cli
## 2.7.0 - 31 May 2026
* improved performance: ~30% faster
* added macOS arm64 binary to the release/download pipeline
* added kubernetes-pod builtin pattern for pod names
* fixed clipboard integration in WSL
* fixed various styling rendering issues related to submatches and backdrop
* removed tput dependency
* improved error handling
* refactored option parsing, now with stricter validation
* updated to crystal 1.19
## 2.6.2 - 15 Feb 2026
* Fix line jumping and backdrop-style rendering issues related with tabs,
double width characters, and zoomed panes.
## 2.6.1 - 27 Jan 2026
* Fix: close stdin before waiting for load-buffer command ( Fixes #157 )
* handle tab characters properly by prefilling line ( Fixes #155 and #140 )
* add SHELL variable to info command, and properly handle unset env vars
* misc dev improvements
## 2.6.0 - 05 Jan 2026
* Added new @fingers-enable-bindings option ( Fixes #149 )
* Fixed colors in tmux 3.6a
* Fixed case where hint is longer than text to highlight ( Fixes #139 and #154 )
## 2.5.1 - 04 Aug 2025
* Use pane_current_path with presence when chdir ( thanks @na0x2c6 ! )
## 2.5.0 - 17 Jul 2025
* Added new @fingers-use-system-clipboard option ( thanks @purajit ! )
## 2.4.1 - 18 May 2025
* Fix active pane restore when passing a format as target.
## 2.4.0 - 04 Apr 2025
* Add support to tmux target-pane tokens, which allows to target other panes
rather than the active one like: adjacent panes, previous pane, etc... ( Fixes #138 )
## 2.3.3 - 06 Feb 2025
* Fix :paste: action again, this time for real. Working in copy mode and non-copy mode. ( Fixes #137 )
## 2.3.2 - 02 Feb 2025
* :paste: will now exit copy mode right before pasting.
* Documentation improvements.
* Properly handle single quotes in url pattern.
## 2.3.1 - 10 Dec 2024
* Fix :paste: action.
## 2.3.0 - 09 Dec 2024
* Added --(main|ctrl|alt|shift)-action cli options.
* Add info command to show information.
* Improved git-status builtin pattern
* Fixed root key table not being properly restored ( Fixes #130 )
* Updated to Crystal 1.14.
* Various perfomance improvements (from 50ms to 40ms on my machine :tm:)
* Improved benchmark and development scripts.
## 2.2.2 - 14 Aug 2024
* Fixed error in multi-user environments.
* Added XDG dirs, logs are now in ~/.local/state/tmux-fingers/fingers.log instead of /tmp/fingers.log.
* Action stderr is now logged to ~/.local/state/tmux-fingers/action-stderr
## 2.2.1 - 29 Jul 2024
* Tweaks to version detection logic ( Fixes #125 ).
## 2.2.0 - 21 Jul 2024
* Add new option @fingers-enabled-builtin-patterns ( Fixes #19 ).
* New way of setting up tmux-fingers bindings, with command line option to allow using only specific patterns (built-in or custom) ( Fixes #117 )
* Fix issue where zoom panes would be unzoomed when closing ( Fixes #123 ).
* Open tmux-fingers in same path as target pane path ( Fixes #120 ).
## 2.1.5 - 02 May 2024
* User defined patterns now take precedence over built-in ones.
* Fix blank screen under certain circumstances due to incorrect handling of capture groups.
## 2.1.4 - 08 Mar 2024
* Fixed "No last pane" error when using "tmux last-pane" ( Fixes #48 )
## 2.1.3 - 31 Jan 2024
* Fix "Too many matches" exception ( fixes #112 ).
## 2.1.2 - 19 Jan 2024
* Added termux support.
* Fixes to Fingers::Dirs to remove hardcoded paths. Default log path is now /tmp/fingers.log.
* Updated Crystal version to generate clean ELF executables.
* Improve exception handling when rendering.
## 2.1.1 - 16 Nov 2023
* Fix copy/jump when using special named capture group "match".
## 2.1.0 - 10 Nov 2023
* Added new jump functionality.
* Improved visual feedback by discarding unreachable highlights.
## 2.0.6 - 26 Oct 2023
* Added new option @fingers-show-copied-notification ( fixes #104 ).
* Fix problem expanding paths in Mac OS ( thanks @brttbndr ! ).
* Fix hints using disallowed characters like "q" ( fixes #105 ).
* Improve performance in hint generation.
## 2.0.5 - 06 Oct 2023
* Fix git/binary version mismatch again ( fixes #103 ).
## 2.0.4 - 05 Oct 2023
* Fix issues when using backquote as tmux prefix ( fixes #102 ).
## 2.0.3 - 29 Sep 2023
* Fix git/binary version mismatch by publishing a new version ( fixes #101 ).
## 2.0.2 - 28 Sep 2023
* Fix `prefix2` being lost after exiting fingers mode ( fixes #100 ).

View file

@ -1,5 +1,6 @@
FROM crystallang/crystal:latest-alpine
RUN apk upgrade && apk add bash libevent-dev ncurses-dev ncurses hyperfine
FROM crystallang/crystal:1.19-alpine
ENV LANG=en_US.UTF-8
RUN apk upgrade && apk add bash libevent-dev ncurses-dev ncurses hyperfine bison
COPY ./spec/install-tmux-versions.sh /opt/install-tmux-versions.sh
COPY ./spec/use-tmux.sh /opt/use-tmux.sh
RUN bash /opt/install-tmux-versions.sh

View file

@ -3,4 +3,9 @@ default:
shell: default
docker run -it --rm -v $(shell pwd):/app fingers bash
echo "Rebuilding tmux-fingers ..." && shards build --production
dev: default
docker run -it --rm -v $(shell pwd):/app fingers bash -c "/opt/use-tmux.sh 3.6a; 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"
test: default
docker run -it --rm -v $(shell pwd):/app fingers bash -c "shards install && crystal spec"

183
README.md
View file

@ -1,4 +1,3 @@
![tmux-fingers](./logo.svg)
![demo](https://github.com/Morantron/tmux-fingers/assets/3304507/cafe8877-1c98-41b1-bb65-b72129fea701)
@ -20,6 +19,8 @@ Here is a list of the stuff highlighted by default.
* UUIDs
* git status/diff output
Checkout [list of built-in patterns](#fingers-enabled-builtin-patterns).
## Key shortcuts
While in **[fingers]** mode, you can use the following shortcuts:
@ -41,11 +42,15 @@ While in **[fingers]** mode, you can use the following shortcuts:
Add the following to your list of TPM plugins in `.tmux.conf`:
```
```tmux
set -g @plugin 'Morantron/tmux-fingers'
```
Hit <kbd>prefix</kbd> + <kbd>I</kbd> to fetch and source the plugin. The first time you run it you'll be presented with a wizard to complete the installation.
Hit <kbd>prefix</kbd> + <kbd>I</kbd> 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:
- Building from source (requires [crystal](https://crystal-lang.org/install/)). _Available in all platforms_
- Install through [brew](https://brew.sh). _macOS only_.
- Download standalone binary. _Linux x86_64 and macOS arm64 only_.
## Manual
@ -57,7 +62,7 @@ $ git clone https://github.com/Morantron/tmux-fingers ~/.tmux/plugins/tmux-finge
Source it in your `.tmux.conf`:
```
```tmux
run-shell ~/.tmux/plugins/tmux-fingers/tmux-fingers.tmux
```
@ -67,14 +72,15 @@ Reload TMUX conf by running:
$ tmux source-file ~/.tmux.conf
```
The first time you run it you'll be presented with a wizard to complete the installation.
The first time you run it you'll be presented with a wizard to complete the installation.
# Configuration
NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` file.
* [@fingers-key](#fingers-key)
* [@fingers-patterns-N](#fingers-patterns-N)
* [@fingers-jump-key](#fingers-jump-key)
* [@fingers-pattern-N](#fingers-pattern-n)
* [@fingers-main-action](#fingers-main-action)
* [@fingers-ctrl-action](#fingers-ctrl-action)
* [@fingers-alt-action](#fingers-alt-action)
@ -86,24 +92,43 @@ NOTE: for changes to take effect, you'll need to source again your `.tmux.conf`
* [@fingers-selected-highlight-style](#fingers-selected-highlight-style)
* [@fingers-hint-position](#fingers-hint-position)
* [@fingers-keyboard-layout](#fingers-keyboard-layout)
* [@fingers-show-copied-notification](#fingers-show-copied-notification)
* [@fingers-enabled-builtin-patterns](#fingers-enabled-builtin-patterns)
* [@fingers-use-system-clipboard](#fingers-use-system-clipboard)
* [@fingers-enable-bindings](#fingers-enable-bindings)
Recipes:
* [Start tmux-fingers without prefix](#start-tmux-fingers-without-prefix)
* [Using only specific patterns](#using-only-specific-patterns)
* [Using arbitrary commands](#using-arbitrary-commands)
* [Target adjacent panes](#target-adjacent-panes)
## @fingers-key
`default: F`
Customize how to enter fingers mode. Always preceded by prefix: `prefix + @fingers-key`
Customize how to enter fingers mode. Always preceded by prefix: `prefix + @fingers-key`.
For example:
```
```tmux
set -g @fingers-key F
```
## @fingers-patterns-N
## @fingers-jump-key
`default: J`
Customize how to enter fingers jump mode. Always preceded by prefix: `prefix + @fingers-jump-key`.
In jump mode, the cursor will be placed in the position of the match after the hint is selected.
## @fingers-pattern-N
You can also add additional patterns if you want more stuff to be highlighted:
```
```tmux
# You can define custom patterns like this
set -g @fingers-pattern-0 'git rebase --(abort|continue)'
@ -132,7 +157,7 @@ By default **tmux-fingers** will copy matches in tmux and system clipboard.
If you still want to set your own custom command you can do so like this:
```
```tmux
set -g @fingers-main-action '<your command here>'
```
This command will also receive the following:
@ -165,7 +190,7 @@ Same as [@fingers-main-action](#fingers-main-action) but only called when match
## @fingers-hint-style
`default: "fg=green,bold`
`default: "fg=green,bold"`
With this option you can define the styles for the letter hints.
@ -179,13 +204,13 @@ Supported styles are: `bright`, `bold`, `dim`, `underscore`, `italics`.
`default: "fg=yellow"`
Custom styles for the highlighted match. See [@fingers-hint-format](#fingers-hint-format) for more details.
Custom styles for the highlighted match. See [@fingers-hint-style](#fingers-hint-style) for more details.
## @fingers-backdrop-style
`default: ""`
Custom styles for all the text that is not matched. See [@fingers-hint-format](#fingers-hint-format) for more details.
Custom styles for all the text that is not matched. See [@fingers-hint-style](#fingers-hint-style) for more details.
## @fingers-selected-hint-style
@ -233,6 +258,136 @@ Hints are generated taking optimal finger movement into account. You can choose
* `dvorak-right-hand`
* `dvorak-homerow`
## @fingers-show-copied-notification
`default: 0`
Show a message using `tmux display-message` notifying about the copied result.
## @fingers-enabled-builtin-patterns
`default: all`
A list of comma separated pattern names. Built-in patterns are the following:
| Name | Description | Example |
| ----------------- | --------------------------------------------------------- | ---------------------------------------------- |
| ip | ipv4 addresses | `192.168.0.1` |
| uuid | uuid identifier | `f1b43afb-773c-4da2-9ae5-fef1aa6945ce` |
| sha | sha identifier | `c8b911e2c7e9a6cc57143eaa12cad57c1f0d69df` |
| digit | four or more digits | `1337` |
| url | urls (supported protocols: http/https/git/ssh/file) | `https://asdf.com` |
| path | file paths | `path/to/file` |
| hex | hexidecimal numbers | `0x00FF` |
| kubernetes | kubernetes identifier | `deployment.apps/zookeeper` |
| git-status | will match file paths in the output of git status | `modified: ./path/to/file` |
| git-status-branch | will match branch name in the output of git status | `Your branch is up to date withname-of-branch` |
| diff | will match paths in diff output | `+++ a/path/to/file` |
## @fingers-use-system-clipboard
`default: 1`
Whether to use the system clipboard when copying matches. If set to `0`,
tmux-fingers will only copy matches to the tmux buffer.
## @fingers-enable-bindings
`default: 1`
By default, tmux-fingers will bind to `prefix` + `@fingers-key` and `prefix` +
`@fingers-jump-key`. You can opt-out of these bindings in case they clash with
other parts of your configuration.
Check [recipes](#Recipes) for alternative ways to set up bindings.
## @fingers-cli
You can set up key bindings directly to invoke tmux-fingers by using a special global option `@fingers-cli` exposed by the plugin.
The following options are available:
```
Usage:
tmux-fingers start <arguments> [options]
Arguments:
pane_id pane id (also accepts tmux target-pane tokens specified in tmux man pages) (required)
Options:
--mode can be "jump" or "default" (default: default)
--patterns comma separated list of pattern names
--main-action command to which the output will be piped
--ctrl-action command to which the output will be piped when holding CTRL key
--alt-action command to which the output will be piped when holding ALT key
--shift-action command to which the output will be pipedwhen holding SHIFT key
-h, --help prints help
```
Check some examples in the [Recipes](#Recipes) section below.
# Recipes
## Start tmux-fingers without prefix
You can start tmux-fingers without having to press tmux prefix by adding bindings like this:
```tmux
# tmux.conf
# Start tmux fingers by pressing Alt+F
bind -n M-f run -b "#{@fingers-cli} start #{pane_id}"
# Start tmux fingers in jump mode by pressing Alt+J
bind -n M-j run -b "#{@fingers-cli} start #{pane_id} --mode jump"
```
## Using only specific patterns
You can start tmux-fingers with an specific set of built-in or custom patterns.
```tmux
# match urls with prefix + u
bind u run -b "#{@fingers-cli} start #{pane_id} --patterns url"
# match hashes with prefix + h
bind h run -b "#{@fingers-cli} start #{pane_id} --patterns sha"
# match git stuff with prefix + g
bind g run -b "#{@fingers-cli} start #{pane_id} --patterns git-status,git-status-branch"
# match custom pattern with prefix + y
set -g @fingers-pattern-yolo "yolo.*"
bind y run -b "#{@fingers-cli} start #{pane_id} --patterns yolo"
```
## Using arbitrary commands
You can use tmux-fingers with any arbitrary command.
```tmux
# edit file using nvim in a new tmux window with prefix + e
bind e run -b "#{@fingers-cli} start #{pane_id} --patterns path --main-action 'xargs tmux new-window nvim'"
```
## Target adjacent panes
You can use tmux target-pane tokens to target adjacent panes. This
configuration uses vim-style <kbd>ALT</kbd>+<kbd>hjkl</kbd> keys to
directionally adjacent panes.
Also uses <kbd>ALT</kbd>+<kbd>o</kbd> to target the last pane.
```tmux
bind -n M-h run -b "#{@fingers-cli} start {left-of}"
bind -n M-j run -b "#{@fingers-cli} start {down-of}"
bind -n M-k run -b "#{@fingers-cli} start {up-of}"
bind -n M-l run -b "#{@fingers-cli} start {right-of}"
bind -n M-o run -b "#{@fingers-cli} start {last}"
```
# Acknowledgements and inspiration
This plugin is heavily inspired by

View file

@ -1,13 +0,0 @@
#!/usr/bin/env bash
/opt/use-tmux.sh 3.3a
shards build --production
tmux -f /app/spec/conf/benchmark.conf new-session -d
tmux resize-window -t '@0' -x 300 -y 300
tmux send-keys 'COLUMNS=300 LINES=100 crystal run spec/fill_screen.cr'
tmux send-keys Enter
sleep 5
tmux new-window 'hyperfine --warmup 5 "bin/tmux-fingers start %0 self" --export-markdown /tmp/benchmark-output'

119
dev/benchmark.cr Normal file
View file

@ -0,0 +1,119 @@
require "json"
require "tablo"
def fix_git_shit
`git config --global --add safe.directory /app`
end
def cleanup
`tmux kill-server`
`rm -rf ~/.local/state/tmux-fingers`
end
def run_benchmark
runs = ENV["BENCHMARK_RUNS"]? || "100"
`FINGERS_LOG_PATH=fingers.log shards build --production`
puts "running: tmux -f #{Dir.current}/spec/conf/benchmark.conf new-session -d"
`tmux -f #{Dir.current}/spec/conf/benchmark.conf new-session -d`
`tmux resize-window -t '@0' -x 300 -y 300`
`tmux send-keys 'COLUMNS=300 LINES=100 crystal run spec/fill_screen.cr'`
`tmux send-keys Enter`
sleep 5
puts "Running benchmarks with #{runs} runs..."
output_file = File.tempfile("benchmark")
`tmux new-window 'hyperfine --prepare "bash kill-windows.sh" --warmup 5 --runs #{runs} "bin/tmux-fingers start %0" --export-json #{output_file.path}'`
while File.size(output_file.path) == 0
puts "Waiting for benchmark results"
sleep 5
end
cleanup
JSON.parse(output_file)
end
def replace_string_in_file(file_path : String, search_string : String, replace_string : String)
content = File.read(file_path)
updated_content = content.gsub(search_string, replace_string)
File.write(file_path, updated_content)
end
def clone_repo_and_cd(version)
fix_git_shit
repo_path = "/tmp/tmux-fingers-#{version}"
`git clone /app #{repo_path}`
Dir.cd(repo_path)
`git checkout #{version}`
replace_string_in_file(
"#{repo_path}/spec/conf/benchmark.conf",
"/app/tmux-fingers.tmux",
"#{repo_path}/tmux-fingers.tmux"
)
end
versions = ARGV
results = [] of JSON::Any
versions.each do |version|
clone_repo_and_cd(version)
results << run_benchmark
end
data = versions.map_with_index do |version, index|
result = results[index]?
next unless result
[
version,
result["results"][0]["mean"].as_f,
result["results"][0]["stddev"].as_f,
result["results"][0]["min"].as_f,
result["results"][0]["max"].as_f,
]
end.compact
time_format = "%.6fs"
table = Tablo::Table.new(data, wrap_body_cells_to: nil) do |t|
t.add_column("Version", width: 40) { |row| row[0] }
t.add_column("Mean", width: 14) { |row| time_format % row[1] }
t.add_column("Std Dev", width: 14) { |row| time_format % row[2] }
t.add_column("Min", width: 14) { |row| time_format % row[3] }
t.add_column("Max", width: 14) { |row| time_format % row[4] }
end
puts table
# The baseline is the last version passed
threshold = (ENV["BENCHMARK_THRESHOLD"]? || "10").to_f / 100.0
baseline_mean = data.last[1].as(Float64)
regression_found = false
data[0..-2].each do |row|
version = row[0]
mean = row[1].as(Float64)
diff = (mean - baseline_mean) / baseline_mean
if diff > threshold
puts "\nRegression detected: #{version} is #{"%.1f" % (diff * 100)}% slower than baseline #{versions.last} (threshold: #{"%.1f" % (threshold * 100)}%)"
regression_found = true
end
end
exit 1 if regression_found

77
dev/release.cr Normal file
View file

@ -0,0 +1,77 @@
require "semantic_version"
require "yaml"
struct ShardFile
include YAML::Serializable
include YAML::Serializable::Unmapped
property version : String
end
current_version = SemanticVersion.parse(`shards version`.chomp)
current_branch = `git symbolic-ref --short HEAD`.chomp
pending_changes = `git status -s`.chomp
if current_branch != "develop"
puts "This script should be ran from develop branch"
exit 1
end
if pending_changes != ""
puts "There are uncommited changes"
exit 1
end
puts "Which component you want to bump? major.minor.patch"
print "> "
component = gets
puts "Bumping #{component} in #{current_version}"
next_version = case component
when "major"
current_version.bump_major
when "minor"
current_version.bump_minor
when "patch"
current_version.bump_patch
else
current_version.bump_patch
end
shard = ShardFile.from_yaml(File.read("shard.yml"))
shard.version = next_version.to_s
File.write("shard.yml", shard.to_yaml)
current_date = Time.local.to_s("%d %b %Y")
`git add shard.yml`
`git commit -am "bump version in shard.yml"`
content_to_prepend = "## #{next_version.to_s} - #{current_date}\n\nEDIT THIS:\n\n#{`git log --oneline #{current_version}..@`.chomp}\n\n"
original_content = File.read("CHANGELOG.md")
File.write("CHANGELOG.md", content_to_prepend + original_content)
Process.run(ENV["EDITOR"], args: ["CHANGELOG.md"], input: :inherit, output: :inherit, error: :inherit)
`git add CHANGELOG.md`
`git commit -am 'updated CHANGELOG.md'`
print "Confirm release? [Y/n]\n >"
answer = gets
if answer == "n"
puts "Canceling release"
exit 1
end
`git checkout master`
`git merge develop`
`git tag #{next_version.to_s}`
puts "Run the following command to push the release"
puts ""
puts "git push && git push --tags"

View file

@ -1,60 +0,0 @@
# Health check troubleshooting
`health-check.sh` performs a checks to ensure that your system has all dependencies needed to run smoothly.
They are not much, and in most GNU/linux environments these are the defaults anyway.
To run the check you need to run the following command.
`$ /path/to/tmux-fingers/scripts/health-check.sh`
## gawk not found
Install `gawk` package and reload your tmux.conf.
### OSX
```
$ brew install gawk && tmux source ~/.tmux.conf
```
### Linux
* Ubuntu: `$ sudo aptitude install gawk && tmux source ~/.tmux.conf`
* Arch linux: `$ sudo pacman -S install gawk && tmux source ~/.tmux.conf`
## bash version is too old
This probably means you are running OSX, which ships with *bash 3*. In order to upgrade to *bash 4* you need need to run:
`$ brew install bash`
## tmux version is too old
You can install latest *tmux* from source, check https://github.com/tmux/tmux
## Wrong $TERM value
Tmux fingers works better with proper 256 support. Set `@default-terminal` to either `screen-256color` or `xterm-256color`.
```
set -g @default-terminal "screen-256color"
```
## submodules not initialized properly
This could happen after an update from a tmux-fingers version prior to 0.6.x,
ensure that all tmux-fingers dependencies are installed properly by running the
following:
```
cd ~/.tmux/plugins/tmux-fingers
git submodule update --init --recursive"
tmux source ~/.tmux.conf"
```
## system clipboard integration
In order for system clipboard integration to work, you need to have program in your system that manages that.
Check [tmux-yank documentation](https://github.com/tmux-plugins/tmux-yank#requirements) to check what program do you need to install, depending on your platform.

View file

@ -4,7 +4,6 @@ CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PLATFORM=$(uname -s)
action=$1
# set up exit trap
function finish {
exit_code=$?
@ -32,14 +31,26 @@ trap finish EXIT
function install_from_source() {
echo "Installing from source..."
# check if crystal is installed
if ! command -v crystal >/dev/null 2>&1; then
echo "crystal is not installed. Please install it first."
echo ""
echo " https://crystal-lang.org/install/"
echo ""
exit 1
fi
# check if shards is installed
if ! command -v shards >/dev/null 2>&1; then
echo "crystal is not installed. Please install it first."
echo "shards is not installed. Please install it first."
echo ""
echo " https://crystal-lang.org/reference/latest/man/shards/index.html"
echo ""
exit 1
fi
pushd $CURRENT_DIR > /dev/null
shards build --production
WIZARD_INSTALLATION_METHOD=build-from-source shards build --production
popd > /dev/null
echo "Build complete!"
@ -56,18 +67,37 @@ function install_with_brew() {
}
function asset_suffix() {
local arch=$(uname -m)
if [[ "$PLATFORM" == "Linux" && "$arch" == "x86_64" ]]; then
echo "linux-x86_64"
elif [[ "$PLATFORM" == "Darwin" && "$arch" == "arm64" ]]; then
echo "macos-arm64"
fi
}
function download_binary() {
local suffix=$(asset_suffix)
if [[ -z "$suffix" ]]; then
echo "No pre-built binary available for your platform ($(uname -s) $(uname -m))."
exit 1
fi
mkdir -p $CURRENT_DIR/bin
# TODO check architecture
echo "Getting latest release..."
# TODO use "latest" tag
url=$(curl -s "https://api.github.com/repos/morantron/tmux-fingers/releases" | grep browser_download_url | head -1 | grep -o https://.*x86_64)
url=$(curl -s "https://api.github.com/repos/Morantron/tmux-fingers/releases" | grep -o "https://.*${suffix}" | head -1)
if [[ -z "$url" ]]; then
echo "Could not find a release for tmux-fingers. Please try again later."
exit 1
fi
echo "Downloading binary from $url"
# download binary to bin/tmux-fingers
curl -L $url -o $CURRENT_DIR/bin/tmux-fingers
chmod a+x $CURRENT_DIR/bin/tmux-fingers
@ -89,40 +119,37 @@ if [[ "$1" == "install-from-source" ]]; then
install_from_source
fi
function binary_or_brew_label() {
if [[ "$PLATFORM" == "Darwin" ]]; then
echo "Install with brew"
else
echo "Download binary"
fi
}
function binary_or_brew_action() {
if [[ "$PLATFORM" == "Darwin" ]]; then
echo "install-with-brew"
else
echo "download-binary"
fi
}
function get_message() {
if [[ "$FINGERS_UPDATE" == "1" ]]; then
echo "It looks like tmux-fingers has been updated. We need to rebuild the binary."
else
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."
fi
}
tmux display-menu -T "tmux-fingers" \
"" \
"- " "" ""\
"- #[nodim,bold]Welcome to tmux-fingers! ✌️ " "" ""\
"- " "" ""\
"- $(get_message) " "" "" \
"- " "" ""\
"" \
"$(binary_or_brew_label)" b "new-window \"$CURRENT_DIR/install-wizard.sh $(binary_or_brew_action)\"" \
"Build from source" s "new-window \"$CURRENT_DIR/install-wizard.sh install-from-source\"" \
"" \
menu_args=(
-T "tmux-fingers"
""
"- " "" ""
"- #[nodim,bold]Welcome to tmux-fingers! ✌️ " "" ""
"- " "" ""
"- $(get_message) " "" ""
"- " "" ""
""
)
if [[ -n "$(asset_suffix)" ]]; then
menu_args+=("Download binary" b "new-window \"$CURRENT_DIR/install-wizard.sh download-binary\"")
fi
if [[ "$PLATFORM" == "Darwin" ]]; then
menu_args+=("Install with brew" w "new-window \"$CURRENT_DIR/install-wizard.sh install-with-brew\"")
fi
menu_args+=(
"Build from source (crystal required)" s "new-window \"$CURRENT_DIR/install-wizard.sh install-from-source\""
""
"Exit" q ""
)
tmux display-menu "${menu_args[@]}"

View file

@ -1,3 +1,3 @@
for id in $(tmux list-windows -F "#{window_id}:#{pane_id}:#{window_name}" | grep -v ":$1:" | grep "fingers" | cut -f1 -d:); do
for id in $(tmux list-windows -F "#{window_id}:#{pane_id}:#{window_name}" | grep -v ":%0:" | grep "fingers" | cut -f1 -d:); do
tmux kill-window -t $id
done

View file

@ -1,6 +1,14 @@
version: 2.0
shards:
ameba:
git: https://github.com/crystal-ameba/ameba.git
version: 1.3.1
cling:
git: https://github.com/devnote-dev/cling.git
version: 3.0.0
tablo:
git: https://github.com/hutou/tablo.git
version: 0.10.1
xdg_base_directory:
git: https://github.com/tijn/xdg_base_directory.git
version: 1.0.5+git.commit.60bf28dc060c221d5af52727927e92b840022eb0

View file

@ -1,18 +1,18 @@
---
version: 2.7.1
name: fingers
version: 2.0.1
authors:
- Jorge Morante <jorge@morante.eu>
- Jorge Morante <jorge@morante.eu>
targets:
tmux-fingers:
main: src/fingers.cr
development_dependencies:
ameba:
github: crystal-ameba/ameba
version: ~> 1.3.0
crystal: 1.9.2
dependencies:
cling:
github: devnote-dev/cling
version: '>= 3.0.0'
xdg_base_directory:
github: tijn/xdg_base_directory
tablo:
github: hutou/tablo
crystal: 1.19
license: MIT

7
spec/conf/dev.conf Normal file
View file

@ -0,0 +1,7 @@
set -g default-terminal 'screen-256color'
set -g @fingers-skip-wizard '1'
bind C-r source-file /app/spec/conf/dev.conf
bind Enter run "cd /app && FINGERS_LOG_PATH='/app/fingers.log' shards build --production"
run '/app/tmux-fingers.tmux'

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -e
if [[ -n "$CI_TMUX_VERSION" ]]; then
VERSIONS=("$CI_TMUX_VERSION")
else
VERSIONS=("3.0a" "3.1c" "3.2a" "3.3a")
VERSIONS=("3.0a" "3.1c" "3.2a" "3.3a" "3.4" "3.5a" "3.6a")
fi
mkdir -p /opt
chmod a+w /opt
pushd /tmp
for version in "${VERSIONS[@]}";
@ -22,6 +22,7 @@ pushd /tmp
pushd "/opt/tmux-${version}"
./configure
make
chmod -R a+r /opt/tmux-${version}
popd
done
popd

View file

@ -35,7 +35,7 @@ describe Fingers::Hinter do
input = generate_lines
output = TextOutput.new
patterns = Fingers::Config::DEFAULT_PATTERNS.values.to_a
patterns = Fingers::BUILTIN_PATTERNS.values.to_a
alphabet = "asdf".split("")
hinter = Fingers::Hinter.new(
@ -46,13 +46,6 @@ describe Fingers::Hinter do
alphabet: alphabet,
output: output,
)
puts "before"
puts input
puts "---------"
puts "after"
hinter.run
puts output.contents
end
it "only highlights captured groups" do
@ -77,7 +70,7 @@ Changes not staged for commit:
"
output = TextOutput.new
patterns = Fingers::Config::DEFAULT_PATTERNS.values.to_a
patterns = Fingers::BUILTIN_PATTERNS.values.to_a
patterns << "On branch (?<capture>.*)"
alphabet = "asdf".split("")
@ -89,12 +82,58 @@ Changes not staged for commit:
alphabet: alphabet,
output: output,
)
end
it "only reuses hints when allow duplicates is false" do
width = 100
output = TextOutput.new
patterns = Fingers::BUILTIN_PATTERNS.values.to_a
alphabet = "asdf".split("")
input = "
modified: src/fingers/cli.cr
modified: src/fingers/cli.cr
modified: src/fingers/cli.cr
"
hinter = Fingers::Hinter.new(
input: input.split("\n"),
width: width,
patterns: patterns,
state: ::Fingers::State.new,
alphabet: alphabet,
output: output,
reuse_hints: false
)
puts "before"
puts input
puts "---------"
puts "after"
hinter.run
puts output.contents
end
it "can rerender when not reusing hints" do
width = 100
output = TextOutput.new
patterns = Fingers::BUILTIN_PATTERNS.values.to_a
alphabet = "asdf".split("")
input = "
modified: src/fingers/cli.cr
modified: src/fingers/cli.cr
modified: src/fingers/cli.cr
"
hinter = Fingers::Hinter.new(
input: input.split("\n"),
width: width,
patterns: patterns,
state: ::Fingers::State.new,
alphabet: alphabet,
output: output,
reuse_hints: false
)
hinter.run
hinter.run
end
end

View file

@ -3,11 +3,11 @@ require "../../../src/fingers/dirs"
require "../../../src/fingers/match_formatter"
def setup(
hint_style : String = "#[fg=yellow,bold]",
highlight_style : String = "#[fg=yellow]",
hint_style : String = "#[hint]",
highlight_style : String = "#[highlight]",
hint_position : String = "left",
selected_hint_style : String = "#[fg=green,bold]",
selected_highlight_style : String = "#[fg=green]",
selected_hint_style : String = "#[selected_hint]",
selected_highlight_style : String = "#[selected_highlight]",
selected : Bool = false,
offset : Tuple(Int32, Int32) | Nil = nil,
hint : String = "a",
@ -15,7 +15,7 @@ def setup(
)
formatter = Fingers::MatchFormatter.new(
highlight_style: highlight_style,
backdrop_style: "#[bg=black,fg=white]",
backdrop_style: "#[backdrop]",
hint_style: hint_style,
selected_highlight_style: selected_highlight_style,
selected_hint_style: selected_hint_style,
@ -31,14 +31,14 @@ describe Fingers::MatchFormatter do
context "is set to left" do
it "places the hint on the left side" do
result = setup(hint_position: "left")
result.should eq("#[reset]#[fg=yellow,bold]a#[reset]#[fg=yellow]olo#[reset]#[bg=black,fg=white]")
result.should eq("#[reset]#[reset]#[hint]a#[reset]#[highlight]olo#[reset]#[backdrop]")
end
end
context "is set to right" do
it "places the hint on the right side" do
result = setup(hint_position: "right")
result.should eq("#[reset]#[fg=yellow]yol#[reset]#[fg=yellow,bold]a#[reset]#[bg=black,fg=white]")
result.should eq("#[reset]#[reset]#[highlight]yol#[reset]#[hint]a#[reset]#[backdrop]")
end
end
end
@ -46,14 +46,21 @@ describe Fingers::MatchFormatter do
context "when a hint is selected" do
it "selects the correct format" do
result = setup(selected: true)
result.should eq("#[reset]#[fg=green,bold]a#[reset]#[fg=green]olo#[reset]#[bg=black,fg=white]")
result.should eq("#[reset]#[reset]#[selected_hint]a#[reset]#[selected_highlight]olo#[reset]#[backdrop]")
end
end
context "when offset is provided" do
it "only highlights at specified offset" do
result = setup(offset: {1, 5}, highlight: "yoloyoloyolo", hint: "a")
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]")
result.should eq("#[reset]#[backdrop]y#[reset]#[hint]a#[reset]#[highlight]loyo#[reset]#[backdrop]loyolo#[backdrop]")
end
end
context "when offset is at the beginning" do
it "only highlights at specified offset" do
result = setup(offset: {0, 3}, highlight: "yolo", hint: "a")
result.should eq("#[reset]#[reset]#[hint]a#[reset]#[highlight]ol#[reset]#[backdrop]o#[backdrop]")
end
end
end

View file

@ -3,11 +3,11 @@ require "../../src/fingers/config"
require "string_scanner"
def matches_for(pattern_name, input)
pattern = Regex.new(::Fingers::Config::DEFAULT_PATTERNS[pattern_name])
pattern = Regex.new(::Fingers::BUILTIN_PATTERNS[pattern_name])
input.scan(pattern).map { |m| m["match"]? || m[0] }
end
describe "default patterns" do
describe "builtin patterns" do
describe "ip" do
it "should match ip addresses" do
input = "

View file

@ -1,31 +0,0 @@
require 'spec_helper'
describe TmuxFormatPrinter do
let(:printer) do
class FakeShell
def exec(cmd)
"$(#{cmd})"
end
end
TmuxFormatPrinter.new(shell: FakeShell.new)
end
it 'transforms tmux status line format into escape sequences' do
result = printer.print('bg=red,fg=yellow,bold', reset_styles_after: true)
expected = '$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)'
expect(result).to eq(expected)
end
it 'transforms tmux status line format into escape sequences' do
result = printer.print('bg=red,fg=yellow,bold', reset_styles_after: true)
expected = '$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)'
expect(result).to eq(expected)
end
xit 'raises on unknown formats' do
# TODO
end
end

View file

@ -1,25 +1,11 @@
require "spec"
require "../../src/tmux_style_printer"
class FakeShell < TmuxStylePrinter::Shell
def exec(cmd)
"$(#{cmd})"
end
end
describe TmuxStylePrinter do
it "transforms tmux status line format into escape sequences" do
printer = TmuxStylePrinter.new(shell = FakeShell.new)
printer = TmuxStylePrinter.new
result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true)
expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)"
result.should eq expected
end
it "transforms tmux status line format into escape sequences" do
printer = TmuxStylePrinter.new(shell = FakeShell.new)
result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true)
expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)"
expected = "\e[48;5;1m\e[38;5;3m\e[1m\e[0m"
result.should eq expected
end

View file

@ -1,9 +1,15 @@
require "./fingers/logger"
require "./fingers/constants"
require "./fingers/options"
require "./fingers/cli"
def running_in_specs? : Bool
{{ @type.has_constant?("Spec") }}
end
module Fingers
VERSION = {{ %(#{`shards version`.chomp}) }}
cli = Cli.new
cli.run
cli.run unless running_in_specs?
end

View file

@ -4,13 +4,24 @@ module Fingers
class ActionRunner
@final_shell_command : String | Nil
def initialize(@modifier : String, @match : String, @hint : String, @original_pane : Tmux::Pane)
def initialize(
@modifier : String,
@match : String,
@hint : String,
@original_pane : Tmux::Pane,
@offset : Tuple(Int32, Int32) | Nil,
@mode : String,
@main_action : String | Nil,
@ctrl_action : String | Nil,
@alt_action : String | Nil,
@shift_action : String | Nil,
)
end
def run
tmux.set_buffer(match)
tmux.set_buffer(match, Fingers.config.use_system_clipboard)
return if final_shell_command.nil?
return if final_shell_command.nil? || final_shell_command.not_nil!.empty?
cmd_path, *args = Process.parse_arguments(final_shell_command.not_nil!)
@ -19,8 +30,8 @@ module Fingers
args,
input: :pipe,
output: :pipe,
error: File.open("/tmp/action-stderr", "a"),
chdir: original_pane.pane_current_path,
error: File.open(::Fingers::Dirs::ROOT / "action-stderr", "a"),
chdir: original_pane.pane_current_path.presence,
env: action_env
)
@ -28,23 +39,28 @@ module Fingers
cmd.input.flush
end
private getter :match, :modifier, :hint, :original_pane
private getter :match, :modifier, :hint, :original_pane, :offset, :mode, :main_action, :ctrl_action, :alt_action, :shift_action
def final_shell_command
return jump if mode == "jump"
return @final_shell_command if @final_shell_command
@final_shell_command = case action
when ":copy:"
copy
when ":open:"
open
when ":paste:"
paste
when nil
# do nothing
else
shell_action
end
@final_shell_command = action_command
end
private def action_command
case action
when ":copy:"
copy
when ":open:"
open
when ":paste:"
paste
when nil
# do nothing
else
shell_action
end
end
def copy
@ -59,8 +75,23 @@ module Fingers
system_open_command
end
def jump
return nil if offset.nil?
`tmux copy-mode -t #{original_pane.pane_id}`
`tmux send-keys -t #{original_pane.pane_id} -X top-line`
`tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![0]} -X cursor-down`
`tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![1]} -X cursor-right`
nil
end
def paste
"tmux paste-buffer"
if original_pane.pane_in_mode
"tmux send-keys -t #{original_pane.pane_id} -X cancel \; paste-buffer -t #{original_pane.pane_id}"
else
"tmux paste-buffer -t #{original_pane.pane_id}"
end
end
def shell_action
@ -74,17 +105,19 @@ module Fingers
private property action : String | Nil do
case modifier
when "main"
Fingers.config.main_action
main_action || Fingers.config.main_action
when "shift"
Fingers.config.shift_action
shift_action || Fingers.config.shift_action
when "alt"
Fingers.config.alt_action
alt_action || Fingers.config.alt_action
when "ctrl"
Fingers.config.ctrl_action
ctrl_action || Fingers.config.ctrl_action
end
end
def system_copy_command
return nil unless Fingers.config.use_system_clipboard
@system_copy_command ||= if program_exists?("pbcopy")
if program_exists?("reattach-to-user-namespace")
"reattach-to-user-namespace"
@ -92,7 +125,7 @@ module Fingers
"pbcopy"
end
elsif program_exists?("clip.exe")
"cat | clip.exe"
"clip.exe"
elsif program_exists?("wl-copy")
"wl-copy"
elsif program_exists?("xclip")
@ -124,9 +157,14 @@ module Fingers
# This takes care of some path expansion weirdness when opening paths that start with ~ in MacOS
def expanded_match
return match unless action == ":open:"
return match unless should_expand_match?
Path[match].expand(base: original_pane.pane_current_path, home: Path.home)
end
private def should_expand_match?
action == ":open:" && match.starts_with?("~")
end
end
end

View file

@ -1,24 +1,33 @@
require "cling"
require "./commands/*"
module Fingers
class MainCommand < Cling::Command
def setup : Nil
@description = "description"
@name = "tmux-fingers"
add_command Fingers::Commands::Version.new
add_command Fingers::Commands::LoadConfig.new
add_command Fingers::Commands::SendInput.new
add_command Fingers::Commands::Start.new
add_command Fingers::Commands::Info.new
end
def run(arguments, options) : Nil
puts help_template
end
end
class Cli
def run
return if ARGV.size == 0
main = MainCommand.new
command, *args = ARGV
cmd = case command
when "start"
Fingers::Commands::Start.new(args)
when "load-config"
Fingers::Commands::LoadConfig.new(args)
when "send-input"
Fingers::Commands::SendInput.new(args)
when "version"
Fingers::Commands::Version.new(args)
end
cmd.run if cmd
main.execute ARGV
end
end
end
# fingers load-config
# fingers version
# fingers send-input INPUT
# fingers start --mode default|jump --pane #{pane_id}

View file

@ -0,0 +1,32 @@
require "cling"
require "tablo"
class Fingers::Commands::Info < Cling::Command
WIZARD_INSTALLATION_METHOD = {{ env("WIZARD_INSTALLATION_METHOD") }}
def setup : Nil
@name = "info"
end
def run(arguments, options) : Nil
data = [
["tmux-fingers", "#{Fingers::VERSION}"],
["xdg-root-folder", "#{Fingers::Dirs::ROOT}"],
["log-path", "#{Fingers::Dirs::LOG_PATH}"],
["installation-method", "#{WIZARD_INSTALLATION_METHOD || "manual"}"],
["tmux-version", `tmux -V`.chomp],
["TERM", ENV["TERM"]? || "not set"],
["SHELL", ENV["SHELL"]? || "not set"],
["crystal-version", Crystal::VERSION]
]
opt_width = data.map { |n| n[0].size }.max
val_width = data.map { |n| n[1].size }.max
table = Tablo::Table.new(data, header_frequency: nil) do |t|
t.add_column("Option", width: opt_width) { |n| n[0] }
t.add_column("Value", width: val_width) { |n| n[1] }
end
puts table
end
end

View file

@ -1,22 +1,28 @@
require "cling"
require "file_utils"
require "./base"
require "../dirs"
require "../config"
require "../options"
require "../options/*"
require "../../tmux"
require "colorize"
class Fingers::Commands::LoadConfig < Fingers::Commands::Base
class Fingers::Commands::LoadConfig < Cling::Command
@fingers_options_names : Array(String) | Nil
property config : Fingers::Config
property config : Fingers::Config = Fingers::Config.new
DISALLOWED_CHARS = /cimqn/
PRIVATE_OPTIONS = [
"cli"
]
def initialize(*args)
super(*args)
def setup : Nil
@config = Fingers::Config.new
@name = "load-config"
end
def run
def run(arguments, options) : Nil
validate_options!
parse_tmux_conf
setup_bindings
@ -27,74 +33,45 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base
def parse_tmux_conf
options = shell_safe_options
user_defined_patterns = [] of String
Fingers.reset_config
config.tmux_version = `tmux -V`.chomp.split(" ").last
options.each do |option, value|
# TODO generate an enum somehow and use an exhaustive case
case option
when "key"
config.key = value
when "keyboard_layout"
config.keyboard_layout = value
when "main_action"
config.main_action = value
when "ctrl_action"
config.ctrl_action = value
when "alt_action"
config.alt_action = value
when "shift_action"
config.shift_action = value
when "benchmark_mode"
config.benchmark_mode = value
when "hint_position"
config.hint_position = value
when "hint_style"
config.hint_style = tmux.parse_style(value)
when "selected_hint_style"
config.selected_hint_style = tmux.parse_style(value)
when "highlight_style"
config.highlight_style = tmux.parse_style(value)
when "backdrop_style"
config.backdrop_style = tmux.parse_style(value)
when "selected_highlight_style"
config.selected_highlight_style = tmux.parse_style(value)
end
if option.match(/pattern/)
check_pattern!(value)
user_defined_patterns.push(value)
end
Fingers::Options.parse(option, value, config)
end
config.patterns = clean_up_patterns([
*enabled_default_patterns,
*user_defined_patterns,
])
add_builtin_patterns
config.alphabet = ::Fingers::Config::ALPHABET_MAP[Fingers.config.keyboard_layout].split("").reject do |char|
char.match(DISALLOWED_CHARS)
end
config.tmux_version = tmux_version
config.save
Fingers.reset_config
rescue e : TmuxStylePrinter::InvalidFormat
puts "[tmux-fingers] #{e.message}"
exit(1)
end
def clean_up_patterns(patterns)
patterns.reject(&.empty?)
def add_builtin_patterns
pattern_names = [] of String
if config.enabled_builtin_patterns == "all"
pattern_names = ::Fingers::BUILTIN_PATTERNS.keys
else
pattern_names = config.enabled_builtin_patterns.split(",")
end
pattern_names.each do |name|
pattern = Fingers::BUILTIN_PATTERNS[name]?
config.patterns[name.to_s] = pattern if pattern
end
end
def setup_bindings
`tmux bind-key #{Fingers.config.key} run-shell -b "#{cli} start "\#{pane_id}" self >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
`tmux bind-key O run-shell -b "#{cli} start "\#{pane_id}" other >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
setup_root_bindings if Fingers.config.enable_bindings
setup_fingers_mode_bindings
`tmux set-option -g @fingers-cli #{cli}`
end
def setup_root_bindings
`tmux bind-key #{Fingers.config.key} run-shell -b "#{cli} start "\#{pane_id}" >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
`tmux bind-key #{Fingers.config.jump_key} run-shell -b "#{cli} start --mode jump "\#{pane_id}" >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
end
def setup_fingers_mode_bindings
@ -120,32 +97,19 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base
fingers_mode_bind("Any", "noop")
end
def enabled_default_patterns
::Fingers::Config::DEFAULT_PATTERNS.values
end
def to_bool(input)
input == "1"
end
def shell_safe_options
options = {} of String => String
fingers_options_names.each do |option|
option_method = option_to_method(option)
fingers_options_names.each do |tmux_option|
option = from_tmux_option(tmux_option)
next if PRIVATE_OPTIONS.includes?(option)
options[option_method] = `tmux show-option -gv #{option}`.chomp
options[option] = `tmux show-option -gv #{tmux_option}`.chomp
end
options
end
def valid_option?(option)
option_method = option_to_method(option)
@config.members.includes?(option_method) || option_method.match(/pattern_[0-9]+/) || option_method == "skip_wizard"
end
def fingers_options_names
@fingers_options_names ||= `tmux show-options -g | grep ^@fingers`
.chomp.split("\n")
@ -153,39 +117,30 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base
.reject { |option| option.empty? }
end
def unset_tmux_option!(option)
`tmux set-option -ug #{option}`
end
def validate_options!
options = shell_safe_options
def check_pattern!(pattern)
begin
Regex.new(pattern)
rescue e: ArgumentError
puts "[tmux-fingers] Invalid pattern: #{pattern}"
puts "[tmux-fingers] #{e.message}"
errors = [] of String
options.each do |option, value|
is_valid, message = Fingers::Options.valid?(option, value)
errors << "#{to_tmux_option(option).colorize.bold}: #{message}" unless is_valid
end
unless errors.empty?
puts "[tmux-fingers] Configuration errors:".colorize(:red)
errors.each { |error| puts " - #{error}" }
exit(1)
end
end
def validate_options!
errors = [] of String
fingers_options_names.each do |option|
unless valid_option?(option)
errors << "'#{option}' is not a valid option"
unset_tmux_option!(option)
end
end
return if errors.empty?
puts "[tmux-fingers] Errors found in tmux.conf:"
errors.each { |error| puts " - #{error}" }
exit(1)
def from_tmux_option(value)
value.gsub(/^@fingers-/, "").tr("-", "_")
end
def option_to_method(option)
option.gsub(/^@fingers-/, "").tr("-", "_")
def to_tmux_option(value)
"@fingers-#{value.to_s.tr("_", "-")}"
end
def fingers_mode_bind(key, command)
@ -197,6 +152,10 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base
end
def tmux
Tmux.new(`tmux -V`.chomp.split(" ").last)
Tmux.new(tmux_version)
end
def tmux_version
`tmux -V`.chomp.split(" ").last
end
end

View file

@ -1,11 +1,18 @@
require "./base"
require "cling"
module Fingers::Commands
class SendInput < Base
def run
class SendInput < Cling::Command
def setup : Nil
@name = "send-input"
@hidden = true
add_argument "input", required: true
end
def run(arguments, options) : Nil
socket = InputSocket.new
socket.send_message(@args[0])
socket.send_message(arguments.get("input"))
end
end
end

View file

@ -1,3 +1,4 @@
require "cling"
require "./base"
require "../hinter"
require "../view"
@ -24,27 +25,121 @@ module Fingers::Commands
end
end
class Start < Base
class Start < Cling::Command
@original_options : Hash(String, String) = {} of String => String
@last_key_table : String = "root"
@last_pane_id : String | Nil
@mode : String = "default"
@pane_id : String = ""
@active_pane : Tmux::Pane | Nil
@patterns : Array(String) = [] of String
@main_action : String | Nil
@ctrl_action : String | Nil
@shift_action : String | Nil
@alt_action : String | Nil
def setup : Nil
@name = "start"
add_argument "pane_id",
description: "pane id (also accepts tmux target-pane tokens specified in tmux man pages)",
required: true
add_option "mode",
description: "can be \"jump\" or \"default\"",
type: :single,
default: "default"
add_option "patterns",
description: "comma separated list of pattern names",
type: :single
add_option "main-action",
description: "command to which the output will be piped",
type: :single
add_option "ctrl-action",
description: "command to which the output will be piped when holding CTRL key",
type: :single
add_option "alt-action",
description: "command to which the output will be piped when holding ALT key",
type: :single
add_option "shift-action",
description: "command to which the output will be pipedwhen holding SHIFT key",
type: :single
add_option 'h', "help", description: "prints help"
end
def pre_run(arguments, options) : Bool
if options.has?("help")
puts help_template
false
else
true
end
end
def run(arguments, options) : Nil
@mode = options.get("mode").as_s
parse_pane_target_format!(arguments.get("pane_id").as_s)
if options.has?("patterns")
@patterns = patterns_from_options(options.get("patterns").as_s)
else
@patterns = Fingers.config.patterns.values
end
@main_action = options.get?("main-action").try(&.as_s)
@ctrl_action = options.get?("ctrl-action").try(&.as_s)
@alt_action = options.get?("alt-action").try(&.as_s)
@shift_action = options.get?("shift-action").try(&.as_s)
track_tmux_state
def run
track_options_to_restore!
show_hints
if Fingers.config.benchmark_mode == "1"
if Fingers.config.benchmark_mode
exit(0)
end
handle_input
process_result
teardown
end
private def track_options_to_restore!
options_to_preserve.each do |option|
value = tmux.get_global_option(option)
@original_options[option] = value
private def patterns_from_options(pattern_names_option : String)
pattern_names = pattern_names_option.split(",")
result = [] of String
pattern_names.each do |pattern_name|
pattern = Fingers.config.patterns[pattern_name]?
if pattern
result << pattern
else
tmux.display_message("[tmux-fingers] error: Unknown pattern #{pattern_name}", 5000)
exit 0
end
end
result
end
private def track_tmux_state
output = tmux.exec("display-message -t '{last}' -p '\#{pane_id};\#{client_key_table};\#{prefix};\#{prefix2}'").chomp
last_pane_id, last_key_table, prefix, prefix2 = output.split(";")
@last_pane_id = last_pane_id
@last_key_table = if last_key_table.empty?
"root"
else
last_key_table
end
@original_options["prefix"] = prefix
@original_options["prefix2"] = prefix2
end
private def restore_options
@ -53,22 +148,48 @@ module Fingers::Commands
end
end
private def restore_last_key_table
tmux.set_key_table(@last_key_table)
end
private def restore_last_pane
tmux.select_pane(@last_pane_id)
select_active_pane
end
private def options_to_preserve
%w[prefix prefix2]
end
private def parse_pane_target_format!(pane_target_format)
if pane_target_format.match(/^%[0-9]+$/)
@pane_id = pane_target_format
@active_pane = target_pane
else
@pane_id = tmux.exec("display-message -t #{pane_target_format} -p '\#{pane_id}'").chomp
@active_pane = tmux.list_panes("\#{pane_active}", target_pane.window_id).first
end
end
private def show_hints
# Attention! It is very important to resize the window at this point to
# match the dimensions of the target pane. Otherwise weird linejumping
# will occur when we have wrapped lines.
# It is very important to resize the window at this point to match the
# dimensions of the target pane. Otherwise weird linejumping will occur
# when we have wrapped lines.
tmux.resize_window(
fingers_window.window_id,
target_pane.pane_width,
target_pane.pane_height,
) if needs_resize?
view.render
tmux.swap_panes(fingers_window.pane_id, target_pane.pane_id)
# Swapping panes with -Z flag causes some issues with rendering panes
# with tabs or double width characters
if target_pane.window_zoomed_flag
tmux.swap_panes(fingers_window.pane_id, target_pane.pane_id)
view.render
else
view.render
tmux.swap_panes(fingers_window.pane_id, target_pane.pane_id)
end
end
private def handle_input
@ -83,23 +204,61 @@ module Fingers::Commands
end
end
private def process_result
return unless state.result
match = hinter.lookup(state.input)
ActionRunner.new(
hint: state.input,
modifier: state.modifier,
match: state.result,
original_pane: active_pane,
offset: match ? match.not_nil!.offset : nil,
mode: mode,
main_action: @main_action,
ctrl_action: @ctrl_action,
alt_action: @alt_action,
shift_action: @shift_action,
).run
tmux.display_message("Copied: #{state.result}", 1000) if should_notify?
rescue e
Log.error { "Error processing result: #{e.message}" }
tmux.display_message("#[bg=red,fg=white,bold][tmux-fingers]#[bg=red,fg=black,nobold] #{e.message}".ljust(target_pane.pane_width * 2, ' '), 3000)
end
private def select_active_pane
tmux.select_pane(active_pane.pane_id)
end
private def needs_resize?
pane_width = target_pane.pane_width.to_i
pane_contents.any? { |line| line.size > pane_width }
pane_contents.any? do |line|
line.bytesize > line.size || line.size > pane_width
end
end
private def teardown
tmux.set_key_table "root"
tmux.swap_panes(fingers_pane_id, target_pane.pane_id)
tmux.kill_pane(fingers_pane_id)
restore_last_pane
restore_last_key_table
restore_options
view.run_action if state.result
end
private getter target_pane : Tmux::Pane do
tmux.find_pane_by_id(@args[0]).not_nil!
tmux.find_pane_by_id(@pane_id).not_nil!
end
private getter active_pane : Tmux::Pane do
@active_pane.not_nil!
end
private getter mode : String do
@mode.not_nil!
end
private getter fingers_window : Tmux::Window do
@ -121,14 +280,16 @@ module Fingers::Commands
private getter hinter : Hinter do
Fingers::Hinter.new(
input: pane_contents,
patterns: @patterns,
width: target_pane.pane_width.to_i,
state: state,
output: pane_printer
output: pane_printer,
reuse_hints: mode != "jump",
)
end
private getter pane_contents : Array(String) do
tmux.capture_pane(target_pane).split("\n")
tmux.capture_pane(target_pane, join: mode != "jump").split("\n")
end
private getter view : View do
@ -137,12 +298,17 @@ module Fingers::Commands
state: state,
output: pane_printer,
original_pane: target_pane,
tmux: tmux
tmux: tmux,
mode: mode,
)
end
private getter tmux : Tmux do
Tmux.new(Fingers.config.tmux_version)
Tmux.new("3.3a")
end
private def should_notify?
!state.result.empty? && Fingers.config.show_copied_notification == "1"
end
end
end

View file

@ -1,8 +1,14 @@
require "./base"
require "cling"
module Fingers::Commands
class Version < Base
def run
class Version < Cling::Command
def setup : Nil
@name = "version"
@description = "Duh."
end
def run(arguments, options) : Nil
puts "#{Fingers::VERSION}"
end
end

View file

@ -1,84 +1,35 @@
require "json"
require "./options"
require "./options/*"
module Fingers
struct Config
class Config
include JSON::Serializable
property key : String
property keyboard_layout : String
property patterns : Array(String)
property alphabet : Array(String)
property benchmark_mode : String
property main_action : String
property ctrl_action : String
property alt_action : String
property shift_action : String
property hint_position : String
property hint_style : String
property selected_hint_style : String
property highlight_style : String
property selected_highlight_style : String
property backdrop_style : String
property tmux_version : String
define_fingers_config_struct_properties
FORMAT_PRINTER = TmuxStylePrinter.new
DEFAULT_PATTERNS = {
BUILTIN_PATTERNS = {
"ip": "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
"uuid": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"sha": "[0-9a-f]{7,128}",
"digit": "[0-9]{4,}",
"url": "((https?://|git@|git://|ssh://|ftp://|file:///)[^\\s()\"]+)",
"url": "((https?://|git@|git://|ssh://|ftp://|file:///)[^\\s()\"']+)",
"path": "(([.\\w\\-~\\$@]+)?(/[.\\w\\-@]+)+/?)",
"hex": "(0x[0-9a-fA-F]+)",
"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:]_#$%&+=/@-]+",
"git-status": "(modified|deleted|new file): +(?<match>.+)",
# Matches deployment-managed pod names like "nginx-deployment-66b6c48dd5-7xb2r".
# K8s generates hashes using a restricted alphabet (no vowels, no 0/1/3)
# to avoid accidental profanity: "bcdfghjklmnpqrstvwxz2456789"
# See: https://github.com/kubernetes/apimachinery/blob/master/pkg/util/rand/rand.go
"kubernetes-pod": "[a-z][a-z0-9-]*[a-z0-9]-[bcdfghjklmnpqrstvwxz2456789]{5,10}-[bcdfghjklmnpqrstvwxz2456789]{5}",
"git-status": "(modified|deleted|deleted by us|new file): +(?<match>.+)",
"git-status-branch": "Your branch is up to date with '(?<match>.*)'.",
"diff": "(---|\\+\\+\\+) [ab]/(?<match>.*)",
}
ALPHABET_MAP = {
"qwerty": "asdfqwerzxcvjklmiuopghtybn",
"qwerty-homerow": "asdfjklgh",
"qwerty-left-hand": "asdfqwerzcxv",
"qwerty-right-hand": "jkluiopmyhn",
"azerty": "qsdfazerwxcvjklmuiopghtybn",
"azerty-homerow": "qsdfjkmgh",
"azerty-left-hand": "qsdfazerwxcv",
"azerty-right-hand": "jklmuiophyn",
"qwertz": "asdfqweryxcvjkluiopmghtzbn",
"qwertz-homerow": "asdfghjkl",
"qwertz-left-hand": "asdfqweryxcv",
"qwertz-right-hand": "jkluiopmhzn",
"dvorak": "aoeuqjkxpyhtnsgcrlmwvzfidb",
"dvorak-homerow": "aoeuhtnsid",
"dvorak-left-hand": "aoeupqjkyix",
"dvorak-right-hand": "htnsgcrlmwvz",
"colemak": "arstqwfpzxcvneioluymdhgjbk",
"colemak-homerow": "arstneiodh",
"colemak-left-hand": "arstqwfpzxcv",
"colemak-right-hand": "neioluymjhk",
}
def initialize(
@key = "F",
@keyboard_layout = "qwerty",
@alphabet = [] of String,
@patterns = [] of String,
@main_action = ":copy:",
@ctrl_action = ":open:",
@alt_action = "",
@shift_action = ":paste:",
@hint_position = "left",
@hint_style = FORMAT_PRINTER.print("fg=green,bold"),
@highlight_style = FORMAT_PRINTER.print("fg=yellow"),
@selected_hint_style = FORMAT_PRINTER.print("fg=blue,bold"),
@selected_highlight_style = FORMAT_PRINTER.print("fg=blue"),
@backdrop_style = "",
@tmux_version = "",
@benchmark_mode = "0"
)
end
define_fingers_config_struct_initializer
def self.load_from_cache
Config.from_json(File.open(::Fingers::Dirs::CONFIG_PATH))
@ -91,6 +42,14 @@ module Fingers
def members : Array(String)
JSON.parse(to_json).as_h.keys
end
# computed properties
def alphabet
Fingers::ALPHABET_MAP[keyboard_layout].split("").reject do |char|
char.match(Fingers::DISALLOWED_CHARS)
end
end
end
def self.config

53
src/fingers/constants.cr Normal file
View file

@ -0,0 +1,53 @@
module Fingers
DISALLOWED_CHARS = /[cimqn]/
ALPHABET_MAP = {
"qwerty": "asdfqwerzxcvjklmiuopghtybn",
"qwerty-homerow": "asdfjklgh",
"qwerty-left-hand": "asdfqwerzcxv",
"qwerty-right-hand": "jkluiopmyhn",
"azerty": "qsdfazerwxcvjklmuiopghtybn",
"azerty-homerow": "qsdfjkmgh",
"azerty-left-hand": "qsdfazerwxcv",
"azerty-right-hand": "jklmuiophyn",
"qwertz": "asdfqweryxcvjkluiopmghtzbn",
"qwertz-homerow": "asdfghjkl",
"qwertz-left-hand": "asdfqweryxcv",
"qwertz-right-hand": "jkluiopmhzn",
"dvorak": "aoeuqjkxpyhtnsgcrlmwvzfidb",
"dvorak-homerow": "aoeuhtnsid",
"dvorak-left-hand": "aoeupqjkyix",
"dvorak-right-hand": "htnsgcrlmwvz",
"colemak": "arstqwfpzxcvneioluymdhgjbk",
"colemak-homerow": "arstneiodh",
"colemak-left-hand": "arstqwfpzxcv",
"colemak-right-hand": "neioluymjhk",
}
BUILTIN_PATTERNS = {
"ip": "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
"uuid": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"sha": "[0-9a-f]{7,128}",
"digit": "[0-9]{4,}",
"url": "((https?://|git@|git://|ssh://|ftp://|file:///)[^\\s()\"']+)",
"path": "(([.\\w\\-~\\$@]+)?(/[.\\w\\-@]+)+/?)",
"hex": "(0x[0-9a-fA-F]+)",
"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:]_#$%&+=/@-]+",
# Matches deployment-managed pod names like "nginx-deployment-66b6c48dd5-7xb2r".
# K8s generates hashes using a restricted alphabet (no vowels, no 0/1/3)
# to avoid accidental profanity: "bcdfghjklmnpqrstvwxz2456789"
# See: https://github.com/kubernetes/apimachinery/blob/master/pkg/util/rand/rand.go
"kubernetes-pod": "[a-z][a-z0-9-]*[a-z0-9]-[bcdfghjklmnpqrstvwxz2456789]{5,10}-[bcdfghjklmnpqrstvwxz2456789]{5}",
"git-status": "(modified|deleted|deleted by us|new file): +(?<match>.+)",
"git-status-branch": "Your branch is up to date with '(?<match>.*)'.",
"diff": "(---|\\+\\+\\+) [ab]/(?<match>.*)",
}
COPY_ACTION = ":copy:"
PASTE_ACTION = ":paste:"
OPEN_ACTION = ":open:"
ACTIONS = [COPY_ACTION, PASTE_ACTION, OPEN_ACTION]
ANSI_RESET = "\e[0m"
end

View file

@ -1,17 +1,34 @@
require "file_utils"
require "xdg_base_directory"
# TODO maybe use some xgd shite here?
def running_in_specs? : Bool
{{ @type.has_constant?("Spec") }}
end
module Fingers::Dirs
TMUX_PID = (ENV["TMUX"] || ",0000").split(",")[1]
# When running in specs use current PID, to avoid using existing cached data
TMUX_PID = running_in_specs? ? Process.pid : (ENV.fetch("TMUX", ",0000")).split(",")[1]
XDG = XdgBaseDirectory.app_directories("tmux-fingers")
ROOT = Path["/tmp"] / "tmux-#{TMUX_PID}"
TMP = Path[File.dirname(File.tempname)]
LOG_PATH = ROOT / "fingers.log"
CACHE = ROOT / "tmux-fingers"
ROOT = Path[XDG.state.file_path("tmux-#{TMUX_PID}")]
{% if env("FINGERS_LOG_PATH") %}
# used in development to read logs outside container more easily
LOG_PATH = {{ env("FINGERS_LOG_PATH") }}
{% else %}
LOG_PATH = Path[XDG.state.file_path("fingers.log")]
{% end %}
CACHE = ROOT
CONFIG_PATH = CACHE / "config.json"
SOCKET_PATH = CACHE / "fingers.sock"
# ensure cache folder
FileUtils.mkdir_p(Fingers::Dirs::CACHE) unless File.exists?(Fingers::Dirs::CACHE)
def self.ensure_folders!
Fingers::Dirs::XDG.state.mkdir unless Fingers::Dirs::XDG.state.exists?
FileUtils.mkdir_p(Fingers::Dirs::ROOT) unless File.exists?(Fingers::Dirs::ROOT)
end
Fingers::Dirs.ensure_folders!
end

View file

@ -4,6 +4,15 @@ require "./match_formatter"
require "./types"
module Fingers
struct Target
property text : String
property hint : String
property offset : Tuple(Int32, Int32)
def initialize(@text, @hint, @offset)
end
end
class Hinter
@formatter : Formatter
@patterns : Array(String)
@ -11,6 +20,7 @@ module Fingers
@pattern : Regex | Nil
@hints : Array(String) | Nil
@n_matches : Int32 | Nil
@reuse_hints : Bool
def initialize(
input : Array(String),
@ -20,42 +30,39 @@ module Fingers
patterns = Fingers.config.patterns,
alphabet = Fingers.config.alphabet,
huffman = Huffman.new,
formatter = ::Fingers::MatchFormatter.new
formatter = ::Fingers::MatchFormatter.new,
reuse_hints = false
)
@lines = input
@width = width
@hints_by_text = {} of String => String
@lookup_table = {} of String => String
@target_by_hint = {} of String => Target
@target_by_text = {} of String => Target
@state = state
@output = output
@formatter = formatter
@huffman = huffman
@patterns = patterns
@alphabet = alphabet
@reuse_hints = reuse_hints
end
def run
lines[0..-2].each { |line| process_line(line, "\n") }
process_line(lines[-1], "")
regenerate_hints!
lines[0..-2].each_with_index { |line, index| process_line(line, index, "\n") }
process_line(lines[-1], lines.size - 1, "")
# STDOUT.flush
output.flush
build_lookup_table!
end
def lookup(hint)
lookup_table.fetch(hint) { nil }
end
def matches
@matches ||= @hints_by_text.keys.uniq!.flatten
def lookup(hint) : Target | Nil
target_by_hint.fetch(hint) { nil }
end
# private
private getter :hints,
:hints_by_text,
:offsets_by_hint,
:input,
:lookup_table,
:width,
@ -64,17 +71,21 @@ module Fingers
:huffman,
:output,
:patterns,
:alphabet
:alphabet,
:reuse_hints,
:target_by_hint,
:target_by_text
def build_lookup_table!
@lookup_table = hints_by_text.invert
end
def process_line(line, line_index, ending)
tab_positions = tab_positions_for(line)
result = line.gsub(pattern) { |_m| replace($~, line_index) }
initial_length = result.size
result = expand_tabs(result, tab_positions)
tab_correction = result.size - initial_length
def process_line(line, ending)
result = line.gsub(pattern) { |_m| replace($~) }
result = Fingers.config.backdrop_style + result
double_width_correction = ((line.bytesize - line.size) / 3).round.to_i
padding_amount = (width - line.size - double_width_correction)
padding_amount = (width - line.size - double_width_correction - tab_correction)
padding = padding_amount > 0 ? " " * padding_amount : ""
output.print(result + padding + ending)
end
@ -86,40 +97,93 @@ module Fingers
def hints : Array(String)
return @hints.as(Array(String)) if !@hints.nil?
@hints = huffman.generate_hints(alphabet: alphabet, n: n_matches)
regenerate_hints!
@hints.as(Array(String))
end
def replace(match)
def regenerate_hints!
@hints = huffman.generate_hints(alphabet: alphabet.clone, n: n_matches)
@target_by_hint.clear
@target_by_text.clear
end
def replace(match, line_index)
text = match[0]
captured_text = match["match"]? || text
captured_text = captured_text_for_match(match)
relative_capture_offset = relative_capture_offset_for_match(match, captured_text)
if match["match"]?
match_start, match_end = {match.begin(0), match.end(0)}
capture_start, capture_end = find_capture_offset(match).not_nil!
capture_offset = {capture_start - match_start, captured_text.size}
else
capture_offset = nil
absolute_offset = {
line_index,
match.begin(0) + (relative_capture_offset ? relative_capture_offset[0] : 0)
}
hint = hint_for_text(captured_text)
# hint is longer than highlighted text, put it back in hint stack
if hint.size > captured_text.size
hints.push(hint)
return text
end
if hints_by_text.has_key?(captured_text)
hint = hints_by_text[captured_text]
else
hint = hints.pop
build_target(captured_text, hint, absolute_offset)
raise "Too many matches" if hint.nil?
hints_by_text[captured_text] = hint
if !state.input.empty? && !hint.starts_with?(state.input)
return text
end
formatter.format(
hint: hint,
highlight: text,
selected: state.selected_hints.includes?(hint),
offset: capture_offset
offset: relative_capture_offset
)
end
def captured_text_for_match(match)
match["match"]? || match[0]
end
def hint_for_text(text)
return pop_hint! unless reuse_hints
target = target_by_text[text]?
if target.nil?
return pop_hint!
end
target.hint
end
def pop_hint! : String
hint = hints.pop?
if hint.nil?
raise "Too many matches"
end
hint
end
def relative_capture_offset_for_match(match, captured_text)
return nil unless match["match"]?
match_start, match_end = {match.begin(0), match.end(0)}
capture_start, capture_end = find_capture_offset(match).not_nil!
{capture_start - match_start, captured_text.size}
end
def build_target(text, hint, offset)
target = Target.new(text, hint, offset)
target_by_hint[hint] = target
target_by_text[text] = target
target
end
def find_capture_offset(match : Regex::MatchData) : Tuple(Int32, Int32) | Nil
index = capture_indices.find { |i| match[i]? }
@ -135,11 +199,19 @@ module Fingers
def n_matches : Int32
return @n_matches.as(Int32) if !@n_matches.nil?
if reuse_hints
@n_matches = count_unique_matches
else
@n_matches = count_matches
end
end
def count_unique_matches
match_set = Set(String).new
lines.each do |line|
line.scan(pattern) do |match|
match_set.add(match[0]?.not_nil!)
match_set.add(captured_text_for_match(match))
end
end
@ -148,6 +220,44 @@ module Fingers
match_set.size
end
def count_matches
result = 0
lines.each do |line|
line.scan(pattern) do |match|
result += 1
end
end
result
end
def tab_positions_for(line)
positions = [] of Int32
offset = 0
loop do
index = line.index("\t", offset)
break unless index
positions << index
offset = index + 1
end
positions
end
def expand_tabs(line, tab_positions)
correction = 0
line.gsub(/\t/) do |_|
tab_position = tab_positions.shift?
next "\t" unless tab_position
spaces = 8 - ((tab_position + correction) % 8)
correction += spaces - 1
" " * spaces
end
end
private property lines : Array(String)
end
end

View file

@ -10,8 +10,7 @@ module Fingers
selected_highlight_style : String = Fingers.config.selected_highlight_style,
backdrop_style : String = Fingers.config.backdrop_style,
hint_position : String = Fingers.config.hint_position,
# TODO #perf remove this shell call
reset_sequence : String = `tput sgr0`.chomp
reset_sequence : String = Fingers::ANSI_RESET
)
@hint_style = hint_style
@highlight_style = highlight_style
@ -31,7 +30,7 @@ module Fingers
private getter :hint_style, :highlight_style, :selected_hint_style, :selected_highlight_style, :hint_position, :reset_sequence, :backdrop_style
private def before_offset(offset, highlight)
return "" if offset.nil?
return "" if offset.nil? || offset[0] == 0
start, _ = offset
backdrop_style + highlight[0..(start - 1)]
end
@ -55,9 +54,9 @@ module Fingers
highlight_pair = (selected ? selected_highlight_style : highlight_style) + chopped_highlight
if hint_position == "right"
highlight_pair + reset_sequence + hint_pair + reset_sequence
reset_sequence + highlight_pair + reset_sequence + hint_pair + reset_sequence
else
hint_pair + reset_sequence + highlight_pair + reset_sequence
reset_sequence + hint_pair + reset_sequence + highlight_pair + reset_sequence
end
end

35
src/fingers/options.cr Normal file
View file

@ -0,0 +1,35 @@
require "./options/*"
require "./options/parsers/*"
require "../tmux_style_printer"
define_key_option :key, "F"
define_key_option :jump_key, "J"
define_bool_option :use_system_clipboard, true
define_bool_option :enable_bindings, true
define_bool_option :benchmark_mode, false
define_bool_option :show_copied_notification, true
define_bool_option :skip_wizard, false
define_enum_option :hint_position, %w(left right), "left"
define_enum_option :keyboard_layout, Fingers::ALPHABET_MAP.keys, "qwerty"
define_multi_enum_option :enabled_builtin_patterns, ["all", *Fingers::BUILTIN_PATTERNS.keys], "all"
define_action_option :main, ":copy:"
define_action_option :ctrl, ":open:"
define_action_option :alt, ""
define_action_option :shift, ":paste:"
define_style_option :hint, Tmux.style_printer.print("fg=green,bold")
define_style_option :highlight, Tmux.style_printer.print("fg=yellow")
define_style_option :selected_hint, Tmux.style_printer.print("fg=blue,bold")
define_style_option :selected_highlight, Tmux.style_printer.print("fg=blue")
define_style_option :backdrop, ""
define_string_option :tmux_version, ""
define_fingers_macros
module Fingers::Options
define_fingers_options_module_helpers
end

View file

@ -0,0 +1,30 @@
module Fingers::Options
class Base
# Type and DEFAULT are defined in subclasses
macro inherited
def write(value : Type, config : Fingers::Config)
config.{{ @type.name.split("::").last.underscore.id }} = process(value)
end
end
def self.as_option_name
self.name.split("::").last.underscore.downcase
end
def parse(raw_value : String, option_name : String)
raw_value
end
def valid?(raw_value : String) : Tuple(Bool, String)
{ true, "ok" }
end
def process(value)
value
end
def ==(other : String)
self.class.as_option_name == other
end
end
end

View file

@ -0,0 +1,130 @@
macro define_fingers_macros
{% option_names = Fingers::Options::Base.all_subclasses.map { |klass| klass.name.split("::").last } %}
macro define_fingers_config_struct_initializer
{% begin %}
def initialize(
{% for option_name in option_names %}
@{{ option_name.underscore.id }} = ::Fingers::Options::{{ option_name.id }}::DEFAULT,
{% end %}
)
end
{% end %}
end
macro define_fingers_config_struct_properties
{% for option_name in option_names %}
property {{ option_name.underscore.id }} : Fingers::Options::{{ option_name.id }}::Type
{% end %}
end
macro define_fingers_options_module_helpers
{% for option_name in option_names %}
def self.{{option_name.underscore.id}}
::Fingers::Options::{{option_name.id}}.new
end
{% end %}
def self.parse(option_name : String, option_value : String, config : Fingers::Config)
{% begin %}
case option_name
{% for option_name in option_names %}
when option = ::Fingers::Options.{{option_name.underscore.id}}
parsed_value = option.parse(option_value, option_name)
option.write(parsed_value, config)
{% end %}
end
{% end %}
end
def self.valid?(option_name : String, option_value : String)
{% begin %}
case option_name
{% for option_name in option_names %}
when option = ::Fingers::Options.{{option_name.underscore.id}}
option.valid?(option_value)
{% end %}
else
{ false, "Unknown option: #{option_name}" }
end
{% end %}
end
end
end
macro define_bool_option(name, default)
module Fingers::Options
class {{name.camelcase.id}} < Base
include ::Fingers::Options::Parsers::BoolParser
DEFAULT = {{ default }}
alias Type = Bool
end
end
end
macro define_enum_option(name, possible_values, default)
module Fingers::Options
class {{name.camelcase.id}} < Base
include ::Fingers::Options::Parsers::EnumParser
DEFAULT = {{ default }}
alias Type = String
def possible_values
{{ possible_values }}
end
end
end
end
macro define_multi_enum_option(name, possible_values, default)
module Fingers::Options
class {{name.camelcase.id}} < Base
include ::Fingers::Options::Parsers::MultiEnumParser
DEFAULT = {{ default }}
alias Type = String
def possible_values
{{ possible_values }}
end
end
end
end
macro define_key_option(name, default)
module Fingers::Options
class {{name.camelcase.id}} < Base
DEFAULT = {{ default }}
alias Type = String
end
end
end
macro define_string_option(name, default)
module Fingers::Options
class {{name.camelcase.id}} < Base
DEFAULT = {{ default }}
alias Type = String
end
end
end
macro define_action_option(name, default)
module Fingers::Options
class {{"#{name}_action".gsub(/^:/, "").camelcase.id}} < Base
include ::Fingers::Options::Parsers::ActionParser
DEFAULT = {{ default }}
alias Type = String
end
end
end
macro define_style_option(name, default)
module Fingers::Options
class {{"#{name}_style".gsub(/^:/, "").camelcase.id}} < Base
include ::Fingers::Options::Parsers::StyleParser
DEFAULT = {{ default }}
alias Type = String
end
end
end

View file

@ -0,0 +1,18 @@
module Fingers::Options::Parsers::ActionParser
def valid?(value : String) : Tuple(Bool, String)
if value.starts_with?(":") && value.ends_with?(":")
validate_action(value)
else
# allow arbirtary shell commands
{ true, "ok" }
end
end
def validate_action(value)
if Fingers::ACTIONS.includes?(value)
{ true, "ok" }
else
{ false, "Invalid action '#{value}'. Possible actions are: #{Fingers::ACTIONS.join(", ")}" }
end
end
end

View file

@ -0,0 +1,5 @@
module Fingers::Options::Parsers::BoolParser
def parse(value : String, _option_name : String) : Bool
value == "1" || value.downcase == "true"
end
end

View file

@ -0,0 +1,18 @@
module Fingers::Options::Parsers::EnumParser
def valid?(value : String) : Tuple(Bool, String)
if possible_values_as_strings.includes?(value.to_s)
{ true, "ok" }
else
{ false, "Invalid value '#{value}'. Possible values are: #{possible_values.join(", ")}" }
end
end
def possible_values
[] of String
end
def possible_values_as_strings
possible_values.map { |value| value.to_s }
end
end

View file

@ -0,0 +1,19 @@
module Fingers::Options::Parsers::MultiEnumParser
def valid?(value : String) : Tuple(Bool, String)
invalid = value.split(",").reject { |name| possible_values_as_strings.includes?(name) }
if invalid.empty?
{ true, "ok" }
else
{ false, "Invalid value(s) '#{invalid.join(",")}'. Possible values are: #{possible_values.join(", ")}" }
end
end
def possible_values
[] of String
end
def possible_values_as_strings
possible_values.map { |value| value.to_s }
end
end

View file

@ -0,0 +1,17 @@
module Fingers::Options::Parsers::StyleParser
def valid?(value : String) : Tuple(Bool, String)
return { true, "ok" } if value.empty?
begin
Tmux.style_printer.print(value)
{ true, "ok" }
rescue e: ::TmuxStylePrinter::InvalidFormat
{ false, "Invalid style format '#{e}'" }
end
end
def process(value : String) : String
return "" if value.empty?
Tmux.style_printer.print(value)
end
end

View file

@ -0,0 +1,34 @@
require "./base"
module Fingers::Options
class Patterns < Base
alias Type = Hash(String, String)
DEFAULT = {} of String => String
def parse(raw_value : String, option_name : String)
val = {} of String => String
val[option_name.gsub(/^pattern[-_]/, "")] = raw_value
val
end
def valid?(raw_value : String) : Tuple(Bool, String)
pattern = raw_value
begin
Regex.new(pattern)
{ true, "ok" }
rescue e: ArgumentError
{ false, e.message || "Could not parse pattern" }
end
end
def ==(other : String)
other.match(/^pattern/)
end
def write(value : Type, config : Fingers::Config)
config.patterns.merge!(value)
end
end
end

View file

@ -13,20 +13,28 @@ module Fingers
@output : Printer
@original_pane : Tmux::Pane
@tmux : Tmux
@mode : String
def initialize(
@hinter,
@output,
@original_pane,
@state,
@tmux
@tmux,
@mode,
)
end
def render
clear_screen
hide_cursor
hinter.run
begin
hinter.run
rescue e
Log.fatal { e }
request_exit!
end
end
def process_input(input : String)
@ -46,17 +54,6 @@ module Fingers
end
end
def run_action
ActionRunner.new(
hint: state.input,
modifier: state.modifier,
match: state.result,
original_pane: original_pane
).run
tmux.display_message("Copied: #{state.result}", 1000) unless state.result.empty?
end
private def hide_cursor
output.print HIDE_CURSOR_SEQ
end
@ -68,13 +65,19 @@ module Fingers
private def process_hint(char, modifier)
state.input += char
state.modifier = modifier
match = hinter.lookup(state.input)
handle_match(match) if match
tmux.display_message(state.input, 300)
if match.nil?
render
else
handle_match(match.not_nil!.text)
end
end
private def process_multimode
return if mode == "jump"
prev_state = state.multi_mode
state.multi_mode = !state.multi_mode
current_state = state.multi_mode
@ -85,7 +88,7 @@ module Fingers
end
end
private getter :output, :hinter, :original_pane, :state, :tmux
private getter :output, :hinter, :original_pane, :state, :tmux, :mode
private def handle_match(match)
if state.multi_mode

View file

@ -1,6 +1,6 @@
require "./priority_queue"
class HuffmanNode
struct HuffmanNode
def initialize(weight : Int32, children : Array(HuffmanNode))
@weight = weight
@children = children

View file

@ -35,9 +35,6 @@ def build_tmux_format(hash)
"{#{fields.join(",")}}"
end
# TODO maybe use system everywhere?
# rubocop:disable Metrics/ClassLength
class Tmux
class Shell
def initialize
@ -111,6 +108,10 @@ class Tmux
@panes : Array(Pane) | Nil
@version : SemanticVersion
def self.style_printer
@@style_printer ||= TmuxStylePrinter.new
end
def self.tmux_version_to_semver(version_string)
match = version_string.match(/(?<major>[1-9]+[0-9]*)\.(?<minor>[0-9]+)(?<patch_letter>[a-z]+)?/)
@ -134,8 +135,20 @@ class Tmux
@version = Tmux.tmux_version_to_semver(version_string)
end
def panes : Array(Pane)
exec("list-panes -a -F '#{PANE_FORMAT}'").chomp.split("\n").map do |pane|
def list_panes(filters = "", target = nil) : Array(Pane)
args = ["list-panes", "-F", PANE_FORMAT]
if target.nil?
args << "-a"
else
args.concat(["-t", target])
end
if !filters.empty?
args.concat(["-f", filters])
end
exec(Process.quote(args)).chomp.split("\n").map do |pane|
Pane.from_json(pane)
end
end
@ -148,20 +161,20 @@ class Tmux
Pane.from_json(output)
end
def capture_pane(pane : Pane)
def capture_pane(pane : Pane, join = true)
if pane.pane_in_mode && !pane.scroll_position.nil?
scroll_position = pane.scroll_position.not_nil!
start_line = -scroll_position.to_i
end_line = pane.pane_height.to_i - scroll_position.to_i - 1
exec("capture-pane -J -p -t '#{pane.pane_id}' -S #{start_line} -E #{end_line}").chomp
exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}' -S #{start_line} -E #{end_line}").chomp
else
exec("capture-pane -J -p -t '#{pane.pane_id}'").chomp
exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}'").chomp
end
end
def create_window(name, cmd, _pane_width, _pane_height)
output = exec("new-window -P -d -n '#{name}' -F '#{WINDOW_FORMAT}' '#{cmd}'").chomp
output = exec("new-window -c '\#{pane_current_path}' -P -d -n '#{name}' -F '#{WINDOW_FORMAT}' '#{cmd}'").chomp
Window.from_json(output)
end
@ -209,17 +222,17 @@ class Tmux
end
def set_global_option(name, value)
exec(["set-option", "-g", name, value].join(' '))
exec(Process.quote(["set-option", "-g", name, value]))
end
def get_global_option(name)
exec(["show", "-gqv", name].join(' ')).chomp
end
def set_buffer(value)
def set_buffer(value, use_system_clipboard = true)
return unless value
if @version >= Tmux.tmux_version_to_semver("3.2")
if @version >= Tmux.tmux_version_to_semver("3.2") && use_system_clipboard
args = ["load-buffer", "-w", "-"]
else
args = ["load-buffer", "-"]
@ -235,7 +248,7 @@ class Tmux
)
cmd.input.print(value)
cmd.input.flush
cmd.input.close
cmd.wait
@ -243,7 +256,13 @@ class Tmux
end
def select_pane(id)
exec(["select-pane", "-t", id].join(' '))
args = ["select-pane", "-t", id]
if @version >= Tmux.tmux_version_to_semver("3.1")
args << "-Z"
end
exec(args.join(' '))
end
def zoom_pane(id)
@ -283,7 +302,7 @@ class Tmux
exec(Process.quote(["display-message", "-d", delay.to_s, msg]))
end
private def exec(cmd)
def exec(cmd)
@sh.exec("#{tmux} #{cmd}")
end
end

View file

@ -3,10 +3,6 @@ class TmuxStylePrinter
class InvalidFormat < Exception
end
abstract class Shell
abstract def exec(cmd)
end
STYLE_SEPARATOR = /[ ,]+/
COLOR_MAP = {
@ -20,32 +16,23 @@ class TmuxStylePrinter
white: 7,
}
LAYER_MAP = {
bg: "setab",
fg: "setaf",
LAYER_CODE = {
"bg" => 48,
"fg" => 38,
}
STYLE_MAP = {
bright: "bold",
bold: "bold",
dim: "dim",
underscore: "smul",
reverse: "rev",
italics: "sitm",
"bright" => "\e[1m",
"bold" => "\e[1m",
"dim" => "\e[2m",
"underscore" => "\e[4m",
"reverse" => "\e[7m",
"italics" => "\e[3m",
}
class ShellExec < Shell
def exec(cmd)
`#{cmd}`.chomp
end
end
@shell : Shell
@applied_styles : Hash(String, String)
@reset_sequence : String | Nil
def initialize(shell = ShellExec.new)
@shell = shell
def initialize
@applied_styles = {} of String => String
end
@ -58,7 +45,7 @@ class TmuxStylePrinter
output += parse_style_definition(style)
end
output += reset_sequence if reset_styles_after && !@applied_styles.empty?
output += Fingers::ANSI_RESET if reset_styles_after && !@applied_styles.empty?
output
end
@ -89,7 +76,7 @@ class TmuxStylePrinter
raise InvalidFormat.new("Invalid color definition: #{style}") if color_to_apply.nil?
result = shell.exec("tput #{LAYER_MAP[layer]} #{color_to_apply}")
result = "\e[#{LAYER_CODE[layer]};5;#{color_to_apply}m"
@applied_styles[layer] = result
@ -108,7 +95,7 @@ class TmuxStylePrinter
raise InvalidFormat.new("Invalid style definition: #{style}") if style_to_apply.nil?
result = style == "dim" ? "\033[2m" : shell.exec("tput #{STYLE_MAP[style]}")
result = style_to_apply
if should_remove_style
@applied_styles.delete(style)
@ -121,14 +108,6 @@ class TmuxStylePrinter
end
private def reset_to_applied_styles!
[reset_sequence, @applied_styles.values].join
end
private def reset_sequence
@reset_sequence ||= shell.exec("tput sgr0").chomp
end
private def shell
@shell
[Fingers::ANSI_RESET, @applied_styles.values].join
end
end

View file

@ -16,7 +16,7 @@ fi
CURRENT_FINGERS_VERSION="$($FINGERS_BINARY version)"
pushd $CURRENT_DIR &> /dev/null
CURRENT_GIT_VERSION=$(git describe --tags | sed "s/-.*//g")
CURRENT_GIT_VERSION=$(cat shard.yml | grep "^version" | cut -f2 -d':' | sed "s/ //g")
popd &> /dev/null
SKIP_WIZARD=$(tmux show-option -gqv @fingers-skip-wizard)
@ -31,5 +31,13 @@ if [ "$SKIP_WIZARD" = "0" ] && [ "$CURRENT_FINGERS_VERSION" != "$CURRENT_GIT_VER
fi
fi
tmux run "$FINGERS_BINARY load-config"
if [[ "$TERM" == "dumb" ]]; then
# force term value to get proper colors in systemd and tmux 3.6a
# https://github.com/Morantron/tmux-fingers/issues/143
FINGERS_TERM=$(tmux show-option -gqv default-terminal)
else
FINGERS_TERM="$TERM"
fi
tmux run "TERM=$FINGERS_TERM $FINGERS_BINARY load-config"
exit $?