m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 22:33:47 -05:00

Compare commits

...

97 Commits

Author SHA1 Message Date
Junegunn Choi
f97d275413 0.50.0 2024-04-15 00:02:27 +09:00
Junegunn Choi
3acb4ca90e Fix streaming filter mode by not running reader callback concurrently
Close #3728
2024-04-14 23:34:25 +09:00
Junegunn Choi
e86b81bbf5 Improve search performance by limiting the search scope
Find the last occurrence of the last character in the pattern and
perform the search algorithm only up to that point.

The effectiveness of this mechanism depends a lot on the shape of the
input and the pattern.
2024-04-14 11:48:44 +09:00
Junegunn Choi
a5447b8b75 Improve search performance by pre-calculating bonus matrix
This gives yet another 5% boost.
2024-04-14 11:47:06 +09:00
Junegunn Choi
7ce6452d83 Improve search performance by pre-calculating character classes
This simple optmization can give more than 15% performance boost
in some scenarios.
2024-04-14 11:47:05 +09:00
junegunn
5643a306bd Deploying to master from @ junegunn/fzf@3c877c504b 🚀 2024-04-14 00:03:45 +00:00
Charlie Vieth
3c877c504b Enable profiling options when 'pprof' tag is set (#2813)
This commit enables cpu, mem, block, and mutex profling of the FZF
executable. To support flushing the profiles at program exit it adds
util.AtExit to register "at exit" functions and mandates that util.Exit
is used instead of os.Exit to stop the program.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-04-13 14:58:11 +09:00
Junegunn Choi
892d1acccb Fix tcell build 2024-04-13 14:47:04 +09:00
Junegunn Choi
1a9c282f76 Fix unit tests 2024-04-13 14:40:43 +09:00
Junegunn Choi
fd1ba46f77 Export $FZF_KEY environment variable to child processes
It's the name of the last key pressed.

Related #3412
2024-04-13 14:00:16 +09:00
Junegunn Choi
a4745626dd Add jump and jump-cancel events
Close #3412

    # Default behavior
    fzf --bind space:jump

    # Same as jump-accept action
    fzf --bind space:jump,jump:accept

    # Accept on jump, abort on cancel
    fzf --bind space:jump,jump:accept,jump-cancel:abort

    # Change header on jump-cancel
    fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
2024-04-10 20:17:12 +09:00
dependabot[bot]
17bb7ad278 Bump golang.org/x/term from 0.18.0 to 0.19.0 (#3718)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/term/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 00:51:29 +09:00
Junegunn Choi
152988c17b [shell] Revert interactiveness checks for eval
So that there's no error even when the scripts are mistakenly evaluated
in non-interactive sessions.

  bash -c 'eval "$(fzf --bash)"; echo done'
  zsh -c 'eval "$(fzf --zsh)"; echo done'

* https://github.com/junegunn/fzf/pull/3675#issuecomment-2044860901
* f103aa4753
2024-04-10 00:46:09 +09:00
Junegunn Choi
4cd37fc02b Disable line wrapping during rendering
Prevent unwanted line wraps that break the layout when the actual
display width of a character is different than expected.
2024-04-09 00:26:25 +09:00
LangLangBart
69b9d674a3 chore: Add new option in issue checklist and modify requirements (#3715) 2024-04-07 10:25:12 +09:00
junegunn
bad8061547 Deploying to master from @ junegunn/fzf@62963dcefd 🚀 2024-04-07 00:01:37 +00:00
Junegunn Choi
62963dcefd 0.49.0 2024-04-05 00:20:26 +09:00
Junegunn Choi
68a35e4735 Do not trim CR on Windows when --read0 is set 2024-04-04 23:39:29 +09:00
Charlie Vieth
9b9ad77e1c mod: update changes/fastwalk to v1.0.3 (#3709)
Update charlievieth/fastwalk to resolve issue #3706.
2024-04-04 13:29:32 +09:00
Junegunn Choi
118b4d4a01 [bash] Add -o nospace to dir completion options (#1987) 2024-04-04 13:20:31 +09:00
Junegunn Choi
da14ab6f16 [bash] Remove -o default from dir completion options (#1987) 2024-04-04 12:58:52 +09:00
Junegunn Choi
09a4ca6ab5 [bash] Fix variable completion of directory-related commands
Fix #1987
2024-04-04 12:43:35 +09:00
Junegunn Choi
8a2df79711 Do not hide separator by default on --info=inline-right|hidden 2024-04-04 00:05:55 +09:00
Junegunn Choi
c30e486b64 Further performance improvements by removing unnecessary copies 2024-04-02 08:43:08 +09:00
Junegunn Choi
a575c0c54b GitHub Actions: Use Go "1.20" 2024-04-02 01:47:24 +09:00
Junegunn Choi
77fe96ac0d GitHub Actions: Use Go 1.20 2024-04-02 01:44:18 +09:00
Junegunn Choi
5234c3759a Improve ingestion performance (by around 40%)
Summary
    fzf --sync --bind load:accept < 27M-lines ran
      1.16 ± 0.01 times faster than fzf-41b3511 --sync --bind load:accept < 27M-lines
      1.44 ± 0.01 times faster than fzf-0.48.1 --sync --bind load:accept < 27M-lines
2024-04-02 01:38:12 +09:00
Junegunn Choi
41b3511ad9 Improve ingestion performance (by around 20%) 2024-04-01 23:38:46 +09:00
Junegunn Choi
128e4a2e8d [fish] Fix $dir in FZF_{CTRL_T,ALT_C}_COMMAND not evaluated
Fix #3705
2024-03-31 20:37:20 +09:00
junegunn
07ac90d798 Deploying to master from @ junegunn/fzf@7de87a9b2c 🚀 2024-03-31 00:01:48 +00:00
Emilio Vesprini
7de87a9b2c [shell] Make ALT-C use the absolute path to the selected directory (#3688)
Rationale: this way the resulting cd command that ends up in the shell
history can be reused to get to the same location regardless of
the current working directory.

Co-authored-by: LangLangBart <92653266+LangLangBart@users.noreply.github.com>
2024-03-31 01:13:15 +09:00
Junegunn Choi
dff865239a [bash-completion] Make dynamic loader return 124 to retry completion
Close #3702
2024-03-29 16:21:43 +09:00
Junegunn Choi
07f8f70c5b Fix flaky test case 2024-03-29 16:15:53 +09:00
Matthieu Cneude
f625c5aabe Add environment variables: FZF_{BORDER,PREVIEW}_LABEL (#3693)
The environment variable get the value of the preview label, even if it
has been updated with an action. It can be useful to track the label of
the preview and be able to switch between previews using only one
binding.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-03-29 16:14:08 +09:00
Junegunn Choi
8a74976c1f Add track-current, untrack-current, and toggle-track-current (#3699)
Close #3691
2024-03-28 20:42:01 +09:00
Junegunn Choi
b6bfd4a5cb Fix typo in comment 2024-03-27 17:36:08 +09:00
Junegunn Choi
008fb9d258 Fix reload and reload-sync behaviors
https://github.com/junegunn/fzf/discussions/3696#discussioncomment-8915593
2024-03-27 17:25:56 +09:00
Junegunn Choi
db6db49ed6 Increase the buffer size for POST requests
Close #3685
2024-03-21 19:18:43 +09:00
Junegunn Choi
05453881c3 Set a 2-second timeout for POST requests
Close #3685
2024-03-21 19:18:38 +09:00
Junegunn Choi
5e47ab9431 README: Mention that you can source individual script files 2024-03-21 12:35:52 +09:00
LangLangBart
ec70acd0b9 chore: transition from markdown to YAML for issue template (#3687) 2024-03-21 08:33:28 +09:00
zeertzjq
25e61056b6 [fish] Fix Ctrl-T and Alt-C not using last token as search root (#3684) 2024-03-19 14:44:42 +09:00
Junegunn Choi
d579e335b5 0.48.1 2024-03-17 16:35:35 +09:00
Junegunn Choi
7e344ceb85 Update README 2024-03-17 16:21:07 +09:00
Junegunn Choi
0145b82ea0 Update README 2024-03-17 16:20:14 +09:00
Junegunn Choi
b4efe7aab7 Show how to disable a key binding 2024-03-17 16:18:19 +09:00
Junegunn Choi
9ffe951f6d Update Makefile target dependencies
Because shell integration scripts are now embedded in the binary
2024-03-17 16:11:21 +09:00
Brayden Hill
a5ea4f57bd Updated link for highlight command (#3680) 2024-03-17 16:09:39 +09:00
Eli Barzilay
88f4c16755 Make it possible to disable Ctrl+T / Alt+C / completions (#3678)
This makes it possible to skip one of the above key bindings or
completions by setting a variable to an empty string. For example,

    FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= \
      eval "$(fzf --zsh)"

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-03-17 16:06:48 +09:00
Junegunn Choi
c7ee071efa Fix panic caused by invalid cursor index
Fix #3681
2024-03-17 15:55:16 +09:00
Junegunn Choi
0740ef7ceb [bash] Fix default completion of unset, unalias, etc
Fix #3679
2024-03-17 15:38:11 +09:00
junegunn
b29bd809ac Deploying to master from @ junegunn/fzf@8977c9257a 🚀 2024-03-17 00:01:33 +00:00
Junegunn Choi
8977c9257a Limit the maximum number of focus events to process at once 2024-03-14 11:18:47 +09:00
Junegunn Choi
091b7eacba 0.48.0 2024-03-14 00:02:53 +09:00
Junegunn Choi
e74b1251c0 Embed shell integration scripts in fzf binary (--bash / --zsh / --fish) (#3675)
This simplifies the distribution, and the users are less likely to have
problems caused by using incompatible scripts and binaries.

    # Set up fzf key bindings and fuzzy completion
    eval "$(fzf --bash)"

    # Set up fzf key bindings and fuzzy completion
    eval "$(fzf --zsh)"

    # Set up fzf key bindings
    fzf --fish | source
2024-03-13 23:59:34 +09:00
Junegunn Choi
d282a1649d Add walker options and replace 'find' with the built-in walker (#3649) 2024-03-13 20:56:31 +09:00
Junegunn Choi
6ce8d49d1b [bash] Fix regression in dynamic completion
Fix #3674
2024-03-13 08:31:31 +09:00
dependabot[bot]
c5b197078a Bump golang.org/x/term from 0.17.0 to 0.18.0 (#3670)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/term/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 08:11:25 +09:00
Junegunn Choi
0494f20d62 Revert "Fix CHANGELOG"
This reverts commit 73aff476dd.
2024-03-10 23:24:59 +09:00
Junegunn Choi
73aff476dd Fix CHANGELOG 2024-03-10 21:52:39 +09:00
Junegunn Choi
98ee5e651a 0.47.0 2024-03-10 21:43:41 +09:00
Koichi Murase
01871ea383 [bash] Update orig_complete after _completion_loader 2024-03-10 21:41:42 +09:00
Koichi Murase
1dbdb9438f [bash] Refactor access to "_fzf_orig_complete_${cmd//[^A-Za-z0-9_]/_}"
In the current codebase, for the original completion settings, the
pieces of the codes to determine the variable name and to access the
stored data are scattered.  In this patch, we define functions to
access these variables.  Those functions will be used in a coming
patch.

* This patch also resolves an inconsistent escaping of "$cmd": $cmd is
  escaped as ${...//[^A-Za-z0-9_]/_} in some places, but it is escaped
  as ${...//[^A-Za-z0-9_=]/_} in some other places.  The latter leaves
  the character "=" in the command name, which causes an issue because
  "=" cannot be a part of a variable name.  For example, the following
  test case produces an error message:

  $ COMP_WORDBREAKS=${COMP_WORDBREAKS//=}
  $ _test1() { COMPREPLY=(); }
  $ complete -vF _test1 cmd.v=1.0
  $ _fzf_setup_completion path cmd.v=1.0
  $ cmd.v=1.0 [TAB]
  bash: _fzf_orig_completion_cmd_v=1_0: invalid variable name

  The behavior of leaving "=" was present from the beginning when
  saving the original completion is introduced in commit 91401514, and
  this does not seem to be a specific reasoning.  In this patch, we
  replace "=" as well as the other non-identifier characters.

* Note: In this patch, the variable REPLY is used to return values
  from functions.  This design is to make it useful with the value
  substitutions, a new Bash feature of the next release 5.3, which is
  taken from mksh.
2024-03-10 21:41:42 +09:00
junegunn
c70f0eadb8 Deploying to master from @ junegunn/fzf@26244ad8c2 🚀 2024-03-10 00:01:32 +00:00
Junegunn Choi
26244ad8c2 Fix preview area not being cleared when using certain types of border styles
fzf --preview 'sleep 3; date' --preview-window hidden \
      --bind ctrl-/:change-preview-window:up,border-bottom
2024-03-09 14:14:42 +09:00
Junegunn Choi
fa0aa5510d Kill preview process when hiding the preview window
via toggle-preview, hide-preview, or change-preview-window
2024-03-08 22:01:45 +09:00
Junegunn Choi
eec557b6aa Fix invalid memory access when the preview window becomes hidden 2024-03-08 17:57:09 +09:00
huajin tong
0cc27c3cc1 Fix typo (#3661) 2024-03-07 01:35:31 +09:00
dependabot[bot]
507089d7b2 Bump actions/checkout from 3 to 4 (#3428)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 11:48:46 +09:00
dependabot[bot]
a6b3517b75 Bump github.com/gdamore/tcell/v2 from 2.7.1 to 2.7.4 (#3658)
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.7.1 to 2.7.4.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.7.1...v2.7.4)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 11:04:38 +09:00
dependabot[bot]
2d6beb7813 Bump crate-ci/typos from 1.18.2 to 1.19.0 (#3657)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.18.2 to 1.19.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 11:04:17 +09:00
onee-only
61bc129e1d Update parseGetParams to call strconv.Atoi when params are valid 2024-03-05 11:03:56 +09:00
onee-only
52210a57f0 Update error return position according to convention 2024-03-05 11:03:56 +09:00
onee-only
8061a2f108 Remove duplicate code 2024-03-05 11:03:56 +09:00
junegunn
7444eff6d4 Deploying to master from @ junegunn/fzf@f35a9da99a 🚀 2024-03-03 00:01:39 +00:00
dependabot[bot]
f35a9da99a Bump crate-ci/typos from 1.17.2 to 1.18.2 (#3624)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.17.2 to 1.18.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.17.2...v1.18.2)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 18:40:13 +09:00
dependabot[bot]
c3098e9ab2 Bump github.com/mattn/go-isatty from 0.0.17 to 0.0.20 (#3489)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.17 to 0.0.20.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.17...v0.0.20)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 18:39:37 +09:00
Junegunn Choi
686f9288fc Allow iTerm2 image data that ends with 'ESC \' (#3646) 2024-03-02 18:24:54 +09:00
Junegunn Choi
1833670fb9 Add $FZF_DEFAULT_OPTS_FILE (#3618)
For those who prefer to manage default options in a file.
If the file is not found, fzf will exit with an error.

We're not setting a default value for it because:

1. it's hard to find a default value that can be universally agreed upon
2. to avoid fzf having to check for the existence of the file even when it's not used
2024-02-29 09:49:33 +09:00
junegunn
3dd42f5aa2 Deploying to master from @ junegunn/fzf@99a7beba57 🚀 2024-02-25 00:01:35 +00:00
Junegunn Choi
99a7beba57 Fix missing bonus score on a delimiter character
Fix #3645
2024-02-22 23:19:11 +09:00
Junegunn Choi
edee2b753c fzf-tmux: Workaround for tmux 3.4 bug
Close #3635

https://github.com/tmux/tmux/pull/3840
2024-02-21 14:39:03 +09:00
dependabot[bot]
545d5770be Bump github.com/gdamore/tcell/v2 from 2.7.0 to 2.7.1 (#3639)
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.7.0 to 2.7.1.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.7.0...v2.7.1)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 10:35:23 +09:00
Junegunn Choi
ca747a2b54 Fix unit tests 2024-02-19 12:39:04 +09:00
Junegunn Choi
17da165cfe CHANGELOG: charlievieth/fastwalk 2024-02-19 12:36:14 +09:00
Junegunn Choi
5e6788c679 Export FZF_* variables to 'reload' process as well 2024-02-19 12:36:14 +09:00
Charlie Vieth
425deadca9 dep: update github.com/charlievieth/fastwalk to v1.0.2 (#3631)
This fixes the build for solaris/illumos and removes the extraneous
godirwalk dependency.
2024-02-18 13:20:50 +09:00
junegunn
2c8e9dd3a5 Deploying to master from @ junegunn/fzf@7a72f1a253 🚀 2024-02-18 00:01:35 +00:00
Junegunn Choi
7a72f1a253 Code cleanup: Remove unused argument 2024-02-15 17:11:30 +09:00
Junegunn Choi
208e556332 Replace "default find command" with built-in directory traversal 2024-02-15 16:55:43 +09:00
Junegunn Choi
c65d11bfb5 Update README: warp.dev 2024-02-15 14:30:44 +09:00
Junegunn Choi
3b5b52d89a Update README: warp.dev 2024-02-13 08:45:33 +09:00
dependabot[bot]
a4f6c8f990 Bump github.com/rivo/uniseg from 0.4.6 to 0.4.7 (#3623)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.4.6 to 0.4.7.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.4.6...v0.4.7)

---
updated-dependencies:
- dependency-name: github.com/rivo/uniseg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 08:53:03 +09:00
dependabot[bot]
670c329852 Bump golang.org/x/term from 0.16.0 to 0.17.0 (#3622)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/term/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 08:52:31 +09:00
dependabot[bot]
f3551c8422 Bump golang.org/x/sys from 0.16.0 to 0.17.0 (#3621)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/sys/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 08:50:49 +09:00
Konstantin Podsvirov
90b8187882 Add info about MSYS2 distro to README.md (#3610) 2024-02-04 19:02:32 +09:00
junegunn
1a43259989 Deploying to master from @ junegunn/fzf@3c0a630475 🚀 2024-02-04 00:01:39 +00:00
59 changed files with 2312 additions and 852 deletions

View File

@@ -1,22 +0,0 @@
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
<!-- Check all that apply [x] -->
- [ ] I have read through the manual page (`man fzf`)
- [ ] I have the latest version of fzf
- [ ] I have searched through the existing issues
## Info
- OS
- [ ] Linux
- [ ] Mac OS X
- [ ] Windows
- [ ] Etc.
- Shell
- [ ] bash
- [ ] zsh
- [ ] fish
## Problem / Steps to reproduce

View File

@@ -0,0 +1,49 @@
---
name: Issue Template
description: Report a problem or bug related to fzf to help us improve
body:
- type: markdown
attributes:
value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have read through the manual page (`man fzf`)
required: true
- label: I have searched through the existing issues
required: true
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
required: false
- type: input
attributes:
label: Output of `fzf --version`
placeholder: e.g. 0.48.1 (d579e33)
validations:
required: true
- type: checkboxes
attributes:
label: OS
options:
- label: Linux
- label: macOS
- label: Windows
- label: Etc.
- type: checkboxes
attributes:
label: Shell
options:
- label: bash
- label: zsh
- label: fish
- type: textarea
attributes:
label: Problem / Steps to reproduce
validations:
required: true

View File

@@ -27,7 +27,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v4 uses: actions/dependency-review-action@v4

View File

@@ -18,14 +18,14 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.19 go-version: "1.20"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1

View File

@@ -15,14 +15,14 @@ jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: "1.20"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Generate Sponsors 💖 - name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1 uses: JamesIves/github-sponsors-readme-action@v1

View File

@@ -6,5 +6,5 @@ jobs:
name: Spell Check with Typos name: Spell Check with Typos
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: crate-ci/typos@v1.17.2 - uses: crate-ci/typos@v1.19.0

View File

@@ -20,10 +20,6 @@ builds:
cat > /tmp/fzf-gon-amd64.hcl << EOF cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"] source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }
@@ -48,10 +44,6 @@ builds:
cat > /tmp/fzf-gon-arm64.hcl << EOF cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"] source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }

View File

@@ -552,7 +552,7 @@ pods() {
- Press enter key on a pod to `kubectl exec` into it - Press enter key on a pod to `kubectl exec` into it
- Press CTRL-O to open the log in your editor - Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list - Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to to rotate through a different sets of preview - Press CTRL-/ repeatedly to rotate through a different sets of preview
window options window options
1. `80%,border-bottom` 1. `80%,border-bottom`
1. `hidden` 1. `hidden`

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.18 or above - Go 1.20 or above
### Using Makefile ### Using Makefile
@@ -24,13 +24,23 @@ make build
make release make release
``` ```
> :warning: Makefile uses git commands to determine the version and the > [!WARNING]
> revision information for `fzf --version`. So if you're building fzf from an > Makefile uses git commands to determine the version and the revision
> information for `fzf --version`. So if you're building fzf from an
> environment where its git information is not available, you have to manually > environment where its git information is not available, you have to manually
> set `$FZF_VERSION` and `$FZF_REVISION`. > set `$FZF_VERSION` and `$FZF_REVISION`.
> >
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make` > e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
> [!TIP]
> To build fzf with profiling options enabled, set `TAGS=pprof`
>
> ```sh
> TAGS=pprof make clean install
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
> ```
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
@@ -42,6 +52,8 @@ Third-party libraries used
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [tcell](https://github.com/gdamore/tcell) - [tcell](https://github.com/gdamore/tcell)
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
- [fastwalk](https://github.com/charlievieth/fastwalk)
- Licensed under [MIT](https://raw.githubusercontent.com/charlievieth/fastwalk/master/LICENSE)
License License
------- -------

View File

@@ -1,6 +1,157 @@
CHANGELOG CHANGELOG
========= =========
0.50.0
------
- Search performance optimization. You can observe 50%+ improvement in some scenarios.
```
$ rg --line-number --no-heading --smart-case . > $DATA
$ wc < $DATA
5520118 26862362 897487793
$ hyperfine -w 1 -L bin fzf-0.49.0,fzf-7ce6452,fzf-a5447b8,fzf '{bin} --filter "///" < $DATA | head -30'
Summary
fzf --filter "///" < $DATA | head -30 ran
1.16 ± 0.03 times faster than fzf-a5447b8 --filter "///" < $DATA | head -30
1.23 ± 0.03 times faster than fzf-7ce6452 --filter "///" < $DATA | head -30
1.52 ± 0.03 times faster than fzf-0.49.0 --filter "///" < $DATA | head -30
```
- Added `jump` and `jump-cancel` events that are triggered when leaving `jump` mode
```sh
# Default behavior
fzf --bind space:jump
# Same as jump-accept action
fzf --bind space:jump,jump:accept
# Accept on jump, abort on cancel
fzf --bind space:jump,jump:accept,jump-cancel:abort
# Change header on jump-cancel
fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
```
- Added a new environment variable `$FZF_KEY` exported to the child processes. It's the name of the last key pressed.
```sh
fzf --bind 'space:jump,jump:accept,jump-cancel:transform:[[ $FZF_KEY =~ ctrl-c ]] && echo abort'
```
- fzf can be built with profiling options. See [BUILD.md](BUILD.md) for more information.
- Bug fixes
0.49.0
------
- Ingestion performance improved by around 40% (more or less depending on options)
- `--info=hidden` and `--info=inline-right` will no longer hide the horizontal separator by default. This gives you more flexibility in customizing the layout.
```sh
fzf --border --info=inline-right
fzf --border --info=inline-right --separator ═
fzf --border --info=inline-right --no-separator
fzf --border --info=hidden
fzf --border --info=hidden --separator ━
fzf --border --info=hidden --no-separator
```
- Added two environment variables exported to the child processes
- `FZF_PREVIEW_LABEL`
- `FZF_BORDER_LABEL`
```sh
# Use the current value of $FZF_PREVIEW_LABEL to determine which actions to perform
git ls-files |
fzf --header 'Press CTRL-P to change preview mode' \
--bind='ctrl-p:transform:[[ $FZF_PREVIEW_LABEL =~ cat ]] \
&& echo "change-preview(git log --color=always \{})+change-preview-label([[ log ]])" \
|| echo "change-preview(bat --color=always \{})+change-preview-label([[ cat ]])"'
```
- Renamed `track` action to `track-current` to highlight the difference between the global tracking state set by `--track` and a one-off tracking action
- `track` is still available as an alias
- Added `untrack-current` and `toggle-track-current` actions
- `*-current` actions are no-op when the global tracking state is set
- Bug fixes and minor improvements
0.48.1
------
- CTRL-T and ALT-C bindings can be disabled by setting `FZF_CTRL_T_COMMAND` and `FZF_ALT_C_COMMAND` to empty strings respectively when sourcing the script
```sh
# bash
FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --bash)"
# zsh
FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --zsh)"
# fish
fzf --fish | FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= source
```
- Setting the variables after sourcing the script will have no effect
- Bug fixes
0.48.0
------
- Shell integration scripts are now embedded in the fzf binary. This simplifies the distribution, and the users are less likely to have problems caused by using incompatible scripts and binaries.
- bash
```sh
# Set up fzf key bindings and fuzzy completion
eval "$(fzf --bash)"
```
- zsh
```sh
# Set up fzf key bindings and fuzzy completion
eval "$(fzf --zsh)"
```
- fish
```fish
# Set up fzf key bindings
fzf --fish | source
```
- Added options for customizing the behavior of the built-in walker
| Option | Description | Default |
| --- | --- | --- |
| `--walker=OPTS` | Walker options (`[file][,dir][,follow][,hidden]`) | `file,follow,hidden` |
| `--walker-root=DIR` | Root directory from which to start walker | `.` |
| `--walker-skip=DIRS` | Comma-separated list of directory names to skip | `.git,node_modules` |
- Examples
```sh
# Built-in walker is only used by standalone fzf when $FZF_DEFAULT_COMMAND is not set
unset FZF_DEFAULT_COMMAND
fzf # default: --walker=file,follow,hidden --walker-root=. --walker-skip=.git,node_modules
fzf --walker=file,dir,hidden,follow --walker-skip=.git,node_modules,target
# Walker options in $FZF_DEFAULT_OPTS
export FZF_DEFAULT_OPTS="--walker=file,dir,hidden,follow --walker-skip=.git,node_modules,target"
fzf
# Reading from STDIN; --walker is ignored
seq 100 | fzf --walker=dir
# Reading from $FZF_DEFAULT_COMMAND; --walker is ignored
export FZF_DEFAULT_COMMAND='seq 100'
fzf --walker=dir
```
- Shell integration scripts have been updated to use the built-in walker with these new options and they are now much faster out of the box.
0.47.0
------
- Replaced ["the default find command"][find] with a built-in directory walker to simplify the code and to achieve better performance and consistent behavior across platforms.
This doesn't affect you if you have `$FZF_DEFAULT_COMMAND` set.
- Breaking changes:
- Unlike [the previous "find" command][find], the new traversal code will list hidden files, but hidden directories will still be ignored
- No filtering of `devtmpfs` or `proc` types
- Traversal is parallelized, so the order of the entries will be different each time
- You may wonder why fzf implements directory walker anyway when it's a filter program following the [Unix philosophy][unix].
But fzf has had [the walker code for years][walker] to tackle the performance problem on Windows. And I decided to use the same approach on different platforms as well for the benefits listed above.
- Built-in walker is using the excellent [charlievieth/fastwalk][fastwalk] library, which easily outperforms its competitors and supports safely following symlinks.
- Added `$FZF_DEFAULT_OPTS_FILE` to allow managing default options in a file
- See [#3618](https://github.com/junegunn/fzf/pull/3618)
- Option precedence from lower to higher
1. Options read from `$FZF_DEFAULT_OPTS_FILE`
1. Options from `$FZF_DEFAULT_OPTS`
1. Options from command-line arguments
- Bug fixes and improvements
[find]: https://github.com/junegunn/fzf/blob/0.46.1/src/constants.go#L60-L64
[walker]: https://github.com/junegunn/fzf/pull/1847
[fastwalk]: https://github.com/charlievieth/fastwalk
[unix]: https://en.wikipedia.org/wiki/Unix_philosophy
0.46.1 0.46.1
------ ------
- Bug fixes and improvements - Bug fixes and improvements

View File

@@ -4,7 +4,7 @@ GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)
@@ -79,6 +79,7 @@ all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \

View File

@@ -238,19 +238,20 @@ call fzf#run({'sink': 'e'})
``` ```
We haven't specified the `source`, so this is equivalent to starting fzf on We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or command line without standard input pipe; fzf will traverse the file system
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current under the current directory to get the list of files. (If
directory. When you select one, it will open it with the sink, `:e` command. `$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
If you want to open it in a new tab, you can pass `:tabedit` command instead instead.) When you select one, it will open it with the sink, `:e` command. If
as the sink. you want to open it in a new tab, you can pass `:tabedit` command instead as
the sink.
```vim ```vim
call fzf#run({'sink': 'tabedit'}) call fzf#run({'sink': 'tabedit'})
``` ```
Instead of using the default find command, you can use any shell command as You can use any shell command as the source to generate the list. The
the source. The following example will list the files managed by git. It's following example will list the files managed by git. It's equivalent to
equivalent to running `git ls-files | fzf` on shell. running `git ls-files | fzf` on shell.
```vim ```vim
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})

194
README.md

File diff suppressed because one or more lines are too long

View File

@@ -95,9 +95,9 @@ while [[ $# -gt 0 ]]; do
elif [[ "$size" =~ %$ ]]; then elif [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:((${#size}-1))}
if [[ -n "$swap" ]]; then if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -l $(( 100 - size ))%"
else else
opt="$opt -p $size" opt="$opt -l $size%"
fi fi
else else
if [[ -n "$swap" ]]; then if [[ -n "$swap" ]]; then
@@ -196,8 +196,9 @@ if [[ "$opt" =~ "-E" ]]; then
exit 2 exit 2
fi fi
fi fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")" [[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")" [[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"

View File

@@ -1,4 +1,4 @@
fzf.txt fzf Last change: January 1 2024 fzf.txt fzf Last change: February 15 2024
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -264,17 +264,18 @@ entry.
call fzf#run({'sink': 'e'}) call fzf#run({'sink': 'e'})
< <
We haven't specified the `source`, so this is equivalent to starting fzf on We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or command line without standard input pipe; fzf will traverse the file system
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current under the current directory to get the list of files. (If
directory. When you select one, it will open it with the sink, `:e` command. `$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
If you want to open it in a new tab, you can pass `:tabedit` command instead instead.) When you select one, it will open it with the sink, `:e` command. If
as the sink. you want to open it in a new tab, you can pass `:tabedit` command instead as
the sink.
> >
call fzf#run({'sink': 'tabedit'}) call fzf#run({'sink': 'tabedit'})
< <
Instead of using the default find command, you can use any shell command as You can use any shell command as the source to generate the list. The
the source. The following example will list the files managed by git. It's following example will list the files managed by git. It's equivalent to
equivalent to running `git ls-files | fzf` on shell. running `git ls-files | fzf` on shell.
> >
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})
< <
@@ -417,24 +418,12 @@ TIPS *fzf-tips*
< fzf inside terminal buffer >________________________________________________~ < fzf inside terminal buffer >________________________________________________~
*fzf-inside-terminal-buffer* *fzf-inside-terminal-buffer*
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with a non-default layout
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}`
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer. On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
If you find the default ANSI colors to be different, consider configuring the If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
in Neovim. in Neovim.
*g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13*
*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9*
*g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5*
*g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1*
*g:terminal_color_0*
> >
" Terminal colors for seoul256 color scheme " Terminal colors for seoul256 color scheme
if has('nvim') if has('nvim')

15
go.mod
View File

@@ -1,21 +1,20 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell/v2 v2.7.0 github.com/charlievieth/fastwalk v1.0.3
github.com/mattn/go-isatty v0.0.17 github.com/gdamore/tcell/v2 v2.7.4
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.6 github.com/rivo/uniseg v0.4.7
github.com/saracen/walker v0.1.3 golang.org/x/sys v0.19.0
golang.org/x/sys v0.16.0 golang.org/x/term v0.19.0
golang.org/x/term v0.16.0
) )
require ( require (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
) )
go 1.17 go 1.20

33
go.sum
View File

@@ -1,21 +1,21 @@
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.0 h1:I5LiGTQuwrysAt1KS9wg1yFfOI3arI3ucFrxtd/xqaA= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.0/go.mod h1:hl/KtAANGBecfIPxk+FzKvThTqI84oplgbPEmVX60b8= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -26,27 +26,24 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

36
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.46.1 version=0.50.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -262,6 +262,12 @@ if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
PATH="\${PATH:+\${PATH}:}$fzf_base/bin" PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
echo "eval \"\$(fzf --$shell)\"" >> "$src"
else
cat >> "$src" << EOF
# Auto-completion # Auto-completion
# --------------- # ---------------
$fzf_completion $fzf_completion
@@ -270,6 +276,7 @@ $fzf_completion
# ------------ # ------------
$fzf_key_bindings $fzf_key_bindings
EOF EOF
fi
echo "OK" echo "OK"
done done
@@ -281,18 +288,6 @@ if [[ "$shells" =~ fish ]]; then
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
EOF EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p "${fish_dir}/functions"
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
if [ $key_bindings -ne 0 ]; then
echo -n "Symlink $fish_binding ... "
ln -sf "$fzf_base/shell/key-bindings.fish" \
"$fish_binding" && echo "OK" || echo "Failed"
else
echo -n "Removing $fish_binding ... "
rm -f "$fish_binding"
echo "OK"
fi
fi fi
append_line() { append_line() {
@@ -355,12 +350,23 @@ done
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then if [ ! -e "$bind_file" ]; then
mkdir -p "${fish_dir}/functions"
create_file "$bind_file" \ create_file "$bind_file" \
'function fish_user_key_bindings' \ 'function fish_user_key_bindings' \
' fzf_key_bindings' \ ' fzf --fish | source' \
'end' 'end'
else else
append_line $update_config "fzf_key_bindings" "$bind_file" echo "Check $bind_file:"
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $lno ]]; then
echo " ** Found 'fzf_key_bindings' in line #$lno"
echo " ** You have to replace the line to 'fzf --fish | source'"
echo
else
echo " - Clear"
echo
append_line $update_config "fzf --fish | source" "$bind_file"
fi
fi fi
fi fi

View File

@@ -1,4 +1,4 @@
$version="0.46.1" $version="0.50.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

45
main.go
View File

@@ -1,14 +1,55 @@
package main package main
import ( import (
_ "embed"
"fmt"
"strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.46" var version string = "0.50"
var revision string = "devel" var revision string = "devel"
//go:embed shell/key-bindings.bash
var bashKeyBindings []byte
//go:embed shell/completion.bash
var bashCompletion []byte
//go:embed shell/key-bindings.zsh
var zshKeyBindings []byte
//go:embed shell/completion.zsh
var zshCompletion []byte
//go:embed shell/key-bindings.fish
var fishKeyBindings []byte
func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content)))
fmt.Println("### end: " + label + " ###")
}
func main() { func main() {
protector.Protect() protector.Protect()
fzf.Run(fzf.ParseOptions(), version, revision) options := fzf.ParseOptions()
if options.Bash {
printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion)
return
}
if options.Zsh {
printScript("key-bindings.zsh", zshKeyBindings)
printScript("completion.zsh", zshCompletion)
return
}
if options.Fish {
printScript("key-bindings.fish", fishKeyBindings)
fmt.Println("fzf_key_bindings")
return
}
fzf.Run(options, version, revision)
} }

158
main_test.go Normal file
View File

@@ -0,0 +1,158 @@
package main
import (
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var pkgs []*build.Package
seen := make(map[string]bool)
err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
return filepath.SkipDir
}
return nil
}
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path)
if !seen[dir] {
pkg, err := build.ImportDir(dir, build.ImportComment)
if err != nil {
return fmt.Errorf("%s: %s", dir, err)
}
if pkg.ImportPath == "" || pkg.ImportPath == "." {
importPath, err := filepath.Rel(wd, dir)
if err != nil {
t.Fatal(err)
}
pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
}
pkgs = append(pkgs, pkg)
seen[dir] = true
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].ImportPath < pkgs[j].ImportPath
})
return pkgs
}
var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
var files []*ast.File
fset := token.NewFileSet()
for _, name := range bpkg.GoFiles {
filename := filepath.Join(bpkg.Dir, name)
af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files = append(files, af)
}
info := types.Info{
Uses: make(map[*ast.Ident]types.Object),
}
conf := types.Config{
Importer: sourceImporter,
}
_, err := conf.Check(bpkg.Name, fset, files, &info)
if err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for id, obj := range info.Uses {
if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
pos := fset.Position(id.Pos())
name, err := filepath.Rel(wd, pos.Filename)
if err != nil {
t.Log(err)
name = pos.Filename
}
name = filepath.ToSlash(name)
// Check if the usage is allowed
if allowed[name] > 0 {
allowed[name]--
continue
}
t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
errOsExit = true
}
}
return errOsExit
}
// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
// references to it anywhere else in the fzf code base.
func TestOSExitNotAllowed(t *testing.T) {
if testing.Short() {
t.Skip("skipping: short test")
}
allowed := map[string]int{
"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
}
var errOsExit bool
for _, pkg := range loadPackages(t) {
t.Run(pkg.ImportPath, func(t *testing.T) {
if checkPackageForOsExit(t, pkg, allowed) {
errOsExit = true
}
})
}
if t.Failed() && errOsExit {
var names []string
for name := range allowed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
const errMsg = `
Test failed because os.Exit was referenced outside of the following files:
%s
Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
This is enforced because calling os.Exit() prevents the functions
registered with util.AtExit() from running.`
t.Errorf(errMsg, strings.Join(names, "\n "))
}
}

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Feb 2024" "fzf 0.46.1" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Apr 2024" "fzf 0.50.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2024" "fzf 0.46.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2024" "fzf 0.50.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -33,6 +33,10 @@ fzf [options]
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
.SH OPTIONS .SH OPTIONS
.SS Note
.TP
Most long options have the opposite version with \fB--no-\fR prefix.
.SS Search mode .SS Search mode
.TP .TP
.B "-x, --extended" .B "-x, --extended"
@@ -187,7 +191,7 @@ actions are affected:
\fBkill-word\fR \fBkill-word\fR
.TP .TP
.BI "--jump-labels=" "CHARS" .BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR mode.
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "[~]HEIGHT[%]" .BI "--height=" "[~]HEIGHT[%]"
@@ -368,20 +372,21 @@ e.g.
.TP .TP
.BI "--info=" "STYLE" .BI "--info=" "STYLE"
Determines the display style of finder info (match counters). Determines the display style of the finder info. (e.g. match counter, loading indicator, etc.)
.BR default " On the left end of the horizontal separator"
.br .br
.BR default " Display on the next line to the prompt" .BR right " On the right end of the horizontal separator"
.br .br
.BR right " Display on the right end of the next line to the prompt" .BR hidden " Do not display finder info"
.br .br
.BR inline " Display on the same line with the default separator ' < '" .BR inline " After the prompt with the default prefix ' < '"
.br .br
.BR inline:SEPARATOR " Display on the same line with a non-default separator" .BR inline:PREFIX " After the prompt with a non-default prefix"
.br .br
.BR inline-right " Display on the right end of the same line .BR inline-right " On the right end of the prompt line"
.br .br
.BR hidden " Do not display finder info" .BR inline-right:PREFIX " On the right end of the prompt line with a custom prefix"
.br .br
.TP .TP
@@ -620,7 +625,7 @@ The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-p
script to render an image using either of the protocols inside the preview window. script to render an image using either of the protocols inside the preview window.
e.g. e.g.
\fBfzf --preview='fzf-preview.sh {}' \fBfzf --preview='fzf-preview.sh {}'\fR
.RE .RE
@@ -854,8 +859,49 @@ e.g.
.B "--version" .B "--version"
Display version information and exit Display version information and exit
.SS Directory traversal
.TP .TP
Note that most options have the opposite versions with \fB--no-\fR prefix. .B "--walker=[file][,dir][,follow][,hidden]"
Determines the behavior of the built-in directory walker that is used when
\fB$FZF_DEFAULT_COMMAND\fR is not set. The default value is \fBfile,follow,hidden\fR.
* \fBfile\fR: Include files in the search result
.br
* \fBdir\fR: Include directories in the search result
.br
* \fBhidden\fR: Include and follow hidden directories
.br
* \fBfollow\fR: Follow symbolic links
.br
.TP
.B "--walker-root=DIR"
The root directory from which to start the built-in directory walker.
The default value is the current working directory.
.TP
.B "--walker-skip=DIRS"
Comma-separated list of directory names to skip during the directory walk.
The default value is \fB.git,node_modules\fR.
.SS Shell integration
.TP
.B "--bash"
Print script to set up Bash shell integration
e.g. \fBeval "$(fzf --bash)"\fR
.TP
.B "--zsh"
Print script to set up Zsh shell integration
e.g. \fBeval "$(fzf --zsh)"\fR
.TP
.B "--fish"
Print script to set up Fish shell integration
e.g. \fBfzf --fish | source\fR
.SH ENVIRONMENT VARIABLES .SH ENVIRONMENT VARIABLES
.TP .TP
@@ -865,7 +911,14 @@ with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in
this case make sure that the command is POSIX-compliant. this case make sure that the command is POSIX-compliant.
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR Default options.
.br
e.g. \fBexport FZF_DEFAULT_OPTS="--layout=reverse --border --cycle"\fR
.TP
.B FZF_DEFAULT_OPTS_FILE
The location of the file that contains the default options.
.br
e.g. \fBexport FZF_DEFAULT_OPTS_FILE=~/.fzfrc\fR
.TP .TP
.B FZF_API_KEY .B FZF_API_KEY
Can be used to require an API key when using \fB--listen\fR option. If not set, Can be used to require an API key when using \fB--listen\fR option. If not set,
@@ -923,8 +976,14 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
.br .br
.BR FZF_PREVIEW_LABEL " Preview label string"
.br
.BR FZF_BORDER_LABEL " Border label string"
.br
.BR FZF_ACTION " The name of the last action performed" .BR FZF_ACTION " The name of the last action performed"
.br .br
.BR FZF_KEY " The name of the last key pressed"
.br
.BR FZF_PORT " Port number when --listen option is used" .BR FZF_PORT " Port number when --listen option is used"
.br .br
@@ -995,21 +1054,21 @@ e.g.
.br .br
\fIctrl-]\fR \fIctrl-]\fR
.br .br
\fIctrl-^\fR (\fIctrl-6\fR) \fIctrl-^\fR (\fIctrl-6\fR)
.br .br
\fIctrl-/\fR (\fIctrl-_\fR) \fIctrl-/\fR (\fIctrl-_\fR)
.br .br
\fIctrl-alt-[a-z]\fR \fIctrl-alt-[a-z]\fR
.br .br
\fIalt-[*]\fR (Any case-sensitive single character is allowed) \fIalt-[*]\fR (Any case-sensitive single character is allowed)
.br .br
\fIf[1-12]\fR \fIf[1-12]\fR
.br .br
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
.br .br
\fIspace\fR \fIspace\fR
.br .br
\fIbspace\fR (\fIbs\fR) \fIbackspace\fR (\fIbspace\fR \fIbs\fR)
.br .br
\fIalt-up\fR \fIalt-up\fR
.br .br
@@ -1023,15 +1082,15 @@ e.g.
.br .br
\fIalt-space\fR \fIalt-space\fR
.br .br
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-backspace\fR (\fIalt-bspace\fR \fIalt-bs\fR)
.br .br
\fItab\fR \fItab\fR
.br .br
\fIbtab\fR (\fIshift-tab\fR) \fIshift-tab\fR (\fIbtab\fR)
.br .br
\fIesc\fR \fIesc\fR
.br .br
\fIdel\fR \fIdelete\fR (\fIdel\fR)
.br .br
\fIup\fR \fIup\fR
.br .br
@@ -1047,9 +1106,9 @@ e.g.
.br .br
\fIinsert\fR \fIinsert\fR
.br .br
\fIpgup\fR (\fIpage-up\fR) \fIpage-up\fR (\fIpgup\fR)
.br .br
\fIpgdn\fR (\fIpage-down\fR) \fIpage-down\fR (\fIpgdn\fR)
.br .br
\fIshift-up\fR \fIshift-up\fR
.br .br
@@ -1103,6 +1162,7 @@ e.g.
\fB# Move cursor to the last item and select all items \fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.RE .RE
\fIload\fR \fIload\fR
.RS .RS
Triggered when the input stream is complete and the initial processing of the Triggered when the input stream is complete and the initial processing of the
@@ -1112,6 +1172,7 @@ e.g.
\fB# Change the prompt to "loaded" when the input stream is complete \fB# Change the prompt to "loaded" when the input stream is complete
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR (seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
.RE .RE
\fIresize\fR \fIresize\fR
.RS .RS
Triggered when the terminal size is changed. Triggered when the terminal size is changed.
@@ -1119,6 +1180,7 @@ Triggered when the terminal size is changed.
e.g. e.g.
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR \fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
.RE .RE
\fIresult\fR \fIresult\fR
.RS .RS
Triggered when the filtering for the current query is complete and the result list is ready. Triggered when the filtering for the current query is complete and the result list is ready.
@@ -1152,6 +1214,7 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE .RE
\fIone\fR \fIone\fR
.RS .RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable Triggered when there's only one match. \fBone:accept\fR binding is comparable
@@ -1163,6 +1226,7 @@ e.g.
\fB# Automatically select the only match \fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR seq 10 | fzf --bind one:accept\fR
.RE .RE
\fIzero\fR \fIzero\fR
.RS .RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to Triggered when there's no match. \fBzero:abort\fR binding is comparable to
@@ -1184,6 +1248,22 @@ e.g.
\fBfzf --bind backward-eof:abort\fR \fBfzf --bind backward-eof:abort\fR
.RE .RE
\fIjump\fR
.RS
Triggered when successfully jumped to the target item in \fBjump\fR mode.
e.g.
\fBfzf --bind space:jump,jump:accept\fR
.RE
\fIjump-cancel\fR
.RS
Triggered when \fBjump\fR mode is cancelled.
e.g.
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
.RE
.SS AVAILABLE ACTIONS: .SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
@@ -1226,7 +1306,6 @@ A key or an event can be bound to one or more of the following actions.
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR) \fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
@@ -1274,9 +1353,10 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle-track\fR \fBtoggle-track\fR (toggle global tracking option (\fB--track\fR))
\fBtoggle-track-current\fR (toggle tracking of the current item)
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtrack\fR (track the current item; automatically disabled if focus changes) \fBtrack-current\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command) \fBtransform(...)\fR (transform states using the output of an external command)
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header using an external command) \fBtransform-header(...)\fR (transform header using an external command)
@@ -1286,6 +1366,7 @@ A key or an event can be bound to one or more of the following actions.
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBuntrack-current\fR (stop tracking the current item; no-op if global tracking is enabled)
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR

View File

@@ -9,26 +9,23 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ $- =~ i ]] || return 0 if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -F _fzf_compgen_path > /dev/null; then #
_fzf_compgen_path() { # _fzf_compgen_path() {
echo "$1" # echo "$1"
command find -L "$1" \ # command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@' # -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
} # }
fi #
# _fzf_compgen_dir() {
if ! declare -F _fzf_compgen_dir > /dev/null; then # command find -L "$1" \
_fzf_compgen_dir() { # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
command find -L "$1" \ # -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ # }
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
}
fi
########################################################### ###########################################################
@@ -63,6 +60,31 @@ __fzf_orig_completion() {
done done
} }
# @param $1 cmd - Command name for which the original completion is searched
# @var[out] REPLY - Original function name is returned
__fzf_orig_completion_get_orig_func() {
local cmd orig_var orig
cmd=$1
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}"
REPLY="${orig##*#}"
[[ $REPLY ]] && type "$REPLY" &> /dev/null
}
# @param $1 cmd - Command name for which the original completion is searched
# @param $2 func - Fzf's completion function to replace the original function
# @var[out] REPLY - Completion setting is returned as a string to "eval"
__fzf_orig_completion_instantiate() {
local cmd func orig_var orig
cmd=$1
func=$2
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}"
orig="${orig%#*}"
[[ $orig == *' %s '* ]] || return 1
printf -v REPLY "$orig" "$func"
}
_fzf_opts_completion() { _fzf_opts_completion() {
local cur prev opts local cur prev opts
COMPREPLY=() COMPREPLY=()
@@ -261,28 +283,32 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd orig_complete local cmd ret REPLY orig_cmd orig_complete
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd" if __fzf_orig_completion_get_orig_func "$cmd"; then
orig="${!orig_var-}" "$REPLY" "$@"
orig="${orig##*#}"
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
$orig "$@"
elif [[ -n "${_fzf_completion_loader-}" ]]; then elif [[ -n "${_fzf_completion_loader-}" ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@" $_fzf_completion_loader "$@"
ret=$? ret=$?
# _completion_loader may not have updated completion for the command # _completion_loader may not have updated completion for the command
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null) __fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
# Update orig_complete by _fzf_orig_completion entry
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
orig_complete=$REPLY
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }" eval "${orig_complete/ -F / -o nospace -F }"
else else
eval "$orig_complete" eval "$orig_complete"
fi fi
fi fi
[[ $ret -eq 0 ]] && return 124
return $ret return $ret
fi fi
} }
@@ -293,7 +319,6 @@ __fzf_generic_path_completion() {
if [[ $cmd == \\* ]]; then if [[ $cmd == \\* ]]; then
cmd="${cmd:1}" cmd="${cmd:1}"
fi fi
cmd="${cmd//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
@@ -309,9 +334,18 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[[ -z "$dir" ]] && dir='.' [[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do matches=$(
printf "%q " "${item%$3}$3" unset FZF_DEFAULT_COMMAND
done) export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2"
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir"
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
)
matches=${matches% } matches=${matches% }
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " [[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n "$matches" ]]; then if [[ -n "$matches" ]]; then
@@ -359,8 +393,8 @@ _fzf_complete() {
post="$(caller 0 | command awk '{print $2}')_post" post="$(caller 0 | command awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post='command cat' type -t "$post" > /dev/null 2>&1 || post='command cat'
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cmd="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
@@ -483,20 +517,25 @@ a_cmds="
# Preserve existing completion # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
if type _completion_loader > /dev/null 2>&1; then if type _comp_load > /dev/null 2>&1; then
_fzf_completion_loader=1 # _comp_load was added in bash-completion 2.12 to replace _completion_loader.
# We use it without -D option so that it does not use _comp_complete_minimal as the fallback.
_fzf_completion_loader=_comp_load
elif type __load_completion > /dev/null 2>&1; then
# In bash-completion 2.11, _completion_loader internally calls __load_completion
# and if it returns a non-zero status, it sets the default 'minimal' completion.
_fzf_completion_loader=__load_completion
elif type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=_completion_loader
fi fi
__fzf_defc() { __fzf_defc() {
local cmd func opts orig_var orig def local cmd func opts REPLY
cmd="$1" cmd="$1"
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" if __fzf_orig_completion_instantiate "$cmd" "$func"; then
orig="${!orig_var-}" eval "$REPLY"
if [[ -n "$orig" ]]; then
printf -v def "$orig" "$func"
eval "$def"
else else
complete -F "$func" $opts "$cmd" complete -F "$func" $opts "$cmd"
fi fi
@@ -509,7 +548,7 @@ done
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
done done
# ssh # ssh
@@ -542,3 +581,5 @@ _fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet _fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill
fi

View File

@@ -9,7 +9,7 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0 if [[ -o interactive ]]; then
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
@@ -77,22 +77,19 @@ fi
{ {
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then #
_fzf_compgen_path() { # _fzf_compgen_path() {
echo "$1" # echo "$1"
command find -L "$1" \ # command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' # -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} # }
fi #
# _fzf_compgen_dir() {
if ! declare -f _fzf_compgen_dir > /dev/null; then # command find -L "$1" \
_fzf_compgen_dir() { # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
command find -L "$1" \ # -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ # }
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
########################################################### ###########################################################
@@ -148,10 +145,19 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do matches=$(
item="${item%$suffix}$suffix" unset FZF_DEFAULT_COMMAND
echo -n "${(q)item} " export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}"
done) if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty
fi | while read item; do
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
done
)
matches=${matches% } matches=${matches% }
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
@@ -345,3 +351,5 @@ bindkey '^I' fzf-completion
eval $__fzf_completion_options eval $__fzf_completion_options
'unset' '__fzf_completion_options' 'unset' '__fzf_completion_options'
} }
fi

View File

@@ -11,20 +11,15 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0 if [[ $- =~ i ]]; then
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd opts local opts
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
-o -type f -print \ FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
-o -type d -print \
-o -type l -print 2> /dev/null | command cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
while read -r item; do while read -r item; do
printf '%q ' "$item" # escape special chars printf '%q ' "$item" # escape special chars
done done
@@ -42,11 +37,11 @@ fzf-file-widget() {
} }
__fzf_cd__() { __fzf_cd__() {
local cmd opts dir local opts dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
-o -type d -print 2> /dev/null | command cut -b3-"}" dir=$(
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m" FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir" ) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
} }
if command -v perl > /dev/null; then if command -v perl > /dev/null; then
@@ -107,9 +102,11 @@ bind -m emacs-standard '"\C-z": vi-editing-mode'
if (( BASH_VERSINFO[0] < 4 )); then if (( BASH_VERSINFO[0] < 4 )); then
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"' bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"' bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
@@ -117,9 +114,11 @@ if (( BASH_VERSINFO[0] < 4 )); then
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard -x '"\C-t": fzf-file-widget' if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m vi-command -x '"\C-t": fzf-file-widget' bind -m emacs-standard -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget' bind -m vi-command -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard -x '"\C-r": __fzf_history__' bind -m emacs-standard -x '"\C-r": __fzf_history__'
@@ -128,6 +127,10 @@ else
fi fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
fi

View File

@@ -21,22 +21,15 @@ function fzf_key_bindings
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -lx dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3] set -l prefix $commandline[3]
# "-path \$dir'*/.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
commandline -f repaint commandline -f repaint
@@ -77,17 +70,15 @@ function fzf_key_bindings
function fzf-cd-widget -d "Change directory" function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -lx dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3] set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd -- $result cd -- $result
@@ -113,14 +104,22 @@ function fzf_key_bindings
end end
end end
bind \ct fzf-file-widget
bind \cr fzf-history-widget bind \cr fzf-history-widget
bind \ec fzf-cd-widget if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
end
if bind -M insert > /dev/null 2>&1 if bind -M insert > /dev/null 2>&1
bind -M insert \ct fzf-file-widget
bind -M insert \cr fzf-history-widget bind -M insert \cr fzf-history-widget
bind -M insert \ec fzf-cd-widget if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind -M insert \ec fzf-cd-widget
end
end end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix' function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'

View File

@@ -11,7 +11,7 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0 if [[ -o interactive ]]; then
# Key bindings # Key bindings
@@ -41,13 +41,9 @@ fi
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@@ -66,33 +62,35 @@ fzf-file-widget() {
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-file-widget if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bindkey -M emacs '^T' fzf-file-widget zle -N fzf-file-widget
bindkey -M vicmd '^T' fzf-file-widget bindkey -M emacs '^T' fzf-file-widget
bindkey -M viins '^T' fzf-file-widget bindkey -M vicmd '^T' fzf-file-widget
bindkey -M viins '^T' fzf-file-widget
fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)" local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
fi fi
zle push-line # Clear buffer. Auto-restored on next prompt. zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="builtin cd -- ${(q)dir}" BUFFER="builtin cd -- ${(q)dir:a}"
zle accept-line zle accept-line
local ret=$? local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion unset dir # ensure this doesn't end up appearing in prompt expansion
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-cd-widget if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bindkey -M emacs '\ec' fzf-cd-widget zle -N fzf-cd-widget
bindkey -M vicmd '\ec' fzf-cd-widget bindkey -M emacs '\ec' fzf-cd-widget
bindkey -M viins '\ec' fzf-cd-widget bindkey -M vicmd '\ec' fzf-cd-widget
bindkey -M viins '\ec' fzf-cd-widget
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
@@ -121,3 +119,5 @@ bindkey -M viins '^R' fzf-history-widget
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options
'unset' '__fzf_key_bindings_options' 'unset' '__fzf_key_bindings_options'
} }
fi

View File

@@ -54,72 +54,74 @@ func _() {
_ = x[actToggleIn-43] _ = x[actToggleIn-43]
_ = x[actToggleOut-44] _ = x[actToggleOut-44]
_ = x[actToggleTrack-45] _ = x[actToggleTrack-45]
_ = x[actToggleHeader-46] _ = x[actToggleTrackCurrent-46]
_ = x[actTrack-47] _ = x[actToggleHeader-47]
_ = x[actDown-48] _ = x[actTrackCurrent-48]
_ = x[actUp-49] _ = x[actUntrackCurrent-49]
_ = x[actPageUp-50] _ = x[actDown-50]
_ = x[actPageDown-51] _ = x[actUp-51]
_ = x[actPosition-52] _ = x[actPageUp-52]
_ = x[actHalfPageUp-53] _ = x[actPageDown-53]
_ = x[actHalfPageDown-54] _ = x[actPosition-54]
_ = x[actOffsetUp-55] _ = x[actHalfPageUp-55]
_ = x[actOffsetDown-56] _ = x[actHalfPageDown-56]
_ = x[actJump-57] _ = x[actOffsetUp-57]
_ = x[actJumpAccept-58] _ = x[actOffsetDown-58]
_ = x[actPrintQuery-59] _ = x[actJump-59]
_ = x[actRefreshPreview-60] _ = x[actJumpAccept-60]
_ = x[actReplaceQuery-61] _ = x[actPrintQuery-61]
_ = x[actToggleSort-62] _ = x[actRefreshPreview-62]
_ = x[actShowPreview-63] _ = x[actReplaceQuery-63]
_ = x[actHidePreview-64] _ = x[actToggleSort-64]
_ = x[actTogglePreview-65] _ = x[actShowPreview-65]
_ = x[actTogglePreviewWrap-66] _ = x[actHidePreview-66]
_ = x[actTransform-67] _ = x[actTogglePreview-67]
_ = x[actTransformBorderLabel-68] _ = x[actTogglePreviewWrap-68]
_ = x[actTransformHeader-69] _ = x[actTransform-69]
_ = x[actTransformPreviewLabel-70] _ = x[actTransformBorderLabel-70]
_ = x[actTransformPrompt-71] _ = x[actTransformHeader-71]
_ = x[actTransformQuery-72] _ = x[actTransformPreviewLabel-72]
_ = x[actPreview-73] _ = x[actTransformPrompt-73]
_ = x[actChangePreview-74] _ = x[actTransformQuery-74]
_ = x[actChangePreviewWindow-75] _ = x[actPreview-75]
_ = x[actPreviewTop-76] _ = x[actChangePreview-76]
_ = x[actPreviewBottom-77] _ = x[actChangePreviewWindow-77]
_ = x[actPreviewUp-78] _ = x[actPreviewTop-78]
_ = x[actPreviewDown-79] _ = x[actPreviewBottom-79]
_ = x[actPreviewPageUp-80] _ = x[actPreviewUp-80]
_ = x[actPreviewPageDown-81] _ = x[actPreviewDown-81]
_ = x[actPreviewHalfPageUp-82] _ = x[actPreviewPageUp-82]
_ = x[actPreviewHalfPageDown-83] _ = x[actPreviewPageDown-83]
_ = x[actPrevHistory-84] _ = x[actPreviewHalfPageUp-84]
_ = x[actPrevSelected-85] _ = x[actPreviewHalfPageDown-85]
_ = x[actPut-86] _ = x[actPrevHistory-86]
_ = x[actNextHistory-87] _ = x[actPrevSelected-87]
_ = x[actNextSelected-88] _ = x[actPut-88]
_ = x[actExecute-89] _ = x[actNextHistory-89]
_ = x[actExecuteSilent-90] _ = x[actNextSelected-90]
_ = x[actExecuteMulti-91] _ = x[actExecute-91]
_ = x[actSigStop-92] _ = x[actExecuteSilent-92]
_ = x[actFirst-93] _ = x[actExecuteMulti-93]
_ = x[actLast-94] _ = x[actSigStop-94]
_ = x[actReload-95] _ = x[actFirst-95]
_ = x[actReloadSync-96] _ = x[actLast-96]
_ = x[actDisableSearch-97] _ = x[actReload-97]
_ = x[actEnableSearch-98] _ = x[actReloadSync-98]
_ = x[actSelect-99] _ = x[actDisableSearch-99]
_ = x[actDeselect-100] _ = x[actEnableSearch-100]
_ = x[actUnbind-101] _ = x[actSelect-101]
_ = x[actRebind-102] _ = x[actDeselect-102]
_ = x[actBecome-103] _ = x[actUnbind-103]
_ = x[actResponse-104] _ = x[actRebind-104]
_ = x[actShowHeader-105] _ = x[actBecome-105]
_ = x[actHideHeader-106] _ = x[actResponse-106]
_ = x[actShowHeader-107]
_ = x[actHideHeader-108]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 640, 655, 670, 687, 694, 699, 708, 719, 730, 743, 758, 769, 782, 789, 802, 815, 832, 847, 860, 874, 888, 904, 924, 936, 959, 977, 1001, 1019, 1036, 1046, 1062, 1084, 1097, 1113, 1125, 1139, 1155, 1173, 1193, 1215, 1229, 1244, 1250, 1264, 1279, 1289, 1305, 1320, 1330, 1338, 1345, 1354, 1367, 1383, 1398, 1407, 1418, 1427, 1436, 1445, 1456, 1469, 1482}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -153,6 +153,12 @@ var (
bonusBoundaryDelimiter int16 = bonusBoundary + 1 bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite initialCharClass charClass = charWhite
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass
// A minor optimization that can give yet another 5% performance boost
bonusMatrix [charNumber + 1][charNumber + 1]int16
) )
type charClass int type charClass int
@@ -187,6 +193,27 @@ func Init(scheme string) bool {
default: default:
return false return false
} }
for i := 0; i <= unicode.MaxASCII; i++ {
char := rune(i)
c := charNonWord
if char >= 'a' && char <= 'z' {
c = charLower
} else if char >= 'A' && char <= 'Z' {
c = charUpper
} else if char >= '0' && char <= '9' {
c = charNumber
} else if strings.ContainsRune(whiteChars, char) {
c = charWhite
} else if strings.ContainsRune(delimiterChars, char) {
c = charDelimiter
}
asciiCharClasses[i] = c
}
for i := 0; i <= int(charNumber); i++ {
for j := 0; j <= int(charNumber); j++ {
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
}
}
return true return true
} }
@@ -214,21 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
return offset, make([]int32, size) return offset, make([]int32, size)
} }
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
} else if strings.ContainsRune(whiteChars, char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass { func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) { if unicode.IsLower(char) {
return charLower return charLower
@@ -248,31 +260,36 @@ func charClassOfNonAscii(char rune) charClass {
func charClassOf(char rune) charClass { func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
return charClassOfAscii(char) return asciiCharClasses[char]
} }
return charClassOfNonAscii(char) return charClassOfNonAscii(char)
} }
func bonusFor(prevClass charClass, class charClass) int16 { func bonusFor(prevClass charClass, class charClass) int16 {
if class > charNonWord { if class > charNonWord {
if prevClass == charWhite { switch prevClass {
case charWhite:
// Word boundary after whitespace // Word boundary after whitespace
return bonusBoundaryWhite return bonusBoundaryWhite
} else if prevClass == charDelimiter { case charDelimiter:
// Word boundary after a delimiter character // Word boundary after a delimiter character
return bonusBoundaryDelimiter return bonusBoundaryDelimiter
} else if prevClass == charNonWord { case charNonWord:
// Word boundary // Word boundary
return bonusBoundary return bonusBoundary
} }
} }
if prevClass == charLower && class == charUpper || if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber { prevClass != charNumber && class == charNumber {
// camelCase letter123 // camelCase letter123
return bonusCamel123 return bonusCamel123
} else if class == charNonWord { }
switch class {
case charNonWord, charDelimiter:
return bonusNonWord return bonusNonWord
} else if class == charWhite { case charWhite:
return bonusBoundaryWhite return bonusBoundaryWhite
} }
return 0 return 0
@@ -282,7 +299,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 { if idx == 0 {
return bonusBoundaryWhite return bonusBoundaryWhite
} }
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
} }
func normalizeRune(r rune) rune { func normalizeRune(r rune) rune {
@@ -335,30 +352,45 @@ func isAscii(runes []rune) bool {
return true return true
} }
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
// Can't determine // Can't determine
if !input.IsBytes() { if !input.IsBytes() {
return 0 return 0, input.Length()
} }
// Not possible // Not possible
if !isAscii(pattern) { if !isAscii(pattern) {
return -1 return -1, -1
} }
firstIdx, idx := 0, 0 firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ { for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 { if idx < 0 {
return -1 return -1, -1
} }
if pidx == 0 && idx > 0 { if pidx == 0 && idx > 0 {
// Step back to find the right bonus point // Step back to find the right bonus point
firstIdx = idx - 1 firstIdx = idx - 1
} }
lastIdx = idx
idx++ idx++
} }
return firstIdx
// Find the last appearance of the last character of the pattern to limit the search scope
bu := b
if !caseSensitive && b >= 'a' && b <= 'z' {
bu = b - 32
}
scope := input.Bytes()[lastIdx:]
for offset := len(scope) - 1; offset > 0; offset-- {
if scope[offset] == b || scope[offset] == bu {
return firstIdx, lastIdx + offset + 1
}
}
return firstIdx, lastIdx + 1
} }
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
@@ -407,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return Result{0, 0, 0}, posArray(withPos, M) return Result{0, 0, 0}, posArray(withPos, M)
} }
N := input.Length() N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Since O(nm) algorithm can be prohibitively expensive for large input, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
@@ -415,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
// Phase 1. Optimized search for ASCII string // Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(input, pattern, caseSensitive) minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 { if minIdx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
N = maxIdx - minIdx
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0 offset16 := 0
@@ -431,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
offset32, F := alloc32(offset32, slab, M) offset32, F := alloc32(offset32, slab, M)
// Rune array // Rune array
_, T := alloc32(offset32, slab, N) _, T := alloc32(offset32, slab, N)
input.CopyRunes(T) input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point // Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0 maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0 pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:] for off, char := range T {
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
var class charClass var class charClass
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
class = charClassOfAscii(char) class = asciiCharClasses[char]
if !caseSensitive && class == charUpper { if !caseSensitive && class == charUpper {
char += 32 char += 32
T[off] = char
} }
} else { } else {
class = charClassOfNonAscii(char) class = charClassOfNonAscii(char)
@@ -454,28 +490,28 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if normalize { if normalize {
char = normalizeRune(char) char = normalizeRune(char)
} }
T[off] = char
} }
Tsub[off] = char bonus := bonusMatrix[prevClass][class]
bonus := bonusFor(prevClass, class) B[off] = bonus
Bsub[off] = bonus
prevClass = class prevClass = class
if char == pchar { if char == pchar {
if pidx < M { if pidx < M {
F[pidx] = int32(idx + off) F[pidx] = int32(off)
pidx++ pidx++
pchar = pattern[util.Min(pidx, M-1)] pchar = pattern[util.Min(pidx, M-1)]
} }
lastIdx = idx + off lastIdx = off
} }
if char == pchar0 { if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score H0[off] = score
C0sub[off] = 1 C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off maxScore, maxScorePos = score, off
if forward && bonus >= bonusBoundary { if forward && bonus >= bonusBoundary {
break break
} }
@@ -483,24 +519,24 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false inGap = false
} else { } else {
if inGap { if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0) H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else { } else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) H0[off] = util.Max16(prevH0+scoreGapStart, 0)
} }
C0sub[off] = 0 C0[off] = 0
inGap = true inGap = true
} }
prevH0 = H0sub[off] prevH0 = H0[off]
} }
if pidx != M { if pidx != M {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if M == 1 { if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
if !withPos { if !withPos {
return result, nil return result, nil
} }
pos := []int{maxScorePos} pos := []int{minIdx + maxScorePos}
return result, &pos return result, &pos
} }
@@ -597,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
if s > s1 && (s > s2 || s == s2 && preferMatch) { if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j) *pos = append(*pos, j+minIdx)
if i == 0 { if i == 0 {
break break
} }
@@ -610,7 +646,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Start offset we return here is only relevant when begin tiebreak is used. // Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't // However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance. // want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
} }
// Implement the same sorting criteria as V2 // Implement the same sorting criteria as V2
@@ -640,7 +676,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
*pos = append(*pos, idx) *pos = append(*pos, idx)
} }
score += scoreMatch score += scoreMatch
bonus := bonusFor(prevClass, class) bonus := bonusMatrix[prevClass][class]
if consecutive == 0 { if consecutive == 0 {
firstBonus = bonus firstBonus = bonus
} else { } else {
@@ -678,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
@@ -772,7 +809,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }

View File

@@ -9,6 +9,10 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func init() {
Init("default")
}
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
} }

View File

@@ -312,7 +312,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive // Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error. // integers and does not allocate on error.
code := 0 code := 0
for _, ch := range []byte(s) { for _, ch := range sbytes(s) {
ch -= '0' ch -= '0'
if ch > 9 { if ch > 9 {
return -1, delimiter, remaining return -1, delimiter, remaining

View File

@@ -2,7 +2,6 @@ package fzf
import ( import (
"math" "math"
"os"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -15,6 +14,7 @@ const (
// Reader // Reader
readerBufferSize = 64 * 1024 readerBufferSize = 64 * 1024
readerSlabSize = 128 * 1024
readerPollIntervalMin = 10 * time.Millisecond readerPollIntervalMin = 10 * time.Millisecond
readerPollIntervalStep = 5 * time.Millisecond readerPollIntervalStep = 5 * time.Millisecond
readerPollIntervalMax = 50 * time.Millisecond readerPollIntervalMax = 50 * time.Millisecond
@@ -54,16 +54,6 @@ const (
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
) )
var defaultCommand string
func init() {
if !util.IsWindows() {
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
} else if os.Getenv("TERM") == "cygwin" {
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
}
}
// fzf events // fzf events
const ( const (
EvtReadNew util.EventType = iota EvtReadNew util.EventType = iota

View File

@@ -3,8 +3,9 @@ package fzf
import ( import (
"fmt" "fmt"
"os" "sync"
"time" "time"
"unsafe"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -18,8 +19,18 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header) Matcher -> EvtHeader -> Terminal (update header)
*/ */
func ustring(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}
func sbytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options, version string, revision string) {
defer util.RunAtExitFuncs()
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
@@ -29,7 +40,7 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
fmt.Println(version) fmt.Println(version)
} }
os.Exit(exitOk) util.Exit(exitOk)
} }
// Event channel // Event channel
@@ -45,16 +56,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored { if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil)
lineAnsiState = newState lineAnsiState = newState
return util.ToChars([]byte(trimmed)), offsets return util.ToChars(sbytes(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(ustring(data), nil, nil)
return util.ToChars([]byte(trimmed)), nil return util.ToChars(sbytes(trimmed)), nil
} }
} }
} }
@@ -66,7 +77,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(data)) header = append(header, ustring(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
@@ -77,7 +88,7 @@ func Run(opts *Options, version string, revision string) {
}) })
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(string(data), opts.Delimiter) tokens := Tokenize(ustring(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
if prevLineAnsiState != nil { if prevLineAnsiState != nil {
@@ -101,7 +112,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor([]byte(transformed)) item.text, item.colors = ansiProcessor(sbytes(transformed))
item.text.TrimTrailingWhitespaces() item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
@@ -117,7 +128,7 @@ func Run(opts *Options, version string, revision string) {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil) }, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource() go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
// Matcher // Matcher
@@ -154,18 +165,21 @@ func Run(opts *Options, version string, revision string) {
found := false found := false
if streamingFilter { if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size) slab := util.MakeSlab(slab16Size, slab32Size)
mutex := sync.Mutex{}
reader := NewReader( reader := NewReader(
func(runes []byte) bool { func(runes []byte) bool {
item := Item{} item := Item{}
if chunkList.trans(&item, runes) { if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString()) opts.Printer(item.text.ToString())
found = true found = true
} }
mutex.Unlock()
} }
return false return false
}, eventBox, opts.ReadZero, false) }, eventBox, opts.ReadZero, false)
reader.ReadSource() reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
@@ -180,9 +194,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
os.Exit(exitOk) util.Exit(exitOk)
} }
os.Exit(exitNoMatch) util.Exit(exitNoMatch)
} }
// Synchronous search // Synchronous search
@@ -213,6 +227,7 @@ func Run(opts *Options, version string, revision string) {
reading := true reading := true
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *string
var nextEnviron []string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
total := 0 total := 0
query := []rune{} query := []rune{}
@@ -232,23 +247,20 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false useSnapshot := false
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command string) { restart := func(command string, environ []string) {
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++ inputRevision++
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command) go reader.restart(command, environ)
} }
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune { input := func() []rune {
reloaded := snapshotRevision != inputRevision
paused, input := terminal.Input() paused, input := terminal.Input()
if reloaded && paused { if !paused {
query = []rune{}
} else if !paused {
query = input query = input
} }
return query return query
@@ -263,11 +275,12 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
} }
os.Exit(value.(int)) util.Exit(value.(int))
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand) restart(*nextCommand, nextEnviron)
nextCommand = nil nextCommand = nil
nextEnviron = nil
break break
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
@@ -276,6 +289,9 @@ func Run(opts *Options, version string, revision string) {
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot, count = chunkList.Snapshot() snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
@@ -292,11 +308,13 @@ func Run(opts *Options, version string, revision string) {
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
var environ []string
var changed bool var changed bool
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
environ = val.environ
changed = val.changed changed = val.changed
if command != nil { if command != nil {
useSnapshot = val.sync useSnapshot = val.sync
@@ -306,18 +324,22 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
nextCommand = command nextCommand = command
nextEnviron = environ
} else { } else {
restart(*command) restart(*command, environ)
} }
} }
if !changed { if !changed {
break break
} }
if !useSnapshot { if !useSnapshot {
newSnapshot, _ := chunkList.Snapshot() newSnapshot, newCount := chunkList.Snapshot()
// We want to avoid showing empty list when reload is triggered // We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed // and the query string is changed at the same time i.e. command != nil && changed
if command == nil || len(newSnapshot) > 0 { if command == nil || newCount > 0 {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot = newSnapshot snapshot = newSnapshot
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
@@ -355,9 +377,9 @@ func Run(opts *Options, version string, revision string) {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) util.Exit(exitOk)
} }
os.Exit(exitNoMatch) util.Exit(exitNoMatch)
} }
determine(val.final) determine(val.final)
} }

View File

@@ -52,7 +52,7 @@ const usage = `usage: fzf [options]
--hscroll-off=COLS Number of screen columns to keep to the right of the --hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump mode
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
@@ -75,7 +75,7 @@ const usage = `usage: fzf [options]
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style --info=STYLE Finder info style
[default|right|hidden|inline[:SEPARATOR]|inline-right] [default|right|hidden|inline[-right][:PREFIX]]
--separator=STR String to form horizontal separator on info line --separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator --no-separator Hide info line separator
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
@@ -124,15 +124,26 @@ const usage = `usage: fzf [options]
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
--version Display version information and exit --version Display version information and exit
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
--walker-root=DIR Root directory from which to start walker (default: .)
--walker-skip=DIRS Comma-separated list of directory names to skip
(default: .git,node_modules)
Shell integration
--bash Print script to set up Bash shell integration
--zsh Print script to set up Zsh shell integration
--fish Print script to set up Fish shell integration
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
(e.g. '--layout=reverse --inline-info') FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
FZF_API_KEY X-API-Key header for HTTP server (--listen) FZF_API_KEY X-API-Key header for HTTP server (--listen)
` `
const defaultInfoSep = " < " const defaultInfoPrefix = " < "
// Case denotes case-sensitivity of search // Case denotes case-sensitivity of search
type Case int type Case int
@@ -206,10 +217,6 @@ const (
infoHidden infoHidden
) )
func (s infoStyle) noExtraLine() bool {
return s == infoInline || s == infoInlineRight || s == infoHidden
}
type labelOpts struct { type labelOpts struct {
label string label string
column int column int
@@ -274,8 +281,18 @@ func firstLine(s string) string {
return strings.SplitN(s, "\n", 2)[0] return strings.SplitN(s, "\n", 2)[0]
} }
type walkerOpts struct {
file bool
dir bool
hidden bool
follow bool
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Bash bool
Zsh bool
Fish bool
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Scheme string Scheme string
@@ -306,7 +323,7 @@ type Options struct {
ScrollOff int ScrollOff int
FileWord bool FileWord bool
InfoStyle infoStyle InfoStyle infoStyle
InfoSep string InfoPrefix string
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
@@ -342,7 +359,24 @@ type Options struct {
ListenAddr *listenAddress ListenAddr *listenAddress
Unsafe bool Unsafe bool
ClearOnExit bool ClearOnExit bool
WalkerOpts walkerOpts
WalkerRoot string
WalkerSkip []string
Version bool Version bool
CPUProfile string
MEMProfile string
BlockProfile string
MutexProfile string
}
func filterNonEmpty(input []string) []string {
output := make([]string, 0, len(input))
for _, str := range input {
if len(str) > 0 {
output = append(output, str)
}
}
return output
} }
func defaultPreviewOpts(command string) previewOpts { func defaultPreviewOpts(command string) previewOpts {
@@ -351,6 +385,9 @@ func defaultPreviewOpts(command string) previewOpts {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Bash: false,
Zsh: false,
Fish: false,
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "default", Scheme: "default",
@@ -413,17 +450,22 @@ func defaultOptions() *Options {
PreviewLabel: labelOpts{}, PreviewLabel: labelOpts{},
Unsafe: false, Unsafe: false,
ClearOnExit: true, ClearOnExit: true,
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
WalkerRoot: ".",
WalkerSkip: []string{".git", "node_modules"},
Version: false} Version: false}
} }
func help(code int) { func help(code int) {
os.Stdout.WriteString(usage) os.Stdout.WriteString(usage)
os.Exit(code) util.Exit(code)
} }
var errorContext = ""
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n") os.Stderr.WriteString(errorContext + msg + "\n")
os.Exit(exitError) util.Exit(exitError)
} }
func optString(arg string, prefixes ...string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
@@ -628,8 +670,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.CtrlM) add(tui.CtrlM)
case "space": case "space":
chords[tui.Key(' ')] = key chords[tui.Key(' ')] = key
case "bspace", "bs": case "backspace", "bspace", "bs":
add(tui.BSpace) add(tui.Backspace)
case "ctrl-space": case "ctrl-space":
add(tui.CtrlSpace) add(tui.CtrlSpace)
case "ctrl-delete": case "ctrl-delete":
@@ -660,12 +702,16 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.One) add(tui.One)
case "zero": case "zero":
add(tui.Zero) add(tui.Zero)
case "jump":
add(tui.Jump)
case "jump-cancel":
add(tui.JumpCancel)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
chords[tui.AltKey(' ')] = key chords[tui.AltKey(' ')] = key
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace", "alt-backspace":
add(tui.AltBS) add(tui.AltBackspace)
case "alt-up": case "alt-up":
add(tui.AltUp) add(tui.AltUp)
case "alt-down": case "alt-down":
@@ -677,11 +723,11 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
case "tab": case "tab":
add(tui.Tab) add(tui.Tab)
case "btab", "shift-tab": case "btab", "shift-tab":
add(tui.BTab) add(tui.ShiftTab)
case "esc": case "esc":
add(tui.ESC) add(tui.Esc)
case "del": case "delete", "del":
add(tui.Del) add(tui.Delete)
case "home": case "home":
add(tui.Home) add(tui.Home)
case "end": case "end":
@@ -689,27 +735,27 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
case "insert": case "insert":
add(tui.Insert) add(tui.Insert)
case "pgup", "page-up": case "pgup", "page-up":
add(tui.PgUp) add(tui.PageUp)
case "pgdn", "page-down": case "pgdn", "page-down":
add(tui.PgDn) add(tui.PageDown)
case "alt-shift-up", "shift-alt-up": case "alt-shift-up", "shift-alt-up":
add(tui.AltSUp) add(tui.AltShiftUp)
case "alt-shift-down", "shift-alt-down": case "alt-shift-down", "shift-alt-down":
add(tui.AltSDown) add(tui.AltShiftDown)
case "alt-shift-left", "shift-alt-left": case "alt-shift-left", "shift-alt-left":
add(tui.AltSLeft) add(tui.AltShiftLeft)
case "alt-shift-right", "shift-alt-right": case "alt-shift-right", "shift-alt-right":
add(tui.AltSRight) add(tui.AltShiftRight)
case "shift-up": case "shift-up":
add(tui.SUp) add(tui.ShiftUp)
case "shift-down": case "shift-down":
add(tui.SDown) add(tui.ShiftDown)
case "shift-left": case "shift-left":
add(tui.SLeft) add(tui.ShiftLeft)
case "shift-right": case "shift-right":
add(tui.SRight) add(tui.ShiftRight)
case "shift-delete": case "shift-delete":
add(tui.SDelete) add(tui.ShiftDelete)
case "left-click": case "left-click":
add(tui.LeftClick) add(tui.LeftClick)
case "right-click": case "right-click":
@@ -964,6 +1010,30 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
return theme return theme
} }
func parseWalkerOpts(str string) walkerOpts {
opts := walkerOpts{}
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "file":
opts.file = true
case "dir":
opts.dir = true
case "hidden":
opts.hidden = true
case "follow":
opts.follow = true
case "":
// Ignored
default:
errorExit("invalid walker option: " + str)
}
}
if !opts.file && !opts.dir {
errorExit("at least one of 'file' or 'dir' should be specified")
}
return opts
}
var ( var (
executeRegexp *regexp.Regexp executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp splitRegexp *regexp.Regexp
@@ -1145,14 +1215,18 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleSearch) appendAction(actToggleSearch)
case "toggle-track": case "toggle-track":
appendAction(actToggleTrack) appendAction(actToggleTrack)
case "toggle-track-current":
appendAction(actToggleTrackCurrent)
case "toggle-header": case "toggle-header":
appendAction(actToggleHeader) appendAction(actToggleHeader)
case "show-header": case "show-header":
appendAction(actShowHeader) appendAction(actShowHeader)
case "hide-header": case "hide-header":
appendAction(actHideHeader) appendAction(actHideHeader)
case "track": case "track", "track-current":
appendAction(actTrack) appendAction(actTrackCurrent)
case "untrack-current":
appendAction(actUntrackCurrent)
case "select": case "select":
appendAction(actSelect) appendAction(actSelect)
case "select-all": case "select-all":
@@ -1436,17 +1510,24 @@ func parseInfoStyle(str string) (infoStyle, string) {
case "right": case "right":
return infoRight, "" return infoRight, ""
case "inline": case "inline":
return infoInline, defaultInfoSep return infoInline, defaultInfoPrefix
case "inline-right": case "inline-right":
return infoInlineRight, "" return infoInlineRight, ""
case "hidden": case "hidden":
return infoHidden, "" return infoHidden, ""
default: default:
prefix := "inline:" type infoSpec struct {
if strings.HasPrefix(str, prefix) { name string
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ") style infoStyle
} }
errorExit("invalid info style (expected: default|right|hidden|inline[:SEPARATOR]|inline-right)") for _, spec := range []infoSpec{
{"inline", infoInline},
{"inline-right", infoInlineRight}} {
if strings.HasPrefix(str, spec.name+":") {
return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " ")
}
}
errorExit("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])")
} }
return infoDefault, "" return infoDefault, ""
} }
@@ -1600,6 +1681,21 @@ func parseOptions(opts *Options, allArgs []string) {
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
case "--bash":
opts.Bash = true
if opts.Zsh || opts.Fish {
errorExit("cannot specify --bash with --zsh or --fish")
}
case "--zsh":
opts.Zsh = true
if opts.Bash || opts.Fish {
errorExit("cannot specify --zsh with --bash or --fish")
}
case "--fish":
opts.Fish = true
if opts.Bash || opts.Zsh {
errorExit("cannot specify --fish with --bash or --zsh")
}
case "-h", "--help": case "-h", "--help":
help(exitOk) help(exitOk)
case "-x", "--extended": case "-x", "--extended":
@@ -1722,13 +1818,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-filepath-word": case "--no-filepath-word":
opts.FileWord = false opts.FileWord = false
case "--info": case "--info":
opts.InfoStyle, opts.InfoSep = parseInfoStyle( opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(
nextString(allArgs, &i, "info style required")) nextString(allArgs, &i, "info style required"))
case "--no-info": case "--no-info":
opts.InfoStyle = infoHidden opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
opts.InfoStyle = infoInline opts.InfoStyle = infoInline
opts.InfoSep = defaultInfoSep opts.InfoPrefix = defaultInfoPrefix
case "--no-inline-info": case "--no-inline-info":
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
case "--separator": case "--separator":
@@ -1864,7 +1960,7 @@ func parseOptions(opts *Options, allArgs []string) {
addr := defaultListenAddr addr := defaultListenAddr
if given { if given {
var err error var err error
err, addr = parseListenAddress(str) addr, err = parseListenAddress(str)
if err != nil { if err != nil {
errorExit(err.Error()) errorExit(err.Error())
} }
@@ -1878,8 +1974,22 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
opts.ClearOnExit = false opts.ClearOnExit = false
case "--walker":
opts.WalkerOpts = parseWalkerOpts(nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]"))
case "--walker-root":
opts.WalkerRoot = nextString(allArgs, &i, "directory required")
case "--walker-skip":
opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ","))
case "--version": case "--version":
opts.Version = true opts.Version = true
case "--profile-cpu":
opts.CPUProfile = nextString(allArgs, &i, "file path required: cpu")
case "--profile-mem":
opts.MEMProfile = nextString(allArgs, &i, "file path required: mem")
case "--profile-block":
opts.BlockProfile = nextString(allArgs, &i, "file path required: block")
case "--profile-mutex":
opts.MutexProfile = nextString(allArgs, &i, "file path required: mutex")
case "--": case "--":
// Ignored // Ignored
default: default:
@@ -1924,7 +2034,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--layout="); match { } else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value) opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match { } else if match, value := optString(arg, "--info="); match {
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value) opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match { } else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match { } else if match, value := optString(arg, "--scrollbar="); match {
@@ -1962,19 +2072,25 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match { } else if match, value := optString(arg, "--listen="); match {
err, addr := parseListenAddress(value) addr, err := parseListenAddress(value)
if err != nil { if err != nil {
errorExit(err.Error()) errorExit(err.Error())
} }
opts.ListenAddr = &addr opts.ListenAddr = &addr
opts.Unsafe = false opts.Unsafe = false
} else if match, value := optString(arg, "--listen-unsafe="); match { } else if match, value := optString(arg, "--listen-unsafe="); match {
err, addr := parseListenAddress(value) addr, err := parseListenAddress(value)
if err != nil { if err != nil {
errorExit(err.Error()) errorExit(err.Error())
} }
opts.ListenAddr = &addr opts.ListenAddr = &addr
opts.Unsafe = true opts.Unsafe = true
} else if match, value := optString(arg, "--walker="); match {
opts.WalkerOpts = parseWalkerOpts(value)
} else if match, value := optString(arg, "--walker-root="); match {
opts.WalkerRoot = value
} else if match, value := optString(arg, "--walker-skip="); match {
opts.WalkerSkip = filterNonEmpty(strings.Split(value, ","))
} else if match, value := optString(arg, "--hscroll-off="); match { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match { } else if match, value := optString(arg, "--scroll-off="); match {
@@ -2143,9 +2259,7 @@ func postProcessOptions(opts *Options) {
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
if opts.Scheme != "default" { processScheme(opts)
processScheme(opts)
}
} }
func expectsArbitraryString(opt string) bool { func expectsArbitraryString(opt string) bool {
@@ -2167,15 +2281,43 @@ func ParseOptions() *Options {
} }
} }
// Options from Env var // 1. Options from $FZF_DEFAULT_OPTS_FILE
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" {
bytes, err := os.ReadFile(path)
if err != nil {
errorContext = "$FZF_DEFAULT_OPTS_FILE: "
errorExit(err.Error())
}
words, parseErr := shellwords.Parse(string(bytes))
if parseErr != nil {
errorContext = path + ": "
errorExit(parseErr.Error())
}
if len(words) > 0 {
parseOptions(opts, words)
}
}
// 2. Options from $FZF_DEFAULT_OPTS string
words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
errorContext = "$FZF_DEFAULT_OPTS: "
if parseErr != nil {
errorExit(parseErr.Error())
}
if len(words) > 0 { if len(words) > 0 {
parseOptions(opts, words) parseOptions(opts, words)
} }
// Options from command-line arguments // 3. Options from command-line arguments
errorContext = ""
parseOptions(opts, os.Args[1:]) parseOptions(opts, os.Args[1:])
if err := opts.initProfiling(); err != nil {
errorExit("failed to start pprof profiles: " + err.Error())
}
postProcessOptions(opts) postProcessOptions(opts)
return opts return opts
} }

11
src/options_no_pprof.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !pprof
// +build !pprof
package fzf
func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
errorExit("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
}
return nil
}

73
src/options_pprof.go Normal file
View File

@@ -0,0 +1,73 @@
//go:build pprof
// +build pprof
package fzf
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"github.com/junegunn/fzf/src/util"
)
func (o *Options) initProfiling() error {
if o.CPUProfile != "" {
f, err := os.Create(o.CPUProfile)
if err != nil {
return fmt.Errorf("could not create CPU profile: %w", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("could not start CPU profile: %w", err)
}
util.AtExit(func() {
pprof.StopCPUProfile()
if err := f.Close(); err != nil {
fmt.Fprintln(os.Stderr, "Error: closing cpu profile:", err)
}
})
}
stopProfile := func(name string, f *os.File) {
if err := pprof.Lookup(name).WriteTo(f, 0); err != nil {
fmt.Fprintf(os.Stderr, "Error: could not write %s profile: %v\n", name, err)
}
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Error: closing %s profile: %v\n", name, err)
}
}
if o.MEMProfile != "" {
f, err := os.Create(o.MEMProfile)
if err != nil {
return fmt.Errorf("could not create MEM profile: %w", err)
}
util.AtExit(func() {
runtime.GC()
stopProfile("allocs", f)
})
}
if o.BlockProfile != "" {
runtime.SetBlockProfileRate(1)
f, err := os.Create(o.BlockProfile)
if err != nil {
return fmt.Errorf("could not create BLOCK profile: %w", err)
}
util.AtExit(func() { stopProfile("block", f) })
}
if o.MutexProfile != "" {
runtime.SetMutexProfileFraction(1)
f, err := os.Create(o.MutexProfile)
if err != nil {
return fmt.Errorf("could not create MUTEX profile: %w", err)
}
util.AtExit(func() { stopProfile("mutex", f) })
}
return nil
}

89
src/options_pprof_test.go Normal file
View File

@@ -0,0 +1,89 @@
//go:build pprof
// +build pprof
package fzf
import (
"bytes"
"flag"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/junegunn/fzf/src/util"
)
// runInitProfileTests is an internal flag used TestInitProfiling
var runInitProfileTests = flag.Bool("test-init-profile", false, "run init profile tests")
func TestInitProfiling(t *testing.T) {
if testing.Short() {
t.Skip("short test")
}
// Run this test in a separate process since it interferes with
// profiling and modifies the global atexit state. Without this
// running `go test -bench . -cpuprofile cpu.out` will fail.
if !*runInitProfileTests {
t.Parallel()
// Make sure we are not the child process.
if os.Getenv("_FZF_CHILD_PROC") != "" {
t.Fatal("already running as child process!")
}
cmd := exec.Command(os.Args[0],
"-test.timeout", "30s",
"-test.run", "^"+t.Name()+"$",
"-test-init-profile",
)
cmd.Env = append(os.Environ(), "_FZF_CHILD_PROC=1")
out, err := cmd.CombinedOutput()
out = bytes.TrimSpace(out)
if err != nil {
t.Fatalf("Child test process failed: %v:\n%s", err, out)
}
// Make sure the test actually ran
if bytes.Contains(out, []byte("no tests to run")) {
t.Fatalf("Failed to run test %q:\n%s", t.Name(), out)
}
return
}
// Child process
tempdir := t.TempDir()
t.Cleanup(util.RunAtExitFuncs)
o := Options{
CPUProfile: filepath.Join(tempdir, "cpu.prof"),
MEMProfile: filepath.Join(tempdir, "mem.prof"),
BlockProfile: filepath.Join(tempdir, "block.prof"),
MutexProfile: filepath.Join(tempdir, "mutex.prof"),
}
if err := o.initProfiling(); err != nil {
t.Fatal(err)
}
profiles := []string{
o.CPUProfile,
o.MEMProfile,
o.BlockProfile,
o.MutexProfile,
}
for _, name := range profiles {
if _, err := os.Stat(name); err != nil {
t.Errorf("Failed to create profile %s: %v", filepath.Base(name), err)
}
}
util.RunAtExitFuncs()
for _, name := range profiles {
if _, err := os.Stat(name); err != nil {
t.Errorf("Failed to write profile %s: %v", filepath.Base(name), err)
}
}
}

View File

@@ -170,8 +170,8 @@ func TestParseKeys(t *testing.T) {
check(tui.CtrlM, "Return") check(tui.CtrlM, "Return")
checkEvent(tui.Key(' '), "space") checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab") check(tui.Tab, "tab")
check(tui.BTab, "btab") check(tui.ShiftTab, "btab")
check(tui.ESC, "esc") check(tui.Esc, "esc")
check(tui.Up, "up") check(tui.Up, "up")
check(tui.Down, "down") check(tui.Down, "down")
check(tui.Left, "left") check(tui.Left, "left")
@@ -182,16 +182,16 @@ func TestParseKeys(t *testing.T) {
t.Error(11) t.Error(11)
} }
check(tui.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(tui.PgUp, "page-up") check(tui.PageUp, "page-up")
check(tui.PgDn, "Page-Down") check(tui.PageDown, "Page-Down")
check(tui.Home, "Home") check(tui.Home, "Home")
check(tui.End, "End") check(tui.End, "End")
check(tui.AltBS, "Alt-BSpace") check(tui.AltBackspace, "Alt-BSpace")
check(tui.SLeft, "shift-left") check(tui.ShiftLeft, "shift-left")
check(tui.SRight, "shift-right") check(tui.ShiftRight, "shift-right")
check(tui.BTab, "shift-tab") check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(tui.BSpace, "bspace") check(tui.Backspace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {

View File

@@ -209,11 +209,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// Flip exactness // Flip exactness
if fuzzy && !inv { if fuzzy && !inv {
typ = termExact typ = termExact
text = text[1:]
} else { } else {
typ = termFuzzy typ = termFuzzy
text = text[1:]
} }
text = text[1:]
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
if typ == termSuffix { if typ == termSuffix {
typ = termEqual typ = termEqual

View File

@@ -1,19 +1,18 @@
package fzf package fzf
import ( import (
"bufio" "bytes"
"context" "context"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/charlievieth/fastwalk"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/saracen/walker"
) )
// Reader reads from command or standard input // Reader reads from command or standard input
@@ -77,48 +76,33 @@ func (r *Reader) fin(success bool) {
func (r *Reader) terminate() { func (r *Reader) terminate() {
r.mutex.Lock() r.mutex.Lock()
defer func() { r.mutex.Unlock() }()
r.killed = true r.killed = true
if r.exec != nil && r.exec.Process != nil { if r.exec != nil && r.exec.Process != nil {
util.KillCommand(r.exec) util.KillCommand(r.exec)
} else if defaultCommand != "" { } else {
os.Stdin.Close() os.Stdin.Close()
} }
r.mutex.Unlock()
} }
func (r *Reader) restart(command string) { func (r *Reader) restart(command string, environ []string) {
r.event = int32(EvtReady) r.event = int32(EvtReady)
r.startEventPoller() r.startEventPoller()
success := r.readFromCommand(nil, command) success := r.readFromCommand(command, environ)
r.fin(success) r.fin(success)
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource() { func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if util.IsTty() {
// The default command for *nix requires a shell that supports "pipefail"
// https://unix.stackexchange.com/a/654932/62171
shell := "bash"
currentShell := os.Getenv("SHELL")
currentShellName := path.Base(currentShell)
for _, shellName := range []string{"bash", "zsh", "ksh", "ash", "hush", "mksh", "yash"} {
if currentShellName == shellName {
shell = currentShell
break
}
}
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
if defaultCommand != "" { success = r.readFiles(root, opts, ignores)
success = r.readFromCommand(&shell, defaultCommand)
} else {
success = r.readFiles()
}
} else { } else {
success = r.readFromCommand(nil, cmd) // We can't export FZF_* environment variables to the default command
success = r.readFromCommand(cmd, nil)
} }
} else { } else {
success = r.readFromStdin() success = r.readFromStdin()
@@ -127,32 +111,90 @@ func (r *Reader) ReadSource() {
} }
func (r *Reader) feed(src io.Reader) { func (r *Reader) feed(src io.Reader) {
/*
readerSlabSize, ae := strconv.Atoi(os.Getenv("SLAB_KB"))
if ae != nil {
readerSlabSize = 128 * 1024
} else {
readerSlabSize *= 1024
}
readerBufferSize, be := strconv.Atoi(os.Getenv("BUF_KB"))
if be != nil {
readerBufferSize = 64 * 1024
} else {
readerBufferSize *= 1024
}
*/
delim := byte('\n') delim := byte('\n')
trimCR := util.IsWindows()
if r.delimNil { if r.delimNil {
delim = '\000' delim = '\000'
trimCR = false
} }
reader := bufio.NewReaderSize(src, readerBufferSize)
slab := make([]byte, readerSlabSize)
leftover := []byte{}
var err error
for { for {
// ReadBytes returns err != nil if and only if the returned data does not n := 0
// end in delim. scope := slab[:util.Min(len(slab), readerBufferSize)]
bytea, err := reader.ReadBytes(delim) for i := 0; i < 100; i++ {
byteaLen := len(bytea) n, err = src.Read(scope)
if byteaLen > 0 { if n > 0 || err != nil {
if err == nil { break
// get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
bytea = bytea[:byteaLen-2]
} else {
bytea = bytea[:byteaLen-1]
}
}
if r.pusher(bytea) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
} }
} }
if err != nil {
// We're not making any progress after 100 tries. Stop.
if n == 0 && err == nil {
break break
} }
buf := slab[:n]
slab = slab[n:]
for len(buf) > 0 {
if i := bytes.IndexByte(buf, delim); i >= 0 {
// Found the delimiter
slice := buf[:i+1]
buf = buf[i+1:]
if trimCR && len(slice) >= 2 && slice[len(slice)-2] == byte('\r') {
slice = slice[:len(slice)-2]
} else {
slice = slice[:len(slice)-1]
}
if len(leftover) > 0 {
slice = append(leftover, slice...)
leftover = []byte{}
}
if (err == nil || len(slice) > 0) && r.pusher(slice) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
} else {
// Could not find the delimiter in the buffer
// NOTE: We can further optimize this by keeping track of the cursor
// position in the slab so that a straddling item that doesn't go
// beyond the boundary of a slab doesn't need to be copied to
// another buffer. However, the performance gain is negligible in
// practice (< 0.1%) and is not
// worth the added complexity.
leftover = append(leftover, buf...)
break
}
}
if err == io.EOF {
leftover = append(leftover, buf...)
break
}
if len(slab) == 0 {
slab = make([]byte, readerSlabSize)
}
}
if len(leftover) > 0 && r.pusher(leftover) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
} }
} }
@@ -161,16 +203,28 @@ func (r *Reader) readFromStdin() bool {
return true return true
} }
func (r *Reader) readFiles() bool { func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
r.killed = false r.killed = false
fn := func(path string, mode os.FileInfo) error { conf := fastwalk.Config{Follow: opts.follow}
fn := func(path string, de os.DirEntry, err error) error {
if err != nil {
return nil
}
path = filepath.Clean(path) path = filepath.Clean(path)
if path != "." { if path != "." {
isDir := mode.Mode().IsDir() isDir := de.IsDir()
if isDir && filepath.Base(path)[0] == '.' { if isDir {
return filepath.SkipDir base := filepath.Base(path)
if !opts.hidden && base[0] == '.' {
return filepath.SkipDir
}
for _, ignore := range ignores {
if ignore == base {
return filepath.SkipDir
}
}
} }
if !isDir && r.pusher([]byte(path)) { if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew)) atomic.StoreInt32(&r.event, int32(EvtReadNew))
} }
} }
@@ -181,20 +235,16 @@ func (r *Reader) readFiles() bool {
} }
return nil return nil
} }
cb := walker.WithErrorCallback(func(pathname string, err error) error { return fastwalk.Walk(&conf, root, fn) == nil
return nil
})
return walker.Walk(".", fn, cb) == nil
} }
func (r *Reader) readFromCommand(shell *string, command string) bool { func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock() r.mutex.Lock()
r.killed = false r.killed = false
r.command = &command r.command = &command
if shell != nil { r.exec = util.ExecCommand(command, true)
r.exec = util.ExecCommandWith(*shell, command, true) if environ != nil {
} else { r.exec.Env = environ
r.exec = util.ExecCommand(command, true)
} }
out, err := r.exec.StdoutPipe() out, err := r.exec.StdoutPipe()
if err != nil { if err != nil {

View File

@@ -22,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Normal command // Normal command
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`)) reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs) t.Errorf("%s", strs)
} }
@@ -47,7 +47,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller() reader.startEventPoller()
// Failing command // Failing command
reader.fin(reader.readFromCommand(nil, `no-such-command`)) reader.fin(reader.readFromCommand(`no-such-command`, nil))
strs = []string{} strs = []string{}
if len(strs) > 0 { if len(strs) > 0 {
t.Errorf("%s", strs) t.Errorf("%s", strs)

View File

@@ -32,6 +32,7 @@ const (
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
httpReadTimeout = 10 * time.Second httpReadTimeout = 10 * time.Second
channelTimeout = 2 * time.Second
jsonContentType = "Content-Type: application/json" + crlf jsonContentType = "Content-Type: application/json" + crlf
maxContentLength = 1024 * 1024 maxContentLength = 1024 * 1024
) )
@@ -53,47 +54,47 @@ func (addr listenAddress) IsLocal() bool {
var defaultListenAddr = listenAddress{"localhost", 0} var defaultListenAddr = listenAddress{"localhost", 0}
func parseListenAddress(address string) (error, listenAddress) { func parseListenAddress(address string) (listenAddress, error) {
parts := strings.SplitN(address, ":", 3) parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 { if len(parts) == 1 {
parts = []string{"localhost", parts[0]} parts = []string{"localhost", parts[0]}
} }
if len(parts) != 2 { if len(parts) != 2 {
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
} }
portStr := parts[len(parts)-1] portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 { if err != nil || port < 0 || port > 65535 {
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
} }
if len(parts[0]) == 0 { if len(parts[0]) == 0 {
parts[0] = "localhost" parts[0] = "localhost"
} }
return nil, listenAddress{parts[0], port} return listenAddress{parts[0], port}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) { func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
host := address.host host := address.host
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port return port, fmt.Errorf("FZF_API_KEY is required to allow remote access")
} }
addrStr := fmt.Sprintf("%s:%d", host, port) addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr) listener, err := net.Listen("tcp", addrStr)
if err != nil { if err != nil {
return fmt.Errorf("failed to listen on %s", addrStr), port return port, fmt.Errorf("failed to listen on %s", addrStr)
} }
if port == 0 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()
parts := strings.Split(addr, ":") parts := strings.Split(addr, ":")
if len(parts) < 2 { if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port return port, fmt.Errorf("cannot extract port: %s", addr)
} }
var err error var err error
port, err = strconv.Atoi(parts[len(parts)-1]) port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil { if err != nil {
return err, port return port, err
} }
} }
@@ -119,7 +120,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
listener.Close() listener.Close()
}() }()
return nil, port return port, nil
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http
@@ -170,7 +171,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
select { select {
case response := <-server.responseChannel: case response := <-server.responseChannel:
return good(response) return good(response)
case <-time.After(2 * time.Second): case <-time.After(channelTimeout):
go func() { go func() {
// Drain the channel // Drain the channel
<-server.responseChannel <-server.responseChannel
@@ -227,7 +228,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
return bad("no action specified") return bad("no action specified")
} }
server.actionChannel <- actions select {
case server.actionChannel <- actions:
case <-time.After(channelTimeout):
return httpUnavailable + crlf
}
return httpOk + crlf return httpOk + crlf
} }
@@ -237,15 +242,13 @@ func parseGetParams(query string) getParams {
parts := strings.SplitN(pair, "=", 2) parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 { if len(parts) == 2 {
switch parts[0] { switch parts[0] {
case "limit": case "limit", "offset":
val, err := strconv.Atoi(parts[1]) if val, err := strconv.Atoi(parts[1]); err == nil {
if err == nil { if parts[0] == "limit" {
params.limit = val params.limit = val
} } else {
case "offset": params.offset = val
val, err := strconv.Atoi(parts[1]) }
if err == nil {
params.offset = val
} }
} }
} }

View File

@@ -55,6 +55,9 @@ var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
// Number of maximum focus events to process synchronously
const maxFocusEvents = 10000
func init() { func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`) placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
@@ -67,7 +70,7 @@ func init() {
// * https://sw.kovidgoyal.net/kitty/graphics-protocol // * https://sw.kovidgoyal.net/kitty/graphics-protocol
// * https://en.wikipedia.org/wiki/Sixel // * https://en.wikipedia.org/wiki/Sixel
// * https://iterm2.com/documentation-images.html // * https://iterm2.com/documentation-images.html
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?\a`) passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?(\a|\x1b\\)`)
} }
type jumpMode int type jumpMode int
@@ -178,7 +181,7 @@ type Status struct {
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoStyle infoStyle infoStyle infoStyle
infoSep string infoPrefix string
separator labelPrinter separator labelPrinter
separatorLen int separatorLen int
spinner []string spinner []string
@@ -290,6 +293,7 @@ type Terminal struct {
executing *util.AtomicBool executing *util.AtomicBool
termSize tui.TermSize termSize tui.TermSize
lastAction actionType lastAction actionType
lastKey string
lastFocus int32 lastFocus int32
areaLines int areaLines int
areaColumns int areaColumns int
@@ -391,8 +395,10 @@ const (
actToggleIn actToggleIn
actToggleOut actToggleOut
actToggleTrack actToggleTrack
actToggleTrackCurrent
actToggleHeader actToggleHeader
actTrack actTrackCurrent
actUntrackCurrent
actDown actDown
actUp actUp
actPageUp actPageUp
@@ -403,7 +409,7 @@ const (
actOffsetUp actOffsetUp
actOffsetDown actOffsetDown
actJump actJump
actJumpAccept actJumpAccept // XXX Deprecated in favor of jump:accept binding
actPrintQuery actPrintQuery
actRefreshPreview actRefreshPreview
actReplaceQuery actReplaceQuery
@@ -455,14 +461,7 @@ const (
) )
func (a actionType) Name() string { func (a actionType) Name() string {
name := "" return util.ToKebabCase(a.String()[3:])
for i, r := range a.String()[3:] {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
} }
func processExecution(action actionType) bool { func processExecution(action actionType) bool {
@@ -499,6 +498,7 @@ type searchRequest struct {
sort bool sort bool
sync bool sync bool
command *string command *string
environ []string
changed bool changed bool
} }
@@ -540,14 +540,14 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.CtrlC, actAbort) add(tui.CtrlC, actAbort)
add(tui.CtrlG, actAbort) add(tui.CtrlG, actAbort)
add(tui.CtrlQ, actAbort) add(tui.CtrlQ, actAbort)
add(tui.ESC, actAbort) add(tui.Esc, actAbort)
add(tui.CtrlD, actDeleteCharEof) add(tui.CtrlD, actDeleteCharEof)
add(tui.CtrlE, actEndOfLine) add(tui.CtrlE, actEndOfLine)
add(tui.CtrlF, actForwardChar) add(tui.CtrlF, actForwardChar)
add(tui.CtrlH, actBackwardDeleteChar) add(tui.CtrlH, actBackwardDeleteChar)
add(tui.BSpace, actBackwardDeleteChar) add(tui.Backspace, actBackwardDeleteChar)
add(tui.Tab, actToggleDown) add(tui.Tab, actToggleDown)
add(tui.BTab, actToggleUp) add(tui.ShiftTab, actToggleUp)
add(tui.CtrlJ, actDown) add(tui.CtrlJ, actDown)
add(tui.CtrlK, actUp) add(tui.CtrlK, actUp)
add(tui.CtrlL, actClearScreen) add(tui.CtrlL, actClearScreen)
@@ -562,11 +562,11 @@ func defaultKeymap() map[tui.Event][]*action {
} }
addEvent(tui.AltKey('b'), actBackwardWord) addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.SLeft, actBackwardWord) add(tui.ShiftLeft, actBackwardWord)
addEvent(tui.AltKey('f'), actForwardWord) addEvent(tui.AltKey('f'), actForwardWord)
add(tui.SRight, actForwardWord) add(tui.ShiftRight, actForwardWord)
addEvent(tui.AltKey('d'), actKillWord) addEvent(tui.AltKey('d'), actKillWord)
add(tui.AltBS, actBackwardKillWord) add(tui.AltBackspace, actBackwardKillWord)
add(tui.Up, actUp) add(tui.Up, actUp)
add(tui.Down, actDown) add(tui.Down, actDown)
@@ -575,12 +575,12 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.Home, actBeginningOfLine) add(tui.Home, actBeginningOfLine)
add(tui.End, actEndOfLine) add(tui.End, actEndOfLine)
add(tui.Del, actDeleteChar) add(tui.Delete, actDeleteChar)
add(tui.PgUp, actPageUp) add(tui.PageUp, actPageUp)
add(tui.PgDn, actPageDown) add(tui.PageDown, actPageDown)
add(tui.SUp, actPreviewUp) add(tui.ShiftUp, actPreviewUp)
add(tui.SDown, actPreviewDown) add(tui.ShiftDown, actPreviewDown)
add(tui.Mouse, actMouse) add(tui.Mouse, actMouse)
add(tui.LeftClick, actClick) add(tui.LeftClick, actClick)
@@ -671,7 +671,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if previewBox != nil && opts.Preview.aboveOrBelow() { if previewBox != nil && opts.Preview.aboveOrBelow() {
effectiveMinHeight += 1 + borderLines(opts.Preview.border) effectiveMinHeight += 1 + borderLines(opts.Preview.border)
} }
if opts.InfoStyle.noExtraLine() { if noSeparatorLine(opts.InfoStyle, opts.Separator == nil || uniseg.StringWidth(*opts.Separator) > 0) {
effectiveMinHeight-- effectiveMinHeight--
} }
effectiveMinHeight += borderLines(opts.BorderShape) effectiveMinHeight += borderLines(opts.BorderShape)
@@ -693,7 +693,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
infoSep: opts.InfoSep, infoPrefix: opts.InfoPrefix,
separator: nil, separator: nil,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
promptString: opts.Prompt, promptString: opts.Prompt,
@@ -772,7 +772,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
theme: opts.Theme, theme: opts.Theme,
startChan: make(chan fitpad, 1), startChan: make(chan fitpad, 1),
killChan: make(chan int), killChan: make(chan int),
serverInputChan: make(chan []*action, 10), serverInputChan: make(chan []*action, 100),
serverOutputChan: make(chan string), serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar) eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
tui: renderer, tui: renderer,
@@ -828,7 +828,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenAddr != nil { if t.listenAddr != nil {
err, port := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan) port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil { if err != nil {
errorExit(err.Error()) errorExit(err.Error())
} }
@@ -845,7 +845,10 @@ func (t *Terminal) environ() []string {
} }
env = append(env, "FZF_QUERY="+string(t.input)) env = append(env, "FZF_QUERY="+string(t.input))
env = append(env, "FZF_ACTION="+t.lastAction.Name()) env = append(env, "FZF_ACTION="+t.lastAction.Name())
env = append(env, "FZF_KEY="+t.lastKey)
env = append(env, "FZF_PROMPT="+string(t.promptString)) env = append(env, "FZF_PROMPT="+string(t.promptString))
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count)) env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length())) env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected))) env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
@@ -874,7 +877,7 @@ func (t *Terminal) visibleHeaderLines() int {
// Extra number of lines needed to display fzf // Extra number of lines needed to display fzf
func (t *Terminal) extraLines() int { func (t *Terminal) extraLines() int {
extra := t.visibleHeaderLines() + 1 extra := t.visibleHeaderLines() + 1
if !t.noInfoLine() { if !t.noSeparatorLine() {
extra++ extra++
} }
return extra return extra
@@ -980,8 +983,18 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
return output, promptLen return output, promptLen
} }
func (t *Terminal) noInfoLine() bool { func noSeparatorLine(style infoStyle, separator bool) bool {
return t.infoStyle.noExtraLine() switch style {
case infoInline:
return true
case infoHidden, infoInlineRight:
return !separator
}
return false
}
func (t *Terminal) noSeparatorLine() bool {
return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
} }
func getScrollbar(total int, height int, offset int) (int, int) { func getScrollbar(total int, height int, offset int) (int, int) {
@@ -1056,11 +1069,11 @@ func (t *Terminal) UpdateProgress(progress float32) {
// UpdateList updates Merger to display the list // UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) { func (t *Terminal) UpdateList(merger *Merger) {
t.mutex.Lock() t.mutex.Lock()
var prevIndex int32 = -1 prevIndex := minItem.Index()
reset := t.revision != merger.Revision() reset := t.revision != merger.Revision()
if !reset && t.track != trackDisabled { if !reset && t.track != trackDisabled {
if t.merger.Length() > 0 { if t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index() prevIndex = t.currentIndex()
} else if merger.Length() > 0 { } else if merger.Length() > 0 {
prevIndex = merger.First().item.Index() prevIndex = merger.First().item.Index()
} }
@@ -1233,7 +1246,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
minAreaWidth := minWidth minAreaWidth := minWidth
minAreaHeight := minHeight minAreaHeight := minHeight
if t.noInfoLine() { if t.noSeparatorLine() {
minAreaHeight -= 1 minAreaHeight -= 1
} }
if t.needPreviewWindow() { if t.needPreviewWindow() {
@@ -1514,7 +1527,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
y = h - y - 1 y = h - y - 1
case layoutReverseList: case layoutReverseList:
n := 2 + t.visibleHeaderLines() n := 2 + t.visibleHeaderLines()
if t.noInfoLine() { if t.noSeparatorLine() {
n-- n--
} }
if y < n { if y < n {
@@ -1554,7 +1567,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
func (t *Terminal) promptLine() int { func (t *Terminal) promptLine() int {
if t.headerFirst { if t.headerFirst {
max := t.window.Height() - 1 max := t.window.Height() - 1
if !t.noInfoLine() { if !t.noSeparatorLine() {
max-- max--
} }
return util.Min(t.visibleHeaderLines(), max) return util.Min(t.visibleHeaderLines(), max)
@@ -1599,20 +1612,8 @@ func (t *Terminal) printInfo() {
t.window.Print(" ") // Clear spinner t.window.Print(" ") // Clear spinner
} }
} }
switch t.infoStyle { printInfoPrefix := func() {
case infoDefault: str := t.infoPrefix
t.move(line+1, 0, t.separatorLen == 0)
printSpinner()
t.move(line+1, 2, false)
pos = 2
case infoRight:
t.move(line+1, 0, false)
case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
t.move(line, pos, true)
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
str := t.infoSep
maxWidth := t.window.Width() - pos maxWidth := t.window.Width() - pos
width := util.StringWidth(str) width := util.StringWidth(str)
if width > maxWidth { if width > maxWidth {
@@ -1627,7 +1628,34 @@ func (t *Terminal) printInfo() {
t.window.CPrint(tui.ColPrompt, str) t.window.CPrint(tui.ColPrompt, str)
} }
pos += width pos += width
}
printSeparator := func(fillLength int, pad bool) {
// --------_
if t.separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
} else if pad {
t.window.Print(strings.Repeat(" ", fillLength+1))
}
}
switch t.infoStyle {
case infoDefault:
t.move(line+1, 0, t.separatorLen == 0)
printSpinner()
t.move(line+1, 2, false)
pos = 2
case infoRight:
t.move(line+1, 0, false)
case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
printInfoPrefix()
case infoHidden: case infoHidden:
if t.separatorLen > 0 {
t.move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return return
} }
@@ -1641,8 +1669,11 @@ func (t *Terminal) printInfo() {
output += " -S" output += " -S"
} }
} }
if t.track != trackDisabled { switch t.track {
case trackEnabled:
output += " +T" output += " +T"
case trackCurrent:
output += " +t"
} }
if t.multi > 0 { if t.multi > 0 {
if t.multi == maxMulti { if t.multi == maxMulti {
@@ -1658,15 +1689,6 @@ func (t *Terminal) printInfo() {
output = fmt.Sprintf("[Command failed: %s]", *t.failed) output = fmt.Sprintf("[Command failed: %s]", *t.failed)
} }
printSeparator := func(fillLength int, pad bool) {
// --------_
if t.separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
} else if pad {
t.window.Print(strings.Repeat(" ", fillLength+1))
}
}
if t.infoStyle == infoRight { if t.infoStyle == infoRight {
maxWidth := t.window.Width() maxWidth := t.window.Width()
if t.reading { if t.reading {
@@ -1689,19 +1711,35 @@ func (t *Terminal) printInfo() {
} }
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3) if len(t.infoPrefix) == 0 {
if pos >= t.window.Width() { pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
return if pos < t.window.Width() {
t.move(line, pos, false)
printSpinner()
pos++
}
if pos < t.window.Width()-1 {
t.window.Print(" ")
pos++
}
} else {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-util.StringWidth(t.infoPrefix)-1)
printInfoPrefix()
} }
t.move(line, pos, false)
printSpinner()
t.window.Print(" ")
pos += 2
} }
maxWidth := t.window.Width() - pos maxWidth := t.window.Width() - pos
output = t.trimMessage(output, maxWidth) output = t.trimMessage(output, maxWidth)
t.window.CPrint(tui.ColInfo, output) t.window.CPrint(tui.ColInfo, output)
if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 {
t.move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return
}
fillLength := maxWidth - len(output) - 2 fillLength := maxWidth - len(output) - 2
if fillLength > 0 { if fillLength > 0 {
t.window.CPrint(tui.ColSeparator, " ") t.window.CPrint(tui.ColSeparator, " ")
@@ -1716,7 +1754,7 @@ func (t *Terminal) printHeader() {
max := t.window.Height() max := t.window.Height()
if t.headerFirst { if t.headerFirst {
max-- max--
if !t.noInfoLine() { if !t.noSeparatorLine() {
max-- max--
} }
} }
@@ -1733,7 +1771,7 @@ func (t *Terminal) printHeader() {
} }
if !t.headerFirst { if !t.headerFirst {
line++ line++
if !t.noInfoLine() { if !t.noSeparatorLine() {
line++ line++
} }
} }
@@ -1765,7 +1803,7 @@ func (t *Terminal) printList() {
i = maxy - 1 - j i = maxy - 1 - j
} }
line := i + 2 + t.visibleHeaderLines() line := i + 2 + t.visibleHeaderLines()
if t.noInfoLine() { if t.noSeparatorLine() {
line-- line--
} }
if i < count { if i < count {
@@ -3091,7 +3129,7 @@ func (t *Terminal) Loop() {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.noInfoLine() { if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
t.printInfo() t.printInfo()
} }
case reqInfo: case reqInfo:
@@ -3160,7 +3198,7 @@ func (t *Terminal) Loop() {
} }
t.previewer.lines = result.lines t.previewer.lines = result.lines
t.previewer.spinner = result.spinner t.previewer.spinner = result.spinner
if t.previewer.following.Enabled() { if t.hasPreviewWindow() && t.previewer.following.Enabled() {
t.previewer.offset = util.Max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.previewOpts.headerLines)) t.previewer.offset = util.Max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.previewOpts.headerLines))
} else if result.offset >= 0 { } else if result.offset >= 0 {
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1) t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
@@ -3247,6 +3285,7 @@ func (t *Terminal) Loop() {
t.mutex.Lock() t.mutex.Lock()
previousInput := t.input previousInput := t.input
previousCx := t.cx previousCx := t.cx
t.lastKey = event.KeyName()
events := []util.EventType{} events := []util.EventType{}
req := func(evts ...util.EventType) { req := func(evts ...util.EventType) {
for _, event := range evts { for _, event := range evts {
@@ -3304,18 +3343,22 @@ func (t *Terminal) Loop() {
var doAction func(*action) bool var doAction func(*action) bool
var doActions func(actions []*action) bool var doActions func(actions []*action) bool
doActions = func(actions []*action) bool { doActions = func(actions []*action) bool {
currentIndex := t.currentIndex() for iter := 0; iter <= maxFocusEvents; iter++ {
for _, action := range actions { currentIndex := t.currentIndex()
if !doAction(action) { for _, action := range actions {
return false if !doAction(action) {
return false
}
} }
}
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs { if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents {
if newIndex := t.currentIndex(); newIndex != currentIndex { if newIndex := t.currentIndex(); newIndex != currentIndex {
t.lastFocus = newIndex t.lastFocus = newIndex
return doActions(onFocus) actions = onFocus
continue
}
} }
break
} }
return true return true
} }
@@ -3381,6 +3424,9 @@ func (t *Terminal) Loop() {
// Discard the preview content so that it won't accidentally appear // Discard the preview content so that it won't accidentally appear
// when preview window is re-enabled and previewDelay is triggered // when preview window is re-enabled and previewDelay is triggered
t.previewer.lines = nil t.previewer.lines = nil
// Also kill the preview process if it's still running
t.cancelPreview()
} }
} }
case actTogglePreviewWrap: case actTogglePreviewWrap:
@@ -3460,11 +3506,13 @@ func (t *Terminal) Loop() {
req(reqHeader) req(reqHeader)
} }
case actChangeBorderLabel: case actChangeBorderLabel:
t.borderLabelOpts.label = a.a
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actChangePreviewLabel: case actChangePreviewLabel:
t.previewLabelOpts.label = a.a
if t.pborder != nil { if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
@@ -3474,14 +3522,16 @@ func (t *Terminal) Loop() {
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {}) actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
return doActions(actions) return doActions(actions)
case actTransformBorderLabel: case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true)
t.borderLabelOpts.label = label
if t.border != nil { if t.border != nil {
label := t.executeCommand(a.a, false, true, true, true)
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actTransformPreviewLabel: case actTransformPreviewLabel:
label := t.executeCommand(a.a, false, true, true, true)
t.previewLabelOpts.label = label
if t.pborder != nil { if t.pborder != nil {
label := t.executeCommand(a.a, false, true, true, true)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
@@ -3767,6 +3817,14 @@ func (t *Terminal) Loop() {
t.track = trackEnabled t.track = trackEnabled
} }
req(reqInfo) req(reqInfo)
case actToggleTrackCurrent:
switch t.track {
case trackCurrent:
t.track = trackDisabled
case trackDisabled:
t.track = trackCurrent
}
req(reqInfo)
case actShowHeader: case actShowHeader:
t.headerVisible = true t.headerVisible = true
req(reqList, reqInfo, reqPrompt, reqHeader) req(reqList, reqInfo, reqPrompt, reqHeader)
@@ -3776,11 +3834,16 @@ func (t *Terminal) Loop() {
case actToggleHeader: case actToggleHeader:
t.headerVisible = !t.headerVisible t.headerVisible = !t.headerVisible
req(reqList, reqInfo, reqPrompt, reqHeader) req(reqList, reqInfo, reqPrompt, reqHeader)
case actTrack: case actTrackCurrent:
if t.track == trackDisabled { if t.track == trackDisabled {
t.track = trackCurrent t.track = trackCurrent
} }
req(reqInfo) req(reqInfo)
case actUntrackCurrent:
if t.track == trackCurrent {
t.track = trackDisabled
}
req(reqInfo)
case actEnableSearch: case actEnableSearch:
t.paused = false t.paused = false
changed = true changed = true
@@ -3869,7 +3932,7 @@ func (t *Terminal) Loop() {
mx -= t.window.Left() mx -= t.window.Left()
my -= t.window.Top() my -= t.window.Top()
min := 2 + t.visibleHeaderLines() min := 2 + t.visibleHeaderLines()
if t.noInfoLine() { if t.noSeparatorLine() {
min-- min--
} }
h := t.window.Height() h := t.window.Height()
@@ -3987,11 +4050,18 @@ func (t *Terminal) Loop() {
// Full redraw // Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) { if !currentPreviewOpts.sameLayout(t.previewOpts) {
wasHidden := t.pwindow == nil // Preview command can be running in the background if the size of
// the preview window is 0 but not 'hidden'
wasHidden := currentPreviewOpts.hidden
updatePreviewWindow(false) updatePreviewWindow(false)
if wasHidden && t.hasPreviewWindow() { if wasHidden && t.hasPreviewWindow() {
// Restart
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
} else if t.previewOpts.hidden {
// Cancel
t.cancelPreview()
} else { } else {
// Refresh
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) { } else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
@@ -4034,6 +4104,9 @@ func (t *Terminal) Loop() {
// Break out of jump mode if any action is submitted to the server // Break out of jump mode if any action is submitted to the server
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
t.jumping = jumpDisabled t.jumping = jumpDisabled
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList) req(reqList)
} }
if len(actions) == 0 { if len(actions) == 0 {
@@ -4047,19 +4120,17 @@ func (t *Terminal) Loop() {
t.truncateQuery() t.truncateQuery()
queryChanged = string(previousInput) != string(t.input) queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs { if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
if !doActions(onChanges) { continue
continue
}
} }
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs { if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
if !doActions(onEOFs) { continue
continue
}
} }
} else { } else {
jumpEvent := tui.JumpCancel
if event.Type == tui.Rune { if event.Type == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
jumpEvent = tui.Jump
t.cy = idx + t.offset t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled { if t.jumping == jumpAcceptEnabled {
req(reqClose) req(reqClose)
@@ -4067,6 +4138,9 @@ func (t *Terminal) Loop() {
} }
} }
t.jumping = jumpDisabled t.jumping = jumpDisabled
if acts, prs := t.keymap[jumpEvent.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList) req(reqList)
} }
@@ -4081,10 +4155,15 @@ func (t *Terminal) Loop() {
req(reqPrompt) req(reqPrompt)
} }
reload := changed || newCommand != nil
var reloadRequest *searchRequest
if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, environ: t.environ(), changed: changed}
}
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed || newCommand != nil { if reload {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed}) t.eventBox.Set(EvtSearchNew, *reloadRequest)
} }
for _, event := range events { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)
@@ -4098,7 +4177,7 @@ func (t *Terminal) constrain() {
// count of lines can be displayed // count of lines can be displayed
height := t.maxItems() height := t.maxItems()
t.cy = util.Constrain(t.cy, 0, count-1) t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
minOffset := util.Max(t.cy-height+1, 0) minOffset := util.Max(t.cy-height+1, 0)
maxOffset := util.Max(util.Min(count-height, t.cy), 0) maxOffset := util.Max(util.Min(count-height, t.cy), 0)
@@ -4149,7 +4228,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - t.visibleHeaderLines() max := t.window.Height() - 2 - t.visibleHeaderLines()
if t.noInfoLine() { if t.noSeparatorLine() {
max++ max++
} }
return util.Max(max, 0) return util.Max(max, 0)

View File

@@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
prefixLength := begin prefixLength := begin
for idx := range tokens { for idx := range tokens {
chars := util.ToChars([]byte(tokens[idx])) chars := util.ToChars(sbytes(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)} ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length() prefixLength += chars.Length()
} }
@@ -187,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars([]byte(joinTokens(tokens))) chars := util.ToChars(sbytes(joinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {

120
src/tui/eventtype_string.go Normal file
View File

@@ -0,0 +1,120 @@
// Code generated by "stringer -type=EventType"; DO NOT EDIT.
package tui
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Rune-0]
_ = x[CtrlA-1]
_ = x[CtrlB-2]
_ = x[CtrlC-3]
_ = x[CtrlD-4]
_ = x[CtrlE-5]
_ = x[CtrlF-6]
_ = x[CtrlG-7]
_ = x[CtrlH-8]
_ = x[Tab-9]
_ = x[CtrlJ-10]
_ = x[CtrlK-11]
_ = x[CtrlL-12]
_ = x[CtrlM-13]
_ = x[CtrlN-14]
_ = x[CtrlO-15]
_ = x[CtrlP-16]
_ = x[CtrlQ-17]
_ = x[CtrlR-18]
_ = x[CtrlS-19]
_ = x[CtrlT-20]
_ = x[CtrlU-21]
_ = x[CtrlV-22]
_ = x[CtrlW-23]
_ = x[CtrlX-24]
_ = x[CtrlY-25]
_ = x[CtrlZ-26]
_ = x[Esc-27]
_ = x[CtrlSpace-28]
_ = x[CtrlDelete-29]
_ = x[CtrlBackSlash-30]
_ = x[CtrlRightBracket-31]
_ = x[CtrlCaret-32]
_ = x[CtrlSlash-33]
_ = x[ShiftTab-34]
_ = x[Backspace-35]
_ = x[Delete-36]
_ = x[PageUp-37]
_ = x[PageDown-38]
_ = x[Up-39]
_ = x[Down-40]
_ = x[Left-41]
_ = x[Right-42]
_ = x[Home-43]
_ = x[End-44]
_ = x[Insert-45]
_ = x[ShiftUp-46]
_ = x[ShiftDown-47]
_ = x[ShiftLeft-48]
_ = x[ShiftRight-49]
_ = x[ShiftDelete-50]
_ = x[F1-51]
_ = x[F2-52]
_ = x[F3-53]
_ = x[F4-54]
_ = x[F5-55]
_ = x[F6-56]
_ = x[F7-57]
_ = x[F8-58]
_ = x[F9-59]
_ = x[F10-60]
_ = x[F11-61]
_ = x[F12-62]
_ = x[AltBackspace-63]
_ = x[AltUp-64]
_ = x[AltDown-65]
_ = x[AltLeft-66]
_ = x[AltRight-67]
_ = x[AltShiftUp-68]
_ = x[AltShiftDown-69]
_ = x[AltShiftLeft-70]
_ = x[AltShiftRight-71]
_ = x[Alt-72]
_ = x[CtrlAlt-73]
_ = x[Invalid-74]
_ = x[Mouse-75]
_ = x[DoubleClick-76]
_ = x[LeftClick-77]
_ = x[RightClick-78]
_ = x[SLeftClick-79]
_ = x[SRightClick-80]
_ = x[ScrollUp-81]
_ = x[ScrollDown-82]
_ = x[SScrollUp-83]
_ = x[SScrollDown-84]
_ = x[PreviewScrollUp-85]
_ = x[PreviewScrollDown-86]
_ = x[Resize-87]
_ = x[Change-88]
_ = x[BackwardEOF-89]
_ = x[Start-90]
_ = x[Load-91]
_ = x[Focus-92]
_ = x[One-93]
_ = x[Zero-94]
_ = x[Result-95]
_ = x[Jump-96]
_ = x[JumpCancel-97]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 458, 467, 477, 487, 498, 506, 516, 525, 536, 551, 568, 574, 580, 591, 596, 600, 605, 608, 612, 618, 622, 632}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {
return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
}

View File

@@ -71,7 +71,7 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h") fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset() r.queued.Reset()
} }
} }
@@ -245,7 +245,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
} }
retries := 0 retries := 0
if c == ESC.Int() || nonblock { if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@@ -260,7 +260,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue continue
} }
break break
} else if c == ESC.Int() && pc != c { } else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} else { } else {
retries = 0 retries = 0
@@ -300,7 +300,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte(): case CtrlQ.Byte():
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28: case 28:
@@ -311,7 +311,7 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil} return Event{CtrlCaret, 0, nil}
case 31: case 31:
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
case ESC.Byte(): case Esc.Byte():
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
@@ -327,7 +327,7 @@ func (r *LightRenderer) GetChar() Event {
} }
char, rsz := utf8.DecodeRune(r.buffer) char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError { if char == utf8.RuneError {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
sz = rsz sz = rsz
return Event{Rune, char, nil} return Event{Rune, char, nil}
@@ -335,7 +335,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event { func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 { if len(r.buffer) < 2 {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
loc := offsetRegexpBegin.FindIndex(r.buffer) loc := offsetRegexpBegin.FindIndex(r.buffer)
@@ -349,15 +349,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
} }
alt := false alt := false
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() { if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
r.buffer = r.buffer[1:] r.buffer = r.buffer[1:]
alt = true alt = true
} }
switch r.buffer[1] { switch r.buffer[1] {
case ESC.Byte(): case Esc.Byte():
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
case 127: case 127:
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
case '[', 'O': case '[', 'O':
if len(r.buffer) < 3 { if len(r.buffer) < 3 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -386,7 +386,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Up, 0, nil} return Event{Up, 0, nil}
case 'Z': case 'Z':
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case 'H': case 'H':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 'F': case 'F':
@@ -434,7 +434,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
if r.buffer[3] == '~' { if r.buffer[3] == '~' {
return Event{Del, 0, nil} return Event{Delete, 0, nil}
} }
if len(r.buffer) == 6 && r.buffer[5] == '~' { if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6 *sz = 6
@@ -442,16 +442,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
case '5': case '5':
return Event{CtrlDelete, 0, nil} return Event{CtrlDelete, 0, nil}
case '2': case '2':
return Event{SDelete, 0, nil} return Event{ShiftDelete, 0, nil}
} }
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case '6': case '6':
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case '7': case '7':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case '8': case '8':
@@ -489,16 +489,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case '1', '2', '3', '5': case '1', '2', '3', '4', '5':
// Kitty iTerm2 WezTerm
// SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3' alt := r.buffer[4] == '3'
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5] char := r.buffer[5]
if altShift { altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 { if len(r.buffer) < 7 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 7 *sz = 7
char = r.buffer[6] char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if len(r.buffer) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
char = r.buffer[5]
} }
switch char { switch char {
case 'A': case 'A':
@@ -506,33 +519,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
} }
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
case 'B': case 'B':
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
} }
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
case 'C': case 'C':
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
} }
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
case 'D': case 'D':
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
} }
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]
@@ -808,13 +821,25 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runeWidth(w.border.top) hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top { if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
} }
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom { if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
} }
} }

View File

@@ -58,7 +58,7 @@ func openTtyIn() *os.File {
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2) util.Exit(2)
} }
return in return in
} }

View File

@@ -320,16 +320,16 @@ func (r *FullscreenRenderer) GetChar() Event {
switch ev.Rune() { switch ev.Rune() {
case 0: case 0:
if ctrl { if ctrl {
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
} }
case rune(tcell.KeyCtrlH): case rune(tcell.KeyCtrlH):
switch { switch {
case ctrl: case ctrl:
return keyfn('h') return keyfn('h')
case alt: case alt:
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
case none, shift: case none, shift:
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
} }
} }
case tcell.KeyCtrlI: case tcell.KeyCtrlI:
@@ -382,17 +382,17 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 3: (Alt)+Backspace2 // section 3: (Alt)+Backspace2
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
} }
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp: case tcell.KeyUp:
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
if shift { if shift {
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
} }
if alt { if alt {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
@@ -400,10 +400,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil} return Event{Up, 0, nil}
case tcell.KeyDown: case tcell.KeyDown:
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
if shift { if shift {
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
} }
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
@@ -411,10 +411,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil} return Event{Down, 0, nil}
case tcell.KeyLeft: case tcell.KeyLeft:
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
if shift { if shift {
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
@@ -422,10 +422,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil} return Event{Left, 0, nil}
case tcell.KeyRight: case tcell.KeyRight:
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
if shift { if shift {
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
} }
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
@@ -442,17 +442,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlDelete, 0, nil} return Event{CtrlDelete, 0, nil}
} }
if shift { if shift {
return Event{SDelete, 0, nil} return Event{ShiftDelete, 0, nil}
} }
return Event{Del, 0, nil} return Event{Delete, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
case tcell.KeyPgUp: case tcell.KeyPgUp:
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case tcell.KeyPgDn: case tcell.KeyPgDn:
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case tcell.KeyBacktab: case tcell.KeyBacktab:
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case tcell.KeyF1: case tcell.KeyF1:
return Event{F1, 0, nil} return Event{F1, 0, nil}
case tcell.KeyF2: case tcell.KeyF2:
@@ -498,7 +498,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc // section 7: Esc
case tcell.KeyEsc: case tcell.KeyEsc:
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
} }
@@ -544,7 +544,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height, height: height,
normal: normal, normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle}
w.drawBorder(false) w.Erase()
return w return w
} }
@@ -561,8 +561,8 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
w.drawBorder(false)
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ') fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
w.drawBorder(false)
} }
func (w *TcellWindow) EraseMaybe() bool { func (w *TcellWindow) EraseMaybe() bool {

View File

@@ -102,22 +102,22 @@ func TestGetCharEventKey(t *testing.T) {
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows) // KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows) // KeyDelete = 0x2E (VK_DELETE constant in Windows)
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH) // KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated {giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,8 +126,8 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}}, {giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}}, {giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}}, {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}}, {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
@@ -161,11 +161,11 @@ func TestGetCharEventKey(t *testing.T) {
// section 7: Esc // section 7: Esc
// KeyEsc and KeyEscape are aliases for KeyESC // KeyEsc and KeyEscape are aliases for KeyESC
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated {giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
// section 8: Invalid // section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated {giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
@@ -259,7 +259,7 @@ Quick reference
37 LeftClick 37 LeftClick
38 RightClick 38 RightClick
39 BTab 39 BTab
40 BSpace 40 Backspace
41 Del 41 Del
42 PgUp 42 PgUp
43 PgDn 43 PgDn
@@ -272,7 +272,7 @@ Quick reference
50 Insert 50 Insert
51 SUp 51 SUp
52 SDown 52 SDown
53 SLeft 53 ShiftLeft
54 SRight 54 SRight
55 F1 55 F1
56 F2 56 F2
@@ -288,15 +288,15 @@ Quick reference
66 F12 66 F12
67 Change 67 Change
68 BackwardEOF 68 BackwardEOF
69 AltBS 69 AltBackspace
70 AltUp 70 AltUp
71 AltDown 71 AltDown
72 AltLeft 72 AltLeft
73 AltRight 73 AltRight
74 AltSUp 74 AltSUp
75 AltSDown 75 AltSDown
76 AltSLeft 76 AltShiftLeft
77 AltSRight 77 AltShiftRight
78 Alt 78 Alt
79 CtrlAlt 79 CtrlAlt
.. ..

View File

@@ -6,10 +6,13 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
// Types of user action // Types of user action
//
//go:generate stringer -type=EventType
type EventType int type EventType int
const ( const (
@@ -41,7 +44,7 @@ const (
CtrlX CtrlX
CtrlY CtrlY
CtrlZ CtrlZ
ESC Esc
CtrlSpace CtrlSpace
CtrlDelete CtrlDelete
@@ -51,27 +54,12 @@ const (
CtrlCaret CtrlCaret
CtrlSlash CtrlSlash
Invalid ShiftTab
Resize Backspace
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
BTab Delete
BSpace PageUp
PageDown
Del
PgUp
PgDn
Up Up
Down Down
@@ -81,11 +69,11 @@ const (
End End
Insert Insert
SUp ShiftUp
SDown ShiftDown
SLeft ShiftLeft
SRight ShiftRight
SDelete ShiftDelete
F1 F1
F2 F2
@@ -100,6 +88,38 @@ const (
F11 F11
F12 F12
AltBackspace
AltUp
AltDown
AltLeft
AltRight
AltShiftUp
AltShiftDown
AltShiftLeft
AltShiftRight
Alt
CtrlAlt
Invalid
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
// Events
Resize
Change Change
BackwardEOF BackwardEOF
Start Start
@@ -108,21 +128,8 @@ const (
One One
Zero Zero
Result Result
Jump
AltBS JumpCancel
AltUp
AltDown
AltLeft
AltRight
AltSUp
AltSDown
AltSLeft
AltSRight
Alt
CtrlAlt
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {
@@ -142,6 +149,31 @@ func (e Event) Comparable() Event {
return Event{e.Type, e.Char, nil} return Event{e.Type, e.Char, nil}
} }
func (e Event) KeyName() string {
if e.Type >= Invalid {
return ""
}
switch e.Type {
case Rune:
return string(e.Char)
case Alt:
return "alt-" + string(e.Char)
case CtrlAlt:
return "ctrl-alt-" + string(e.Char)
case CtrlBackSlash:
return "ctrl-\\"
case CtrlRightBracket:
return "ctrl-]"
case CtrlCaret:
return "ctrl-^"
case CtrlSlash:
return "ctrl-/"
}
return util.ToKebabCase(e.Type.String())
}
func Key(r rune) Event { func Key(r rune) Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
@@ -646,7 +678,7 @@ func NoColorTheme() *ColorTheme {
func errorExit(message string) { func errorExit(message string) {
fmt.Fprintln(os.Stderr, message) fmt.Fprintln(os.Stderr, message)
os.Exit(2) util.Exit(2)
} }
func init() { func init() {

38
src/util/atexit.go Normal file
View File

@@ -0,0 +1,38 @@
package util
import (
"os"
"sync"
)
var atExitFuncs []func()
// AtExit registers the function fn to be called on program termination.
// The functions will be called in reverse order they were registered.
func AtExit(fn func()) {
if fn == nil {
panic("AtExit called with nil func")
}
once := &sync.Once{}
atExitFuncs = append(atExitFuncs, func() {
once.Do(fn)
})
}
// RunAtExitFuncs runs any functions registered with AtExit().
func RunAtExitFuncs() {
fns := atExitFuncs
for i := len(fns) - 1; i >= 0; i-- {
fns[i]()
}
}
// Exit executes any functions registered with AtExit() then exits the program
// with os.Exit(code).
//
// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
// the program before any of the AtExit functions can run.
func Exit(code int) {
defer os.Exit(code)
RunAtExitFuncs()
}

24
src/util/atexit_test.go Normal file
View File

@@ -0,0 +1,24 @@
package util
import (
"reflect"
"testing"
)
func TestAtExit(t *testing.T) {
want := []int{3, 2, 1, 0}
var called []int
for i := 0; i < 4; i++ {
n := i
AtExit(func() { called = append(called, n) })
}
RunAtExitFuncs()
if !reflect.DeepEqual(called, want) {
t.Errorf("AtExit: want call order: %v got: %v", want, called)
}
RunAtExitFuncs()
if !reflect.DeepEqual(called, want) {
t.Error("AtExit: should only call exit funcs once")
}
}

View File

@@ -163,7 +163,7 @@ func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(runes) return string(runes)
} }
return string(chars.slice) return unsafe.String(unsafe.SliceData(chars.slice), len(chars.slice))
} }
func (chars *Chars) ToRunes() []rune { func (chars *Chars) ToRunes() []rune {
@@ -178,12 +178,12 @@ func (chars *Chars) ToRunes() []rune {
return runes return runes
} }
func (chars *Chars) CopyRunes(dest []rune) { func (chars *Chars) CopyRunes(dest []rune, from int) {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes) copy(dest, runes[from:])
return return
} }
for idx, b := range chars.slice[:len(dest)] { for idx, b := range chars.slice[from:][:len(dest)] {
dest[idx] = rune(b) dest[idx] = rune(b)
} }
} }

View File

@@ -176,3 +176,15 @@ func RepeatToFill(str string, length int, limit int) string {
} }
return output return output
} }
// ToKebabCase converts the given CamelCase string to kebab-case
func ToKebabCase(s string) string {
name := ""
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
}

View File

@@ -18,7 +18,6 @@ UNSETS = %w[
FZF_ALT_C_COMMAND FZF_ALT_C_COMMAND
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
FZF_API_KEY FZF_API_KEY
fish_history
].freeze ].freeze
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
@@ -67,7 +66,7 @@ class Shell
end end
def fish def fish
UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish' "unset #{UNSETS.join(' ')}; FZF_DEFAULT_OPTS=--no-scrollbar fish_history= fish"
end end
end end
end end
@@ -1469,6 +1468,19 @@ class TestGoFZF < TestBase
assert_equal '3', readonce.chomp assert_equal '3', readonce.chomp
end end
def test_jump_events
tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump,jump:preview(echo jumped to {}),jump-cancel:preview(echo jump cancelled at {})'")}", :Enter
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_includes lines[-7], '5 5' }
tmux.send_keys '3'
tmux.until { |lines| assert(lines.any? { _1.include?('jumped to 3') }) }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_includes lines[-7], '5 5' }
tmux.send_keys 'C-c'
tmux.until { |lines| assert(lines.any? { _1.include?('jump cancelled at 3') }) }
end
def test_pointer def test_pointer
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed # Assert that specified pointer is displayed
@@ -1720,7 +1732,7 @@ class TestGoFZF < TestBase
end end
def test_info_hidden def test_info_hidden
tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter tmux.send_keys 'seq 10 | fzf --info=hidden --no-separator', :Enter
tmux.until { |lines| assert_equal '> 1', lines[-2] } tmux.until { |lines| assert_equal '> 1', lines[-2] }
end end
@@ -1918,7 +1930,7 @@ class TestGoFZF < TestBase
end end
def test_reload def test_reload
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
tmux.until { |lines| assert_equal 998, lines.match_count } tmux.until { |lines| assert_equal 998, lines.match_count }
tmux.send_keys 'a' tmux.send_keys 'a'
tmux.until do |lines| tmux.until do |lines|
@@ -2464,6 +2476,84 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.match_count } tmux.until { |lines| assert_equal 10, lines.match_count }
end end
def test_reload_disabled_case1
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case2
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case3
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case4
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case5
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1001, lines.match_count }
end
def test_reload_disabled_case6
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys '9'
tmux.until { |lines| assert_equal 9, lines.match_count }
tmux.send_keys '9'
tmux.until { |lines| assert_equal 99, lines.match_count }
# TODO: How do we verify if an intermediate empty list is not shown?
end
def test_scroll_off def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count } tmux.until { |lines| assert_equal 1000, lines.item_count }
@@ -2582,6 +2672,7 @@ class TestGoFZF < TestBase
def test_change_preview_window_rotate def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \ tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter "a:change-preview-window(right|down|up|hidden|)'", :Enter
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
3.times do 3.times do
tmux.until { |lines| lines[0].start_with?('hello') } tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a' tmux.send_keys 'a'
@@ -2799,6 +2890,27 @@ class TestGoFZF < TestBase
end end
end end
def test_labels_variables
tmux.send_keys ': | fzf --border --border-label foobar --preview "echo \$FZF_BORDER_LABEL // \$FZF_PREVIEW_LABEL" --preview-label barfoo --bind "space:change-border-label(barbaz)+change-preview-label(bazbar)+refresh-preview,enter:transform-border-label(echo 123)+transform-preview-label(echo 456)+refresh-preview"', :Enter
tmux.until do
assert_includes(_1[0], '─foobar─')
assert_includes(_1[1], '─barfoo─')
assert_includes(_1[2], ' foobar // barfoo ')
end
tmux.send_keys :Space
tmux.until do
assert_includes(_1[0], '─barbaz─')
assert_includes(_1[1], '─bazbar─')
assert_includes(_1[2], ' barbaz // bazbar ')
end
tmux.send_keys :Enter
tmux.until do
assert_includes(_1[0], '─123─')
assert_includes(_1[1], '─456─')
assert_includes(_1[2], ' 123 // 456 ')
end
end
def test_info_separator_unicode def test_info_separator_unicode
tmux.send_keys 'seq 100 | fzf -q55', :Enter tmux.send_keys 'seq 100 | fzf -q55', :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 ─') } tmux.until { assert_includes(_1[-2], ' 1/100 ─') }
@@ -3002,7 +3114,7 @@ class TestGoFZF < TestBase
end end
tmux.send_keys :t tmux.send_keys :t
tmux.until do |lines| tmux.until do |lines|
assert_includes lines[-2], '+T' assert_includes lines[-2], '+t'
end end
tmux.send_keys :BSpace tmux.send_keys :BSpace
tmux.until do |lines| tmux.until do |lines|
@@ -3014,7 +3126,7 @@ class TestGoFZF < TestBase
tmux.send_keys '4' tmux.send_keys '4'
tmux.until do |lines| tmux.until do |lines|
assert_equal 28, lines.match_count assert_equal 28, lines.match_count
refute_includes lines[-2], '+T' refute_includes lines[-2], '+t'
end end
tmux.send_keys :BSpace tmux.send_keys :BSpace
tmux.until do |lines| tmux.until do |lines|
@@ -3023,11 +3135,11 @@ class TestGoFZF < TestBase
end end
tmux.send_keys :t tmux.send_keys :t
tmux.until do |lines| tmux.until do |lines|
assert_includes lines[-2], '+T' assert_includes lines[-2], '+t'
end end
tmux.send_keys :Up tmux.send_keys :Up
tmux.until do |lines| tmux.until do |lines|
refute_includes lines[-2], '+T' refute_includes lines[-2], '+t'
end end
end end
@@ -3307,7 +3419,10 @@ module CompletionTest
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 } tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'foobar$' tmux.send_keys 'foobar$'
tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> /tmp/fzf test/foobar')
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] } tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }

View File

@@ -94,6 +94,7 @@ done
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ -f "$bind_file" ]; then if [ -f "$bind_file" ]; then
remove_line "$bind_file" "fzf_key_bindings" remove_line "$bind_file" "fzf_key_bindings"
remove_line "$bind_file" "fzf --fish | source"
fi fi
if [ -d "${fish_dir}/functions" ]; then if [ -d "${fish_dir}/functions" ]; then