Compare commits

...

171 Commits

Author SHA1 Message Date
junegunn daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 2 days ago
Zhizhen He 01e7668915
chore: use strings.ReplaceAll (#3801) 2 days ago
Enno 0994d9c881
Make :FZF work in Vim from Git Bash (#3798)
* make :FZF work in Vim from Git Bash

Despite its title 'Calling fzf#run with a list as source fail (n)vim is used from git bash' the issue in 

https://github.com/junegunn/fzf/issues/3777

of running `:FZF` in Vim in Git Bash was apparently only fixed for Neovim in Git Bash on Windows 11, but not for Vim from Git Bash.

In view of this, replacing /C by ///C might be considered a universal fix.

This PR just proposes the patch in https://github.com/junegunn/fzf/issues/1983 that still seems open.

In view of the fourth item in the most recent 2.45.0 https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues little seems to have changed regarding path conversion of arguments containing forward slashes

* prefer doubling slashed instead of generic env. var

If MSYS_NO_PATHCONV=1 is used, then all arguments are preserved, in particular possibly paths passed in s:command.
Therefore, only avoid converting `/C` from `cmd` to a path.
5 days ago
LangLangBart 030428ba43
docs: update zsh integration instructions (#3794) 6 days ago
Junegunn Choi 8a110e02b9
Fix tcell test case 6 days ago
Junegunn Choi 86d92c17c4
Refactor tui.TtyIn() 6 days ago
Junegunn Choi c4cc7891b4
Revert "Close handles to /dev/tty", instead reuse handles 6 days ago
Junegunn Choi 218843b9f1
Close handles to /dev/tty 6 days ago
Junegunn Choi d274d093af
Render UI directly to /dev/tty
See https://github.com/junegunn/fzf/discussions/3792

This allows us to separately capture the standard error from fzf and its
child processes, and there's less chance of user errors of redirecting
the error stream and hiding fzf.
6 days ago
Junegunn Choi 6432f00f0d
0.52.1 7 days ago
junegunn 4e9e842aa4 Deploying to master from @ junegunn/fzf@07880ca441 🚀 1 week ago
LangLangBart 07880ca441
chore: Update flags to include long-form options for case (#3785) 2 weeks ago
Junegunn Choi bcda25a513
0.52.0 2 weeks ago
Junegunn Choi 8256fcde15
Minor fixup 2 weeks ago
Junegunn Choi af65aa298a
Add color names: selected-{fg,bg,hl} 2 weeks ago
Junegunn Choi 6834d17844
[vim] Revert 7411da8d5a
Fix #3777
2 weeks ago
Junegunn Choi ed511d7867
[install] tar --no-same-owner
Close #3776
2 weeks ago
Junegunn Choi cd8d736a9f
[shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2 weeks ago
Junegunn Choi 0952b2dfd4
Rename --cursor-line to --highlight-line 2 weeks ago
Junegunn Choi 4bedd33c59
Refactor the code to remove global variables 2 weeks ago
Junegunn Choi c5fb0c43f9
Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2 weeks ago
Junegunn Choi 9e4780510e
Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2 weeks ago
Junegunn Choi e8405f40fe
Refactor the code so that fzf can be used as a library (#3769) 2 weeks ago
dependabot[bot] 065b9e6fb2
Bump golang.org/x/term from 0.19.0 to 0.20.0 (#3774)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.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>
2 weeks ago
dependabot[bot] 98141ca7d8
Bump crate-ci/typos from 1.20.10 to 1.21.0 (#3772)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.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.20.10...v1.21.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>
2 weeks ago
Junegunn Choi 501577ab28
Fix flaky test case 2 weeks ago
Junegunn Choi 5669f48343
Do not enable delayed expansion mode when running cmd.exe
And simplify the argument escaping code. Fix #3764.

This may breaks some existing use cases, but the mode causes too much
trouble when escaping arguments and it makes some things not possible.

  # Now you can pass special characters to rg process without any escaping problems: &|<>()@^%!
  fzf --ansi --disabled --bind "change:reload:rg --column --line-number --no-heading --color=always --smart-case -- {q}"

  # No sudden expansion of the arguments on '!'
  fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!" --prompt "&|<>()@^%!"
2 weeks ago
Junegunn Choi 24ff66d4a9
Fix `change-preview` reset by `change-preview-window`
Fix #3770
2 weeks ago
Junegunn Choi bf184449bc
Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2 weeks ago
Kuremu 7b98c2c653
Add click-header event for reporting clicks within header (#3768)
Sets $FZF_CLICK_HEADER_LINE and $FZF_CLICK_HEADER_COLUMN env vars with
coordinates of the last click inside and relative to the header and
fires click-header event.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2 weeks ago
Junegunn Choi b6add2a257
Fix rendering of preview window border of tcell renderer
(sleep 1; find .) |
    go run -tags tcell main.go --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2 weeks ago
Junegunn Choi 2bd41f1330
Reduce flicking when changing the size of the preview window with --border
(sleep 1; find .) |
    fzf --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2 weeks ago
Junegunn Choi c37cd11ca5
Remove unnecessary flicking when changing the size of the preview window
fzf --bind 'space:change-preview-window(60%|70%|80%|90%|hidden|)' --preview 'cat {}'
2 weeks ago
Junegunn Choi 9dee8edc0c
Clear characters on 1-column margin after the preview window on the left 2 weeks ago
Junegunn Choi f6aa28c380
Fix --info inline-right not properly clearing the previous output
(seq 100000; sleep 1) | fzf --info inline-right --bind load:change-query:x
3 weeks ago
cyqsimon dba1644518
Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
3 weeks ago
Junegunn Choi 260a65b0fb
0.51.0 3 weeks ago
Junegunn Choi 835d2fb98c
[vim] Fix argument escaping for Windows batch file
Fix #3620
3 weeks ago
Charlie Vieth a9811addaa
Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
3 weeks ago
dependabot[bot] ee9d88b637
Bump crate-ci/typos from 1.20.9 to 1.20.10 (#3757)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.9 to 1.20.10.
- [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.20.9...v1.20.10)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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>
3 weeks ago
Junegunn Choi 194a763c46
Escaping for cmd.exe: always use double quotes 3 weeks ago
Junegunn Choi 8d74446bef
Fix escaping for cmd.exe
Close #3651
Close #2609
3 weeks ago
Junegunn Choi 7ed6c7905c
Determine shell type once by the basename 3 weeks ago
Junegunn Choi 159a37fa37
Restore CmdLine parameter when running commands using cmd.exe 3 weeks ago
junegunn f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 3 weeks ago
Junegunn Choi 4a68eac99b
Suggest using toggle+up instead of toggle-up 3 weeks ago
Junegunn Choi 2665580120
Add $FZF_POS environment variable
Close #2175
Close #3753
3 weeks ago
Junegunn Choi a4391aeedd
Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
3 weeks ago
dependabot[bot] b86a967ee2
Bump crate-ci/typos from 1.19.0 to 1.20.9 (#3749)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.19.0 to 1.20.9.
- [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.19.0...v1.20.9)

---
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>
3 weeks ago
Junegunn Choi 608232568b
Add 'change-multi' action
Close #3754
3 weeks ago
Junegunn Choi 7f85beccb5
[completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
3 weeks ago
Junegunn Choi 767f1255ab
Make completion.bash load faster
* Reduce number of `__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)`s
* Clean up options in fzf completion
* Remove telnet completion
4 weeks ago
Junegunn Choi fddbfe7b0e
Fix 'reload' not terminating closed standard input stream
Fix #3750
4 weeks ago
Junegunn Choi 4ab7fdc28e
Merge identical case clauses 4 weeks ago
Junegunn Choi e352b68878
Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
4 weeks ago
Junegunn Choi 207deeadba
Add -trimpath to build command 4 weeks ago
Cheng d18d92f925
Replace fmt.Errorf with no parameters with errors.New (#3747) 4 weeks ago
junegunn af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 4 weeks ago
Junegunn Choi d8bfb6712d
Remove invalid 'result' event when using --sync option
When the search for the initial query doesn't finish immediately
fzf would trigger an invalid 'result' event for an empty query.

  seq 100 | fzf --query 99 --bind result:accept --sync
    # Prints 99

  seq 1000000 | fzf --query 99 --bind result:accept --sync
    # Should print 99, but fzf would print 1
1 month ago
Junegunn Choi f864f8b5f7
Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
1 month ago
Junegunn Choi 31d72efba7
Describe how to build fzf from the latest source using brew 1 month ago
LangLangBart d169c951f3
fix: Move 'emulate' command outside interactive check (#3736) 1 month ago
Junegunn Choi 90d7e38909
[fzf-tmux] Replace `command -v` with `which`
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

Fix #3730
1 month ago
hidewrong 938f23e429
Fix typo in comment (#3734)
Signed-off-by: hidewrong <hidewrong@outlook.com>
1 month ago
Junegunn Choi f97d275413
0.50.0 1 month ago
Junegunn Choi 3acb4ca90e
Fix streaming filter mode by not running reader callback concurrently
Close #3728
1 month ago
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.
1 month ago
Junegunn Choi a5447b8b75
Improve search performance by pre-calculating bonus matrix
This gives yet another 5% boost.
1 month ago
Junegunn Choi 7ce6452d83
Improve search performance by pre-calculating character classes
This simple optmization can give more than 15% performance boost
in some scenarios.
1 month ago
junegunn 5643a306bd Deploying to master from @ junegunn/fzf@3c877c504b 🚀 1 month ago
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>
1 month ago
Junegunn Choi 892d1acccb
Fix tcell build 1 month ago
Junegunn Choi 1a9c282f76
Fix unit tests 1 month ago
Junegunn Choi fd1ba46f77
Export $FZF_KEY environment variable to child processes
It's the name of the last key pressed.

Related #3412
1 month ago
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'
1 month ago
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>
1 month ago
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
1 month ago
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.
1 month ago
LangLangBart 69b9d674a3
chore: Add new option in issue checklist and modify requirements (#3715) 1 month ago
junegunn bad8061547 Deploying to master from @ junegunn/fzf@62963dcefd 🚀 1 month ago
Junegunn Choi 62963dcefd
0.49.0 2 months ago
Junegunn Choi 68a35e4735
Do not trim CR on Windows when --read0 is set 2 months ago
Charlie Vieth 9b9ad77e1c
mod: update changes/fastwalk to v1.0.3 (#3709)
Update charlievieth/fastwalk to resolve issue #3706.
2 months ago
Junegunn Choi 118b4d4a01
[bash] Add -o nospace to dir completion options (#1987) 2 months ago
Junegunn Choi da14ab6f16
[bash] Remove -o default from dir completion options (#1987) 2 months ago
Junegunn Choi 09a4ca6ab5
[bash] Fix variable completion of directory-related commands
Fix #1987
2 months ago
Junegunn Choi 8a2df79711
Do not hide separator by default on --info=inline-right|hidden 2 months ago
Junegunn Choi c30e486b64
Further performance improvements by removing unnecessary copies 2 months ago
Junegunn Choi a575c0c54b
GitHub Actions: Use Go "1.20" 2 months ago
Junegunn Choi 77fe96ac0d
GitHub Actions: Use Go 1.20 2 months ago
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
2 months ago
Junegunn Choi 41b3511ad9
Improve ingestion performance (by around 20%) 2 months ago
Junegunn Choi 128e4a2e8d
[fish] Fix $dir in FZF_{CTRL_T,ALT_C}_COMMAND not evaluated
Fix #3705
2 months ago
junegunn 07ac90d798 Deploying to master from @ junegunn/fzf@7de87a9b2c 🚀 2 months ago
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>
2 months ago
Junegunn Choi dff865239a
[bash-completion] Make dynamic loader return 124 to retry completion
Close #3702
2 months ago
Junegunn Choi 07f8f70c5b
Fix flaky test case 2 months ago
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>
2 months ago
Junegunn Choi 8a74976c1f
Add track-current, untrack-current, and toggle-track-current (#3699)
Close #3691
2 months ago
Junegunn Choi b6bfd4a5cb
Fix typo in comment 2 months ago
Junegunn Choi 008fb9d258
Fix reload and reload-sync behaviors
https://github.com/junegunn/fzf/discussions/3696#discussioncomment-8915593
2 months ago
Junegunn Choi db6db49ed6
Increase the buffer size for POST requests
Close #3685
2 months ago
Junegunn Choi 05453881c3
Set a 2-second timeout for POST requests
Close #3685
2 months ago
Junegunn Choi 5e47ab9431
README: Mention that you can source individual script files 2 months ago
LangLangBart ec70acd0b9
chore: transition from markdown to YAML for issue template (#3687) 2 months ago
zeertzjq 25e61056b6
[fish] Fix Ctrl-T and Alt-C not using last token as search root (#3684) 2 months ago
Junegunn Choi d579e335b5
0.48.1 2 months ago
Junegunn Choi 7e344ceb85
Update README 2 months ago
Junegunn Choi 0145b82ea0
Update README 2 months ago
Junegunn Choi b4efe7aab7
Show how to disable a key binding 2 months ago
Junegunn Choi 9ffe951f6d
Update Makefile target dependencies
Because shell integration scripts are now embedded in the binary
2 months ago
Brayden Hill a5ea4f57bd
Updated link for highlight command (#3680) 2 months ago
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>
2 months ago
Junegunn Choi c7ee071efa
Fix panic caused by invalid cursor index
Fix #3681
2 months ago
Junegunn Choi 0740ef7ceb
[bash] Fix default completion of unset, unalias, etc
Fix #3679
2 months ago
junegunn b29bd809ac Deploying to master from @ junegunn/fzf@8977c9257a 🚀 2 months ago
Junegunn Choi 8977c9257a
Limit the maximum number of focus events to process at once 2 months ago
Junegunn Choi 091b7eacba
0.48.0 2 months ago
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
2 months ago
Junegunn Choi d282a1649d
Add walker options and replace 'find' with the built-in walker (#3649) 2 months ago
Junegunn Choi 6ce8d49d1b
[bash] Fix regression in dynamic completion
Fix #3674
2 months ago
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>
2 months ago
Junegunn Choi 0494f20d62
Revert "Fix CHANGELOG"
This reverts commit 73aff476dd.
2 months ago
Junegunn Choi 73aff476dd
Fix CHANGELOG 2 months ago
Junegunn Choi 98ee5e651a
0.47.0 2 months ago
Koichi Murase 01871ea383 [bash] Update orig_complete after _completion_loader 2 months ago
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.
2 months ago
junegunn c70f0eadb8 Deploying to master from @ junegunn/fzf@26244ad8c2 🚀 2 months ago
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
2 months ago
Junegunn Choi fa0aa5510d
Kill preview process when hiding the preview window
via toggle-preview, hide-preview, or change-preview-window
2 months ago
Junegunn Choi eec557b6aa
Fix invalid memory access when the preview window becomes hidden 2 months ago
huajin tong 0cc27c3cc1
Fix typo (#3661) 2 months ago
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>
3 months ago
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>
3 months ago
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>
3 months ago
onee-only 61bc129e1d Update parseGetParams to call strconv.Atoi when params are valid 3 months ago
onee-only 52210a57f0 Update error return position according to convention 3 months ago
onee-only 8061a2f108 Remove duplicate code 3 months ago
junegunn 7444eff6d4 Deploying to master from @ junegunn/fzf@f35a9da99a 🚀 3 months ago
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>
3 months ago
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>
3 months ago
Junegunn Choi 686f9288fc
Allow iTerm2 image data that ends with 'ESC \' (#3646) 3 months ago
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
3 months ago
junegunn 3dd42f5aa2 Deploying to master from @ junegunn/fzf@99a7beba57 🚀 3 months ago
Junegunn Choi 99a7beba57
Fix missing bonus score on a delimiter character
Fix #3645
3 months ago
Junegunn Choi edee2b753c
fzf-tmux: Workaround for tmux 3.4 bug
Close #3635

https://github.com/tmux/tmux/pull/3840
3 months ago
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>
3 months ago
Junegunn Choi ca747a2b54
Fix unit tests 3 months ago
Junegunn Choi 17da165cfe
CHANGELOG: charlievieth/fastwalk 3 months ago
Junegunn Choi 5e6788c679
Export FZF_* variables to 'reload' process as well 3 months ago
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.
3 months ago
junegunn 2c8e9dd3a5 Deploying to master from @ junegunn/fzf@7a72f1a253 🚀 3 months ago
Junegunn Choi 7a72f1a253
Code cleanup: Remove unused argument 3 months ago
Junegunn Choi 208e556332
Replace "default find command" with built-in directory traversal 3 months ago
Junegunn Choi c65d11bfb5
Update README: warp.dev 3 months ago
Junegunn Choi 3b5b52d89a
Update README: warp.dev 3 months ago
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>
3 months ago
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>
3 months ago
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>
3 months ago
Konstantin Podsvirov 90b8187882
Add info about MSYS2 distro to README.md (#3610) 4 months ago
junegunn 1a43259989 Deploying to master from @ junegunn/fzf@3c0a630475 🚀 4 months ago
Junegunn Choi 3c0a630475
0.46.1 4 months ago
Junegunn Choi 2a1e5a9729
More test fixes for tcell on GitHub Actions 4 months ago
Junegunn Choi 413c66beba
Fix tests for tcell build 4 months ago
Junegunn Choi 1416e696b1
Avoid full redraw on 'preview' action when preview window exists 4 months ago
Junegunn Choi d373cf89c7
Retain preview window on resize after 'preview' action 4 months ago
dependabot[bot] dd886d22f0
Bump github.com/rivo/uniseg from 0.4.5 to 0.4.6 (#3605)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.4.5 to 0.4.6.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.4.5...v0.4.6)

---
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>
4 months ago
junegunn 472569a27c Deploying to master from @ junegunn/fzf@76cf6559cc 🚀 4 months ago
Junegunn Choi 76cf6559cc
junegunn/uniseg -> rivo/uniseg
https://github.com/rivo/uniseg/pull/47
4 months ago
Junegunn Choi a34e8dcdc9
Downgrade Go version to keep support for old Windows (#3601)
Go 1.21 dropped support for older versions of Windows.

* https://tip.golang.org/doc/go1.21#windows

But there is no absolute reason for fzf to use Go 1.21, so we downgrade
the dependency.
4 months ago
Junegunn Choi da752fc9a4
Fix Windows build
Fix #3598
4 months ago

@ -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

@ -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

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

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

@ -11,18 +11,21 @@ on:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.19
go-version: "1.20"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
@ -42,4 +45,7 @@ jobs:
run: make test
- name: Integration test
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
- name: Integration test (tcell)
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose

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

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

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

@ -12,6 +12,8 @@ builds:
- darwin
goarch:
- amd64
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
@ -19,11 +21,7 @@ builds:
sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
bundle_id = "junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
@ -40,6 +38,8 @@ builds:
- darwin
goarch:
- arm64
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
@ -47,11 +47,7 @@ builds:
sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
bundle_id = "junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
@ -79,6 +75,8 @@ builds:
- 5
- 6
- 7
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
ignore:

@ -1 +1 @@
golang 1.21.6
golang 1.20.13

@ -552,7 +552,7 @@ pods() {
- Press enter key on a pod to `kubectl exec` into it
- Press CTRL-O to open the log in your editor
- 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
1. `80%,border-bottom`
1. `hidden`

@ -6,7 +6,7 @@ Build instructions
### Prerequisites
- Go 1.18 or above
- Go 1.20 or above
### Using Makefile
@ -24,13 +24,23 @@ make build
make release
```
> :warning: Makefile uses git commands to determine the version and the
> revision information for `fzf --version`. So if you're building fzf from an
> [!WARNING]
> 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
> set `$FZF_VERSION` and `$FZF_REVISION`.
>
> 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
--------------------------
@ -42,6 +52,8 @@ Third-party libraries used
- Licensed under [MIT](http://mattn.mit-license.org)
- [tcell](https://github.com/gdamore/tcell)
- 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
-------

@ -1,6 +1,228 @@
CHANGELOG
=========
0.52.1
------
- Fixed a critical bug in the Windows version
- Windows users are strongly encouraged to upgrade to this version
0.52.0
------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
- Added color names for selected lines: `selected-fg`, `selected-bg`, and `selected-hl`
```sh
fzf --border --multi --info inline-right --layout reverse --marker ▏ --pointer ▌ --prompt '▌ ' \
--highlight-line --color gutter:-1,selected-bg:238,selected-fg:146,current-fg:189
```
- Added `click-header` event that is triggered when the header section is clicked. When the event is triggered, `$FZF_CLICK_HEADER_COLUMN` and `$FZF_CLICK_HEADER_LINE` are set.
```sh
fd --type f |
fzf --header $'[Files] [Directories]' --header-first \
--bind 'click-header:transform:
(( FZF_CLICK_HEADER_COLUMN <= 7 )) && echo "reload(fd --type f)"
(( FZF_CLICK_HEADER_COLUMN >= 9 )) && echo "reload(fd --type d)"
'
```
- Add `$FZF_COMPLETION_{DIR,PATH}_OPTS` for separately customizing the behavior of fuzzy completion
```sh
# Set --walker options without 'follow' not to follow symbolic links
FZF_COMPLETION_PATH_OPTS="--walker=file,dir,hidden"
FZF_COMPLETION_DIR_OPTS="--walker=dir,hidden"
```
- Fixed Windows argument escaping
- Bug fixes and improvements
- The code was heavily refactored to allow using fzf as a library in Go programs. The API is still experimental and subject to change.
- https://gist.github.com/junegunn/193990b65be48a38aac6ac49d5669170
0.51.0
------
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.
```sh
# Toggle selection to the top or to the bottom
seq 30 | fzf --multi --bind 'load:pos(10)' \
--bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \
--bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done'
```
- Added `--with-shell` option to start child processes with a custom shell command and flags
```sh
gem list | fzf --with-shell 'ruby -e' \
--preview 'pp Gem::Specification.find_by_name({1})' \
--bind 'ctrl-o:execute-silent:
spec = Gem::Specification.find_by_name({1})
[spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
system "open", _1
end
'
```
- Added `change-multi` action for dynamically changing `--multi` option
- `change-multi` - enable multi-select mode with no limit
- `change-multi(NUM)` - enable multi-select mode with a limit
- `change-multi(0)` - disable multi-select mode
- Windows improvements
- `become` action is now supported on Windows
- Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
- Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes.
- Bug fixes and improvements
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
------
- Bug fixes and improvements
- Fixed Windows binaries
- Downgraded Go version to 1.20 to support older versions of Windows
- https://tip.golang.org/doc/go1.21#windows
- Updated [rivo/uniseg](https://github.com/rivo/uniseg) dependency to v0.4.6
0.46.0
------
- Added two new events

@ -1,6 +1,6 @@
FROM --platform=linux/amd64 ubuntu:22.04
FROM ubuntu:24.04
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
RUN gem install --no-document -v 5.14.2 minitest
RUN gem install --no-document -v 5.22.3 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile

@ -1,10 +1,10 @@
SHELL := bash
GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
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
VERSION := $(FZF_VERSION)
@ -25,7 +25,7 @@ endif
ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION)
endif
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64
@ -173,12 +173,12 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
docker:
docker build -t fzf-arch .
docker run -it fzf-arch tmux
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu tmux
docker-test:
docker build -t fzf-arch .
docker run -it fzf-arch
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu
update:
$(GO) get -u

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

File diff suppressed because one or more lines are too long

@ -7,7 +7,7 @@ fail() {
exit 2
}
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
args=()
@ -95,9 +95,9 @@ while [[ $# -gt 0 ]]; do
elif [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))}
if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))"
opt="$opt -l $(( 100 - size ))%"
else
opt="$opt -p $size"
opt="$opt -l $size%"
fi
else
if [[ -n "$swap" ]]; then
@ -196,8 +196,9 @@ if [[ "$opt" =~ "-E" ]]; then
exit 2
fi
fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$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 "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf"

@ -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*
==============================================================================
@ -264,17 +264,18 @@ entry.
call fzf#run({'sink': 'e'})
<
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
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
directory. When you select one, it will open it with the sink, `:e` command.
If you want to open it in a new tab, you can pass `:tabedit` command instead
as the sink.
command line without standard input pipe; fzf will traverse the file system
under the current directory to get the list of files. (If
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
instead.) When you select one, it will open it with the sink, `:e` command. If
you want to open it in a new tab, you can pass `:tabedit` command instead as
the sink.
>
call fzf#run({'sink': 'tabedit'})
<
Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's
equivalent to running `git ls-files | fzf` on shell.
You can use any shell command as the source to generate the list. The
following example will list the files managed by git. It's equivalent to
running `git ls-files | fzf` on shell.
>
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
<
@ -417,24 +418,12 @@ TIPS *fzf-tips*
< 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.
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`
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
if has('nvim')

@ -1,22 +1,20 @@
module github.com/junegunn/fzf
require (
github.com/gdamore/tcell/v2 v2.5.4
github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2
github.com/mattn/go-isatty v0.0.17
github.com/charlievieth/fastwalk v1.0.3
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/saracen/walker v0.1.3
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.0
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
golang.org/x/text v0.14.0 // indirect
)
go 1.17
go 1.20

@ -1,52 +1,57 @@
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/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2 h1:oEwPBh29BPu1MaTsz2dM9bDrkOgKBoYFC0u6uY2izWo=
github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2/go.mod h1:ywqF55XaSE3/uS2tkJqVFKiE0oIYAXRvU2N7DU4y3XQ=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/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/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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-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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -2,7 +2,7 @@
set -u
version=0.46.0
version=0.52.1
auto_completion=
key_bindings=
update_config=2
@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() {
command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf -
curl -fL $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@ -125,7 +125,7 @@ try_curl() {
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf -
wget -O - $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@ -262,6 +262,16 @@ if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else
cat >> "$src" << EOF
# Auto-completion
# ---------------
$fzf_completion
@ -270,6 +280,7 @@ $fzf_completion
# ------------
$fzf_key_bindings
EOF
fi
echo "OK"
done
@ -281,18 +292,6 @@ if [[ "$shells" =~ fish ]]; then
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
EOF
[ $? -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
append_line() {
@ -355,12 +354,23 @@ done
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then
mkdir -p "${fish_dir}/functions"
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf_key_bindings' \
' fzf --fish | source' \
'end'
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

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

@ -1,14 +1,82 @@
package main
import (
_ "embed"
"fmt"
"os"
"strings"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.46"
var revision string = "devel"
var version = "0.52"
var revision = "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 exit(code int, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() {
protector.Protect()
fzf.Run(fzf.ParseOptions(), version, revision)
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
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
}
if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
code, err := fzf.Run(options)
exit(code, err)
}

@ -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
THE SOFTWARE.
..
.TH fzf-tmux 1 "Jan 2024" "fzf 0.46.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "May 2024" "fzf 0.52.1" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

@ -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
THE SOFTWARE.
..
.TH fzf 1 "Jan 2024" "fzf 0.46.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "May 2024" "fzf 0.52.1" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@ -33,6 +33,10 @@ fzf [options]
fzf is a general-purpose command-line fuzzy finder.
.SH OPTIONS
.SS Note
.TP
Most long options have the opposite version with \fB--no-\fR prefix.
.SS Search mode
.TP
.B "-x, --extended"
@ -42,10 +46,10 @@ it with \fB+x\fR or \fB--no-extended\fR.
.B "-e, --exact"
Enable exact-match
.TP
.B "-i"
.B "-i, --ignore-case"
Case-insensitive match (default: smart-case match)
.TP
.B "+i"
.B "+i, --no-ignore-case"
Case-sensitive match
.TP
.B "--literal"
@ -187,7 +191,7 @@ actions are affected:
\fBkill-word\fR
.TP
.BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR
Label characters for \fBjump\fR mode.
.SS Layout
.TP
.BI "--height=" "[~]HEIGHT[%]"
@ -368,20 +372,21 @@ e.g.
.TP
.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 default " Display on the next line to the prompt"
.BR right " On the right end of the horizontal separator"
.br
.BR right " Display on the right end of the next line to the prompt"
.BR hidden " Do not display finder info"
.br
.BR inline " Display on the same line with the default separator ' < '"
.BR inline " After the prompt with the default prefix ' < '"
.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 inline-right " Display on the right end of the same line
.BR inline-right " On the right end of the prompt line"
.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
.TP
@ -459,14 +464,17 @@ color mappings.
.B COLOR NAMES:
\fBfg \fRText
\fBselected-fg \fRSelected line text
\fBpreview-fg \fRPreview window text
\fBbg \fRBackground
\fBselected-bg \fRSelected line background
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBselected-hl \fRHighlighted substrings in the selected line
\fBcurrent-fg (fg+) \fRText (current line)
\fBcurrent-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line)
\fBcurrent-hl (hl+) \fRHighlighted substrings (current line)
\fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters)
@ -529,6 +537,9 @@ color mappings.
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE
.TP
.B "--highlight-line"
Highlight the whole current line
.TP
.B "--no-bold"
Do not use bold text
.TP
@ -620,7 +631,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.
e.g.
\fBfzf --preview='fzf-preview.sh {}'
\fBfzf --preview='fzf-preview.sh {}'\fR
.RE
@ -807,12 +818,22 @@ e.g.
.TP
.B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete.
the finder only after the input stream is complete.
.RS
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
.B "--with-shell=STR"
Shell command and flags to start child processes with. On *nix Systems, the
default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR.
On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
set.
.RS
e.g. \fBgem list | fzf --with-shell 'ruby -e' --preview 'pp Gem::Specification.find_by_name({1})'\fR
.RE
.TP
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method.
@ -854,8 +875,49 @@ e.g.
.B "--version"
Display version information and exit
.SS Directory traversal
.TP
.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
Note that most options have the opposite versions with \fB--no-\fR prefix.
.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. \fBsource <(fzf --zsh)\fR
.TP
.B "--fish"
Print script to set up Fish shell integration
e.g. \fBfzf --fish | source\fR
.SH ENVIRONMENT VARIABLES
.TP
@ -865,7 +927,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.
.TP
.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
.B FZF_API_KEY
Can be used to require an API key when using \fB--listen\fR option. If not set,
@ -879,6 +948,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br
.BR 2 " Error"
.br
.BR 127 " Invalid shell command for \fBbecome\fR action"
.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION
@ -919,12 +990,20 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_SELECT_COUNT " Number of selected items"
.br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_QUERY " Current query string"
.br
.BR FZF_PROMPT " Prompt string"
.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
.BR FZF_KEY " The name of the last key pressed"
.br
.BR FZF_PORT " Port number when --listen option is used"
.br
@ -995,21 +1074,21 @@ e.g.
.br
\fIctrl-]\fR
.br
\fIctrl-^\fR (\fIctrl-6\fR)
\fIctrl-^\fR (\fIctrl-6\fR)
.br
\fIctrl-/\fR (\fIctrl-_\fR)
\fIctrl-/\fR (\fIctrl-_\fR)
.br
\fIctrl-alt-[a-z]\fR
.br
\fIalt-[*]\fR (Any case-sensitive single character is allowed)
\fIalt-[*]\fR (Any case-sensitive single character is allowed)
.br
\fIf[1-12]\fR
.br
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
.br
\fIspace\fR
.br
\fIbspace\fR (\fIbs\fR)
\fIbackspace\fR (\fIbspace\fR \fIbs\fR)
.br
\fIalt-up\fR
.br
@ -1023,15 +1102,15 @@ e.g.
.br
\fIalt-space\fR
.br
\fIalt-bspace\fR (\fIalt-bs\fR)
\fIalt-backspace\fR (\fIalt-bspace\fR \fIalt-bs\fR)
.br
\fItab\fR
.br
\fIbtab\fR (\fIshift-tab\fR)
\fIshift-tab\fR (\fIbtab\fR)
.br
\fIesc\fR
.br
\fIdel\fR
\fIdelete\fR (\fIdel\fR)
.br
\fIup\fR
.br
@ -1047,9 +1126,9 @@ e.g.
.br
\fIinsert\fR
.br
\fIpgup\fR (\fIpage-up\fR)
\fIpage-up\fR (\fIpgup\fR)
.br
\fIpgdn\fR (\fIpage-down\fR)
\fIpage-down\fR (\fIpgdn\fR)
.br
\fIshift-up\fR
.br
@ -1103,6 +1182,7 @@ e.g.
\fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.RE
\fIload\fR
.RS
Triggered when the input stream is complete and the initial processing of the
@ -1112,6 +1192,7 @@ e.g.
\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
.RE
\fIresize\fR
.RS
Triggered when the terminal size is changed.
@ -1119,6 +1200,7 @@ Triggered when the terminal size is changed.
e.g.
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
.RE
\fIresult\fR
.RS
Triggered when the filtering for the current query is complete and the result list is ready.
@ -1152,6 +1234,7 @@ e.g.
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
@ -1163,6 +1246,7 @@ e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIzero\fR
.RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
@ -1184,6 +1268,31 @@ e.g.
\fBfzf --bind backward-eof:abort\fR
.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
\fIclick-header\fR
.RS
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
e.g.
\fBprintf "head1\\nhead2" | fzf --header-lines=2 --bind 'click-header:transform-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
.RE
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
@ -1202,6 +1311,8 @@ A key or an event can be bound to one or more of the following actions.
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-multi\fR (enable multi-select mode with no limit)
\fBchange-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
@ -1226,7 +1337,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
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
@ -1274,9 +1384,10 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\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
\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-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header using an external command)
@ -1286,6 +1397,7 @@ A key or an event can be bound to one or more of the following actions.
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\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
\fByank\fR \fIctrl-y\fR
@ -1358,8 +1470,6 @@ call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list

@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction
function! s:wrap_cmds(cmds)
return map([
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
return map(['@echo off']
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
\ + ['endlocal'],
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ '<SID>enc_to_cp(v:val."\r")')
endfunction
else
@ -84,11 +81,21 @@ else
endif
function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
let e = '"'
let slashes = 0
for c in split(a:arg, '\zs')
if c ==# '\'
let slashes += 1
elseif c ==# '"'
let e .= repeat('\', slashes + 1)
let slashes = 0
else
let slashes = 0
endif
let e .= c
endfor
let e .= repeat('\', slashes) .'"'
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
endfunction
function! fzf#shellescape(arg, ...)
@ -501,19 +508,19 @@ try
endif
if has_key(dict, 'source')
let source = remove(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
let source_command = source
let prefix = '('.source.')|'
elseif type == 3
let temps.input = s:fzf_tempname()
call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else
throw 'Invalid source type'
endif
else
let source_command = ''
let prefix = ''
endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@ -537,11 +544,7 @@ try
endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term
return s:execute_term(dict, command, temps)
@ -552,14 +555,6 @@ try
call s:callback(dict, lines)
return lines
finally
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry
endfunction
@ -589,8 +584,8 @@ function! s:fzf_tmux(dict)
endif
endfor
endif
return printf('LINES=%d COLUMNS=%d %s %s - --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction
function! s:splittable(dict)
@ -716,11 +711,12 @@ function! s:execute(dict, command, use_height, temps) abort
elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname()
call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let command = 'cmd.exe //C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript
endif
if a:use_height
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else
execute 'silent !'.command
endif

@ -4,37 +4,44 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.bash
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
[[ $- =~ i ]] || return 0
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -F _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() {
echo "$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 \) \
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
}
fi
if ! declare -F _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() {
command find -L "$1" \
-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
#
# _fzf_compgen_path() {
# echo "$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 \) \
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
# }
#
# _fzf_compgen_dir() {
# command find -L "$1" \
# -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@^\./@@'
# }
###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@"
@ -63,6 +70,31 @@ __fzf_orig_completion() {
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() {
local cur prev opts
COMPREPLY=()
@ -70,128 +102,77 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-h --help
-x --extended
-e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo
--scheme
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--disabled
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth
--with-nth
-s --sort
+s --no-sort
--track
--no-track
--tac
--no-tac
-i
+i
-i --ignore-case
+i --no-ignore-case
-m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse
+c --no-color
+2 --no-256
--black
--no-black
--bold
--no-bold
--layout
--reverse
--no-reverse
--cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels
-1 --select-1
+1 --no-select-1
-0 --exit-0
+0 --no-exit-0
--read0
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt
--pointer
--marker
--sync
--no-sync
--async
--no-history
--history
--history-size
--no-header
--no-header-lines
--header
--header-lines
--header-first
--no-header-first
--ellipsis
--preview
--no-preview
--preview-window
--height
--min-height
--no-height
--no-margin
--no-padding
--no-border
--border
--no-border-label
--border-label
--border-label-pos
--no-preview-label
--preview-label
--preview-label-pos
--no-unicode
--unicode
--margin
--padding
--tabstop
--listen
--no-listen
--clear
--no-clear
--version
--"
case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
@ -261,28 +242,32 @@ _fzf_opts_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"
shift
orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd"
orig="${!orig_var-}"
orig="${orig##*#}"
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
$orig "$@"
if __fzf_orig_completion_get_orig_func "$cmd"; then
"$REPLY" "$@"
elif [[ -n "${_fzf_completion_loader-}" ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@"
$_fzf_completion_loader "$@"
ret=$?
# _completion_loader may not have updated completion for the command
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__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
eval "${orig_complete/ -F / -o nospace -F }"
else
eval "$orig_complete"
fi
fi
[[ $ret -eq 0 ]] && return 124
return $ret
fi
}
@ -293,7 +278,6 @@ __fzf_generic_path_completion() {
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
fi
cmd="${cmd//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
@ -309,9 +293,24 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/}
[[ -z "$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
printf "%q " "${item%$3}$3"
done)
matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
if [[ $1 =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
)
matches=${matches% }
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n "$matches" ]]; then
@ -359,13 +358,16 @@ _fzf_complete() {
post="$(caller 0 | command awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post='command cat'
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cmd="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")
@ -469,8 +471,11 @@ complete -o default -F _fzf_opts_completion fzf
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
# undocumented and subject to change in the future.
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
awk bat cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby
@ -478,25 +483,31 @@ a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp
svn tar unzip zip"
svn tar unzip zip"}"
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
# Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
if type _comp_load > /dev/null 2>&1; then
# _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
__fzf_defc() {
local cmd func opts orig_var orig def
local cmd func opts REPLY
cmd="$1"
func="$2"
opts="$3"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}"
if [[ -n "$orig" ]]; then
printf -v def "$orig" "$func"
eval "$def"
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
eval "$REPLY"
else
complete -F "$func" $opts "$cmd"
fi
@ -509,13 +520,24 @@ done
# Directory
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
# Variables
for cmd in $v_cmds; do
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
done
# Aliases
__fzf_defc unalias _fzf_alias_completion "-a"
# Processes
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
# ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds
unset cmd d_cmds a_cmds v_cmds
_fzf_setup_completion() {
local kind fn cmd
@ -537,8 +559,4 @@ _fzf_setup_completion() {
done
}
# Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill
fi

@ -4,12 +4,12 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.zsh
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
# Both branches of the following `if` do the same thing -- define
@ -75,27 +75,35 @@ fi
# This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options.
{
# The 'emulate' command should not be placed inside the interactive if check;
# placing it there fails to disable alias expansion. See #3731.
if [[ -o interactive ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() {
echo "$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 \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() {
command find -L "$1" \
-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
#
# _fzf_compgen_path() {
# echo "$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 \) \
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
# }
#
# _fzf_compgen_dir() {
# command find -L "$1" \
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
# }
###########################################################
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@"
@ -148,10 +156,25 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/}
[ -z "$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
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
done)
matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
if [[ $compgen =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
done
)
matches=${matches% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
@ -211,7 +234,10 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo"
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
@ -309,7 +335,7 @@ fzf-completion() {
# Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@ -339,6 +365,7 @@ fzf-completion() {
zle -N fzf-completion
bindkey '^I' fzf-completion
fi
} always {
# Restore the original options.

@ -11,20 +11,24 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0
if [[ $- =~ i ]]; then
# Key bindings
# ------------
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_select__() {
local cmd 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 \
-o -type f -print \
-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) "$@" |
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
while read -r item; do
printf '%q ' "$item" # escape special chars
done
@ -42,23 +46,24 @@ fzf-file-widget() {
}
__fzf_cd__() {
local cmd 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 \
-o -type d -print 2> /dev/null | command cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
local dir
dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
}
if command -v perl > /dev/null; then
__fzf_history__() {
local output opts script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
@ -69,14 +74,13 @@ if command -v perl > /dev/null; then
}
else # awk - fallback for POSIX systems
__fzf_history__() {
local output opts script n x y z d
local output script n x y z d
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next }
@ -87,7 +91,8 @@ else # awk - fallback for POSIX systems
set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
@ -107,9 +112,11 @@ bind -m emacs-standard '"\C-z": vi-editing-mode'
if (( BASH_VERSINFO[0] < 4 )); then
# 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"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
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-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
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
@ -117,9 +124,11 @@ if (( BASH_VERSINFO[0] < 4 )); then
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard -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'
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m emacs-standard -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
bind -m emacs-standard -x '"\C-r": __fzf_history__'
@ -128,6 +137,10 @@ else
fi
# 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"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
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-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
fi

@ -18,25 +18,28 @@ status is-interactive; or exit 0
# ------------
function fzf_key_bindings
function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
end
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
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%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --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_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
@ -56,7 +59,8 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
@ -77,17 +81,16 @@ function fzf_key_bindings
function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
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%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --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_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ]
cd -- $result
@ -113,14 +116,22 @@ function fzf_key_bindings
end
end
bind \ct fzf-file-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
bind -M insert \ct fzf-file-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
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'

@ -11,8 +11,6 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0
# Key bindings
# ------------
@ -38,16 +36,23 @@ fi
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{
if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
# CTRL-T - Paste the selected file path(s) into the command line
__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-"}"
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
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=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
echo -n "${(q)item} "
done
local ret=$?
@ -61,45 +66,51 @@ __fzfcmd() {
}
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$?
zle reset-prompt
return $ret
}
zle -N fzf-file-widget
bindkey -M emacs '^T' fzf-file-widget
bindkey -M vicmd '^T' fzf-file-widget
bindkey -M viins '^T' fzf-file-widget
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
zle -N fzf-file-widget
bindkey -M emacs '^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
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
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=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
if [[ -z "$dir" ]]; then
zle redisplay
return 0
fi
zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="builtin cd -- ${(q)dir}"
BUFFER="builtin cd -- ${(q)dir:a}"
zle accept-line
local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion
zle reset-prompt
return $ret
}
zle -N fzf-cd-widget
bindkey -M emacs '\ec' fzf-cd-widget
bindkey -M vicmd '\ec' fzf-cd-widget
bindkey -M viins '\ec' fzf-cd-widget
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
zle -N fzf-cd-widget
bindkey -M emacs '\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
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))"
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
local ret=$?
if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected")
@ -116,6 +127,7 @@ zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
fi
} always {
eval $__fzf_key_bindings_options

@ -26,100 +26,104 @@ func _() {
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleHeader-46]
_ = x[actTrack-47]
_ = x[actDown-48]
_ = x[actUp-49]
_ = x[actPageUp-50]
_ = x[actPageDown-51]
_ = x[actPosition-52]
_ = x[actHalfPageUp-53]
_ = x[actHalfPageDown-54]
_ = x[actOffsetUp-55]
_ = x[actOffsetDown-56]
_ = x[actJump-57]
_ = x[actJumpAccept-58]
_ = x[actPrintQuery-59]
_ = x[actRefreshPreview-60]
_ = x[actReplaceQuery-61]
_ = x[actToggleSort-62]
_ = x[actShowPreview-63]
_ = x[actHidePreview-64]
_ = x[actTogglePreview-65]
_ = x[actTogglePreviewWrap-66]
_ = x[actTransform-67]
_ = x[actTransformBorderLabel-68]
_ = x[actTransformHeader-69]
_ = x[actTransformPreviewLabel-70]
_ = x[actTransformPrompt-71]
_ = x[actTransformQuery-72]
_ = x[actPreview-73]
_ = x[actChangePreview-74]
_ = x[actChangePreviewWindow-75]
_ = x[actPreviewTop-76]
_ = x[actPreviewBottom-77]
_ = x[actPreviewUp-78]
_ = x[actPreviewDown-79]
_ = x[actPreviewPageUp-80]
_ = x[actPreviewPageDown-81]
_ = x[actPreviewHalfPageUp-82]
_ = x[actPreviewHalfPageDown-83]
_ = x[actPrevHistory-84]
_ = x[actPrevSelected-85]
_ = x[actPut-86]
_ = x[actNextHistory-87]
_ = x[actNextSelected-88]
_ = x[actExecute-89]
_ = x[actExecuteSilent-90]
_ = x[actExecuteMulti-91]
_ = x[actSigStop-92]
_ = x[actFirst-93]
_ = x[actLast-94]
_ = x[actReload-95]
_ = x[actReloadSync-96]
_ = x[actDisableSearch-97]
_ = x[actEnableSearch-98]
_ = x[actSelect-99]
_ = x[actDeselect-100]
_ = x[actUnbind-101]
_ = x[actRebind-102]
_ = x[actBecome-103]
_ = x[actResponse-104]
_ = x[actShowHeader-105]
_ = x[actHideHeader-106]
_ = x[actChangeMulti-18]
_ = x[actChangePreviewLabel-19]
_ = x[actChangePrompt-20]
_ = x[actChangeQuery-21]
_ = x[actClearScreen-22]
_ = x[actClearQuery-23]
_ = x[actClearSelection-24]
_ = x[actClose-25]
_ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28]
_ = x[actFatal-29]
_ = x[actForwardChar-30]
_ = x[actForwardWord-31]
_ = x[actKillLine-32]
_ = x[actKillWord-33]
_ = x[actUnixLineDiscard-34]
_ = x[actUnixWordRubout-35]
_ = x[actYank-36]
_ = x[actBackwardKillWord-37]
_ = x[actSelectAll-38]
_ = x[actDeselectAll-39]
_ = x[actToggle-40]
_ = x[actToggleSearch-41]
_ = x[actToggleAll-42]
_ = x[actToggleDown-43]
_ = x[actToggleUp-44]
_ = x[actToggleIn-45]
_ = x[actToggleOut-46]
_ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49]
_ = x[actTrackCurrent-50]
_ = x[actUntrackCurrent-51]
_ = x[actDown-52]
_ = x[actUp-53]
_ = x[actPageUp-54]
_ = x[actPageDown-55]
_ = x[actPosition-56]
_ = x[actHalfPageUp-57]
_ = x[actHalfPageDown-58]
_ = x[actOffsetUp-59]
_ = x[actOffsetDown-60]
_ = x[actJump-61]
_ = x[actJumpAccept-62]
_ = x[actPrintQuery-63]
_ = x[actRefreshPreview-64]
_ = x[actReplaceQuery-65]
_ = x[actToggleSort-66]
_ = x[actShowPreview-67]
_ = x[actHidePreview-68]
_ = x[actTogglePreview-69]
_ = x[actTogglePreviewWrap-70]
_ = x[actTransform-71]
_ = x[actTransformBorderLabel-72]
_ = x[actTransformHeader-73]
_ = x[actTransformPreviewLabel-74]
_ = x[actTransformPrompt-75]
_ = x[actTransformQuery-76]
_ = x[actPreview-77]
_ = x[actChangePreview-78]
_ = x[actChangePreviewWindow-79]
_ = x[actPreviewTop-80]
_ = x[actPreviewBottom-81]
_ = x[actPreviewUp-82]
_ = x[actPreviewDown-83]
_ = x[actPreviewPageUp-84]
_ = x[actPreviewPageDown-85]
_ = x[actPreviewHalfPageUp-86]
_ = x[actPreviewHalfPageDown-87]
_ = x[actPrevHistory-88]
_ = x[actPrevSelected-89]
_ = x[actPut-90]
_ = x[actNextHistory-91]
_ = x[actNextSelected-92]
_ = x[actExecute-93]
_ = x[actExecuteSilent-94]
_ = x[actExecuteMulti-95]
_ = x[actSigStop-96]
_ = x[actFirst-97]
_ = x[actLast-98]
_ = x[actReload-99]
_ = x[actReloadSync-100]
_ = x[actDisableSearch-101]
_ = x[actEnableSearch-102]
_ = x[actSelect-103]
_ = x[actDeselect-104]
_ = x[actUnbind-105]
_ = x[actRebind-106]
_ = x[actBecome-107]
_ = x[actResponse-108]
_ = x[actShowHeader-109]
_ = x[actHideHeader-110]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
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, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {

@ -152,7 +152,13 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite
initialCharClass = 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
@ -187,6 +193,27 @@ func Init(scheme string) bool {
default:
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
}
@ -214,21 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
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 {
if unicode.IsLower(char) {
return charLower
@ -248,31 +260,36 @@ func charClassOfNonAscii(char rune) charClass {
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return charClassOfAscii(char)
return asciiCharClasses[char]
}
return charClassOfNonAscii(char)
}
func bonusFor(prevClass charClass, class charClass) int16 {
if class > charNonWord {
if prevClass == charWhite {
switch prevClass {
case charWhite:
// Word boundary after whitespace
return bonusBoundaryWhite
} else if prevClass == charDelimiter {
case charDelimiter:
// Word boundary after a delimiter character
return bonusBoundaryDelimiter
} else if prevClass == charNonWord {
case charNonWord:
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
} else if class == charNonWord {
}
switch class {
case charNonWord, charDelimiter:
return bonusNonWord
} else if class == charWhite {
case charWhite:
return bonusBoundaryWhite
}
return 0
@ -282,7 +299,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 {
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 {
@ -335,30 +352,45 @@ func isAscii(runes []rune) bool {
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
if !input.IsBytes() {
return 0
return 0, input.Length()
}
// Not possible
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++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 {
return -1
return -1, -1
}
if pidx == 0 && idx > 0 {
// Step back to find the right bonus point
firstIdx = idx - 1
}
lastIdx = 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) {
@ -407,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return Result{0, 0, 0}, posArray(withPos, M)
}
N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Since O(nm) algorithm can be prohibitively expensive for large input,
// 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
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 {
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if minIdx < 0 {
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
offset16 := 0
@ -431,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
offset32, F := alloc32(offset32, slab, M)
// Rune array
_, T := alloc32(offset32, slab, N)
input.CopyRunes(T)
input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
for off, char := range T {
var class charClass
if char <= unicode.MaxASCII {
class = charClassOfAscii(char)
class = asciiCharClasses[char]
if !caseSensitive && class == charUpper {
char += 32
T[off] = char
}
} else {
class = charClassOfNonAscii(char)
@ -454,28 +490,28 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if normalize {
char = normalizeRune(char)
}
T[off] = char
}
Tsub[off] = char
bonus := bonusFor(prevClass, class)
Bsub[off] = bonus
bonus := bonusMatrix[prevClass][class]
B[off] = bonus
prevClass = class
if char == pchar {
if pidx < M {
F[pidx] = int32(idx + off)
F[pidx] = int32(off)
pidx++
pchar = pattern[util.Min(pidx, M-1)]
}
lastIdx = idx + off
lastIdx = off
}
if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score
C0sub[off] = 1
H0[off] = score
C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off
maxScore, maxScorePos = score, off
if forward && bonus >= bonusBoundary {
break
}
@ -483,24 +519,24 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false
} else {
if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
}
C0sub[off] = 0
C0[off] = 0
inGap = true
}
prevH0 = H0sub[off]
prevH0 = H0[off]
}
if pidx != M {
return Result{-1, -1, 0}, nil
}
if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
if !withPos {
return result, nil
}
pos := []int{maxScorePos}
pos := []int{minIdx + maxScorePos}
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) {
*pos = append(*pos, j)
*pos = append(*pos, j+minIdx)
if i == 0 {
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.
// However finding the accurate offset requires backtracking, and we don't
// 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
@ -640,7 +676,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
*pos = append(*pos, idx)
}
score += scoreMatch
bonus := bonusFor(prevClass, class)
bonus := bonusMatrix[prevClass][class]
if consecutive == 0 {
firstBonus = bonus
} else {
@ -678,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
if len(pattern) == 0 {
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
}
@ -772,7 +809,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
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
}

@ -9,6 +9,10 @@ import (
"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) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
}

@ -3,7 +3,7 @@
package algo
var normalized map[rune]rune = map[rune]rune{
var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER

@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string
i := -1
var i int
if delimiter == 0 {
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
@ -312,7 +312,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range []byte(s) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0
ptr := &state.fg
var delimiter byte = 0
var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int

@ -342,8 +342,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
state := interpretCode(code, prevState)
if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
}
}
assert("\x1b[m", nil, "")

@ -12,8 +12,14 @@ type ChunkCache struct {
}
// NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
func NewChunkCache() *ChunkCache {
return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
func (cc *ChunkCache) Clear() {
cc.mutex.Lock()
cc.cache = make(map[*Chunk]*queryCache)
cc.mutex.Unlock()
}
// Add adds the list to the cache

@ -2,7 +2,6 @@ package fzf
import (
"math"
"os"
"time"
"github.com/junegunn/fzf/src/util"
@ -15,6 +14,7 @@ const (
// Reader
readerBufferSize = 64 * 1024
readerSlabSize = 128 * 1024
readerPollIntervalMin = 10 * time.Millisecond
readerPollIntervalStep = 5 * time.Millisecond
readerPollIntervalMax = 50 * time.Millisecond
@ -54,16 +54,6 @@ const (
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
const (
EvtReadNew util.EventType = iota
@ -77,9 +67,9 @@ const (
)
const (
exitCancel = -1
exitOk = 0
exitNoMatch = 1
exitError = 2
exitInterrupt = 130
ExitCancel = -1
ExitOk = 0
ExitNoMatch = 1
ExitError = 2
ExitInterrupt = 130
)

@ -2,8 +2,7 @@
package fzf
import (
"fmt"
"os"
"sync"
"time"
"github.com/junegunn/fzf/src/util"
@ -19,19 +18,23 @@ Matcher -> EvtHeader -> Terminal (update header)
*/
// Run starts fzf
func Run(opts *Options, version string, revision string) {
sort := opts.Sort > 0
sortCriteria = opts.Criteria
func Run(opts *Options) (int, error) {
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
os.Exit(exitOk)
}
sort := opts.Sort > 0
sortCriteria = opts.Criteria
// Event channel
eventBox := util.NewEventBox()
@ -45,16 +48,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars([]byte(trimmed)), offsets
return util.ToChars(stringBytes(trimmed)), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil)
return util.ToChars([]byte(trimmed)), nil
trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(stringBytes(trimmed)), nil
}
}
}
@ -66,7 +69,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, string(data))
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
@ -77,7 +80,7 @@ func Run(opts *Options, version string, revision string) {
})
} else {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(string(data), opts.Delimiter)
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
@ -101,7 +104,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor([]byte(transformed))
item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
@ -110,14 +113,17 @@ func Run(opts *Options, version string, revision string) {
})
}
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource()
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}
// Matcher
@ -133,14 +139,16 @@ func Run(opts *Options, version string, revision string) {
forward = true
}
}
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode
if opts.Filter != nil {
@ -154,18 +162,21 @@ func Run(opts *Options, version string, revision string) {
found := false
if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size)
mutex := sync.Mutex{}
reader := NewReader(
func(runes []byte) bool {
item := Item{}
if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
found = true
}
mutex.Unlock()
}
return false
}, eventBox, opts.ReadZero, false)
reader.ReadSource()
}, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
@ -180,9 +191,9 @@ func Run(opts *Options, version string, revision string) {
}
}
if found {
os.Exit(exitOk)
return ExitOk, nil
}
os.Exit(exitNoMatch)
return ExitNoMatch, nil
}
// Synchronous search
@ -193,9 +204,13 @@ func Run(opts *Options, version string, revision string) {
// Go interactive
go matcher.Loop()
defer matcher.Stop()
// Terminal I/O
terminal := NewTerminal(opts, eventBox)
terminal, err := NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0
heightUnknown := opts.Height.auto
@ -212,7 +227,8 @@ func Run(opts *Options, version string, revision string) {
// Event coordination
reading := true
ticks := 0
var nextCommand *string
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
total := 0
query := []rune{}
@ -232,23 +248,23 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string) {
restart := func(command commandSpec, environ []string) {
reading = true
chunkList.Clear()
itemIndex = 0
inputRevision++
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
go reader.restart(command, environ)
}
exitCode := ExitOk
stop := false
for {
delay := true
ticks++
input := func() []rune {
reloaded := snapshotRevision != inputRevision
paused, input := terminal.Input()
if reloaded && paused {
query = []rune{}
} else if !paused {
if !paused {
query = input
}
return query
@ -263,11 +279,16 @@ func Run(opts *Options, version string, revision string) {
if reading {
reader.terminate()
}
os.Exit(value.(int))
quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand)
restart(*nextCommand, nextEnviron)
nextCommand = nil
nextEnviron = nil
break
} else {
reading = reading && evt == EvtReadNew
@ -276,14 +297,16 @@ func Run(opts *Options, version string, revision string) {
useSnapshot = false
}
if !useSnapshot {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
}
if heightUnknown && !deferred {
determine(!reading)
@ -291,12 +314,14 @@ func Run(opts *Options, version string, revision string) {
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew:
var command *string
var command *commandSpec
var environ []string
var changed bool
switch val := value.(type) {
case searchRequest:
sort = val.sort
command = val.command
environ = val.environ
changed = val.changed
if command != nil {
useSnapshot = val.sync
@ -306,18 +331,22 @@ func Run(opts *Options, version string, revision string) {
if reading {
reader.terminate()
nextCommand = command
nextEnviron = environ
} else {
restart(*command)
restart(*command, environ)
}
}
if !changed {
break
}
if !useSnapshot {
newSnapshot, _ := chunkList.Snapshot()
newSnapshot, newCount := chunkList.Snapshot()
// 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
if command == nil || len(newSnapshot) > 0 {
if command == nil || newCount > 0 {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot = newSnapshot
snapshotRevision = inputRevision
}
@ -354,20 +383,24 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
}
if count > 0 {
os.Exit(exitOk)
if count == 0 {
exitCode = ExitNoMatch
}
os.Exit(exitNoMatch)
stop = true
return
}
determine(val.final)
}
}
terminal.UpdateList(val)
terminal.UpdateList(val, true)
}
}
}
events.Clear()
})
if stop {
break
}
if delay && reading {
dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep,
@ -375,4 +408,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur)
}
}
return exitCode, err
}

@ -0,0 +1,35 @@
package fzf
import (
"os"
"strings"
"unsafe"
)
func writeTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil {
// Unable to create temporary file
// FIXME: Should we terminate the program?
return ""
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
return f.Name()
}
func removeFiles(files []string) {
for _, filename := range files {
os.Remove(filename)
}
}
func stringBytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
func byteString(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}

@ -21,6 +21,7 @@ type MatchRequest struct {
// Matcher is responsible for performing search
type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern
sort bool
tac bool
@ -38,10 +39,11 @@ const (
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
sort: sort,
tac: tac,
@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for {
var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events {
for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) {
case MatchRequest:
request = val
@ -71,12 +78,15 @@ func (m *Matcher) Loop() {
}
events.Clear()
})
if stop {
break
}
if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
m.cache.Clear()
}
// Restart search
@ -236,3 +246,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
}
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
}
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}

File diff suppressed because it is too large Load Diff

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

@ -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
}

@ -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)
}
}
}

@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) {
func TestSplitNth(t *testing.T) {
{
ranges := splitNth("..")
ranges, _ := splitNth("..")
if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis {
@ -88,7 +88,7 @@ func TestSplitNth(t *testing.T) {
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@ -137,7 +137,7 @@ func TestIrrelevantNth(t *testing.T) {
}
func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) {
if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s)
@ -163,35 +163,35 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
check(tui.CtrlM, "Return")
checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab")
check(tui.BTab, "btab")
check(tui.ESC, "esc")
check(tui.ShiftTab, "btab")
check(tui.Esc, "esc")
check(tui.Up, "up")
check(tui.Down, "down")
check(tui.Left, "left")
check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 {
t.Error(11)
}
check(tui.Tab, "Ctrl-I")
check(tui.PgUp, "page-up")
check(tui.PgDn, "Page-Down")
check(tui.PageUp, "page-up")
check(tui.PageDown, "Page-Down")
check(tui.Home, "Home")
check(tui.End, "End")
check(tui.AltBS, "Alt-BSpace")
check(tui.SLeft, "shift-left")
check(tui.SRight, "shift-right")
check(tui.BTab, "shift-tab")
check(tui.AltBackspace, "Alt-BSpace")
check(tui.ShiftLeft, "shift-left")
check(tui.ShiftRight, "shift-right")
check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter")
check(tui.BSpace, "bspace")
check(tui.Backspace, "bspace")
}
func TestParseKeysWithComma(t *testing.T) {
@ -206,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) {
}
}
pairs := parseKeyChords(",", "")
pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "")
pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "")
pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "")
pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "")
pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "")
pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "")
pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,")
}
@ -262,17 +262,13 @@ func TestBind(t *testing.T) {
}
}
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp)
@ -290,20 +286,17 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn)
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
}
parseKeymap(keymap, "f1:abort", errorFn)
parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
}
func TestColorSpec(t *testing.T) {
theme := tui.Dark256
dark := parseTheme(theme, "dark")
dark, _ := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
@ -311,7 +304,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
light, _ := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
@ -322,7 +315,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized")
}
@ -335,7 +328,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized")
}
@ -475,7 +468,7 @@ func TestValidateSign(t *testing.T) {
}
func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions))
}
@ -491,11 +484,8 @@ func TestParseSingleActionList(t *testing.T) {
}
func TestParseSingleActionListError(t *testing.T) {
err := ""
parseSingleActionList("change-query(foobar)baz", func(e string) {
err = e
})
if len(err) == 0 {
_, err := parseSingleActionList("change-query(foobar)baz")
if err == nil {
t.Errorf("Failed to detect error")
}
}

@ -60,32 +60,17 @@ type Pattern struct {
delimiter Delimiter
nth []Range
procFun map[termType]algo.Algo
cache *ChunkCache
}
var (
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
var _splitRegex *regexp.Regexp
func init() {
_splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
}
func clearChunkCache() {
_cache = NewChunkCache()
}
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string
@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes)
}
cached, found := _patternCache[asString]
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
cached, found := patternCache[asString]
if found {
return cached
}
@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
cacheable: cacheable,
nth: nth,
delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey()
@ -162,19 +150,19 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr
patternCache[asString] = ptr
return ptr
}
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1)
str = strings.ReplaceAll(str, "\\ ", "\t")
tokens := _splitRegex.Split(str, -1)
sets := []termSet{}
set := termSet{}
switchSet := false
afterBar := false
for _, token := range tokens {
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
@ -209,11 +197,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// Flip exactness
if fuzzy && !inv {
typ = termExact
text = text[1:]
} else {
typ = termFuzzy
text = text[1:]
}
text = text[1:]
} else if strings.HasPrefix(text, "^") {
if typ == termSuffix {
typ = termEqual
@ -283,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match
cacheKey := p.CacheKey()
if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached
}
}
// Prefix/suffix cache
space := _cache.Search(chunk, cacheKey)
space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab)
if p.cacheable {
_cache.Add(chunk, cacheKey, matches)
p.cache.Add(chunk, cacheKey, matches)
}
return matches
}

@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
}
}
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
}
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive(
@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
}
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str))
@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
}
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}})
@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if pat.cacheable != cacheable {
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
}
clearPatternCache()
}
test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true)
@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
}
clearPatternCache()
}
test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false)

@ -3,6 +3,4 @@
package protector
// Protect calls OS specific protections like pledge on OpenBSD
func Protect() {
return
}
func Protect() {}

@ -1,24 +1,24 @@
package fzf
import (
"bufio"
"bytes"
"context"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/charlievieth/fastwalk"
"github.com/junegunn/fzf/src/util"
"github.com/saracen/walker"
)
// Reader reads from command or standard input
type Reader struct {
pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox
delimNil bool
event int32
@ -31,8 +31,8 @@ type Reader struct {
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@ -77,48 +77,49 @@ func (r *Reader) fin(success bool) {
func (r *Reader) terminate() {
r.mutex.Lock()
defer func() { r.mutex.Unlock() }()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
util.KillCommand(r.exec)
} else if defaultCommand != "" {
} else {
os.Stdin.Close()
}
r.mutex.Unlock()
}
func (r *Reader) restart(command string) {
func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(nil, command)
success := r.readFromCommand(command.command, environ)
r.fin(success)
removeFiles(command.tempFiles)
}
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource() {
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller()
var success bool
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
}
}
if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
if defaultCommand != "" {
success = r.readFromCommand(&shell, defaultCommand)
} else {
success = r.readFiles()
}
success = r.readFiles(root, opts, ignores)
} else {
success = r.readFromCommand(nil, cmd)
// We can't export FZF_* environment variables to the default command
success = r.readFromCommand(cmd, nil)
}
} else {
success = r.readFromStdin()
@ -127,32 +128,90 @@ func (r *Reader) ReadSource() {
}
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')
trimCR := util.IsWindows()
if r.delimNil {
delim = '\000'
trimCR = false
}
reader := bufio.NewReaderSize(src, readerBufferSize)
slab := make([]byte, readerSlabSize)
leftover := []byte{}
var err error
for {
// ReadBytes returns err != nil if and only if the returned data does not
// end in delim.
bytea, err := reader.ReadBytes(delim)
byteaLen := len(bytea)
if byteaLen > 0 {
if err == nil {
// get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
bytea = bytea[:byteaLen-2]
n := 0
scope := slab[:util.Min(len(slab), readerBufferSize)]
for i := 0; i < 100; i++ {
n, err = src.Read(scope)
if n > 0 || err != nil {
break
}
}
// We're not making any progress after 100 tries. Stop.
if n == 0 {
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 {
bytea = bytea[:byteaLen-1]
slice = slice[:len(slice)-1]
}
}
if r.pusher(bytea) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
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 != nil {
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 +220,28 @@ func (r *Reader) readFromStdin() bool {
return true
}
func (r *Reader) readFiles() bool {
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
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)
if path != "." {
isDir := mode.Mode().IsDir()
if isDir && filepath.Base(path)[0] == '.' {
return filepath.SkipDir
isDir := de.IsDir()
if isDir {
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))
}
}
@ -181,20 +252,16 @@ func (r *Reader) readFiles() bool {
}
return nil
}
cb := walker.WithErrorCallback(func(pathname string, err error) error {
return nil
})
return walker.Walk(".", fn, cb) == nil
return fastwalk.Walk(&conf, root, fn) == nil
}
func (r *Reader) readFromCommand(shell *string, command string) bool {
func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
if shell != nil {
r.exec = util.ExecCommandWith(*shell, command, true)
} else {
r.exec = util.ExecCommand(command, true)
r.exec = r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
}
out, err := r.exec.StdoutPipe()
if err != nil {

@ -10,9 +10,10 @@ import (
func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
exec := util.NewExecutor("")
reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true },
eb, false, true)
eb, exec, false, true)
reader.startEventPoller()
@ -22,7 +23,7 @@ func TestReadFromCommand(t *testing.T) {
}
// 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" {
t.Errorf("%s", strs)
}
@ -47,7 +48,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
reader.fin(reader.readFromCommand(nil, `no-such-command`))
reader.fin(reader.readFromCommand(`no-such-command`, nil))
strs = []string{}
if len(strs) > 0 {
t.Errorf("%s", strs)

@ -32,6 +32,7 @@ const (
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
httpReadTimeout = 10 * time.Second
channelTimeout = 2 * time.Second
jsonContentType = "Content-Type: application/json" + crlf
maxContentLength = 1024 * 1024
)
@ -53,47 +54,47 @@ func (addr listenAddress) IsLocal() bool {
var defaultListenAddr = listenAddress{"localhost", 0}
func parseListenAddress(address string) (error, listenAddress) {
func parseListenAddress(address string) (listenAddress, error) {
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
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]
port, err := strconv.Atoi(portStr)
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 {
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) (net.Listener, int, error) {
host := address.host
port := address.port
apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 {
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
}
addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr)
if err != nil {
return fmt.Errorf("failed to listen on %s", addrStr), port
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return err, port
return nil, port, err
}
}
@ -108,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
return
}
continue
}
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
listener.Close()
}()
return nil, port
return listener, port, nil
}
// Here we are writing a simplistic HTTP server without using net/http
@ -170,7 +169,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
select {
case response := <-server.responseChannel:
return good(response)
case <-time.After(2 * time.Second):
case <-time.After(channelTimeout):
go func() {
// Drain the channel
<-server.responseChannel
@ -216,18 +215,19 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
if err != nil {
return bad(err.Error())
}
if len(actions) == 0 {
return bad("no action specified")
}
server.actionChannel <- actions
select {
case server.actionChannel <- actions:
case <-time.After(channelTimeout):
return httpUnavailable + crlf
}
return httpOk + crlf
}
@ -237,15 +237,13 @@ func parseGetParams(query string) getParams {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
switch parts[0] {
case "limit":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.limit = val
}
case "offset":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.offset = val
case "limit", "offset":
if val, err := strconv.Atoi(parts[1]); err == nil {
if parts[0] == "limit" {
params.limit = val
} else {
params.offset = val
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -13,7 +13,7 @@ import (
)
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{
replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
delimiter: delimiter,
@ -23,7 +23,9 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
executor: util.NewExecutor(""),
})
return replaced
}
func TestReplacePlaceholder(t *testing.T) {
@ -244,6 +246,7 @@ func TestQuoteEntry(t *testing.T) {
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes
exec := util.NewExecutor("")
if util.IsWindows() {
effectiveStyle = windowsStyle
@ -278,7 +281,7 @@ func TestQuoteEntry(t *testing.T) {
}
for input, expected := range tests {
escaped := quoteEntry(input)
escaped := exec.QuoteEntry(input)
expected = templateToString(expected, effectiveStyle)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
@ -317,9 +320,9 @@ func TestUnixCommands(t *testing.T) {
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) {
if !util.IsWindows() {
t.SkipNow()
}
// XXX Deprecated
t.SkipNow()
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}

@ -5,26 +5,11 @@ package fzf
import (
"os"
"os/signal"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
var escaper *strings.Replacer
func init() {
tokens := strings.Split(os.Getenv("SHELL"), "/")
if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
}
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}
@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}
func quoteEntry(entry string) string {
return "'" + escaper.Replace(entry) + "'"
}

@ -4,8 +4,6 @@ package fzf
import (
"os"
"regexp"
"strings"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}
func quoteEntry(entry string) string {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
if strings.Contains(shell, "cmd") {
// backslash escaping is done here for applications
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
// caret is the escape character for cmd shell
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}

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

@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, Delimiter{})
{
ranges := splitNth("1,2,3")
ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
{
ranges := splitNth("1..2,3,2..,1")
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, delimiterRegexp(":"))
{
ranges := splitNth("1..2,3,2..,1")
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
}
func TestTransformIndexOutOfBounds(t *testing.T) {
Transform([]Token{}, splitNth("1"))
s, _ := splitNth("1")
Transform([]Token{}, s)
}

@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape BorderShape = BorderRounded
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
@ -29,13 +29,14 @@ const (
StrikeThrough = Attr(1 << 7)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) PassThrough(string) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }

@ -0,0 +1,121 @@
// 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[Fatal-75]
_ = x[Mouse-76]
_ = x[DoubleClick-77]
_ = x[LeftClick-78]
_ = x[RightClick-79]
_ = x[SLeftClick-80]
_ = x[SRightClick-81]
_ = x[ScrollUp-82]
_ = x[ScrollDown-83]
_ = x[SScrollUp-84]
_ = x[SScrollDown-85]
_ = x[PreviewScrollUp-86]
_ = x[PreviewScrollDown-87]
_ = x[Resize-88]
_ = x[Change-89]
_ = x[BackwardEOF-90]
_ = x[Start-91]
_ = x[Load-92]
_ = x[Focus-93]
_ = x[One-94]
_ = x[Zero-95]
_ = x[Result-96]
_ = x[Jump-97]
_ = x[JumpCancel-98]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
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, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
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]]
}

@ -2,6 +2,7 @@ package tui
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
@ -10,7 +11,8 @@ import (
"time"
"unicode/utf8"
"github.com/junegunn/uniseg"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
)
@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
@ -71,13 +73,14 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() {
if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset()
}
}
// Light renderer
type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme
mouse bool
forceBlack bool
@ -85,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time
clicks [][2]int
ttyin *os.File
ttyout *os.File
buffer []byte
origState *term.State
width int
@ -123,19 +127,29 @@ type LightWindow struct {
bg Color
}
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn()
if err != nil {
return nil, err
}
out, err := openTtyOut()
if err != nil {
out = os.Stderr
}
r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
clearOnExit: clearOnExit,
ttyin: openTtyIn(),
ttyin: in,
ttyout: out,
yoffset: 0,
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
return &r
return &r, nil
}
func repeat(r rune, times int) string {
@ -153,11 +167,11 @@ func atoi(s string, defaultValue int) int {
return value
}
func (r *LightRenderer) Init() {
func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil {
errorExit(err.Error())
return err
}
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@ -195,6 +209,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
return nil
}
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@ -233,19 +248,20 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer, false)
func (r *LightRenderer) getBytes() ([]byte, error) {
bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
}
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
return nil, errors.New("failed to read " + consoleDevice)
}
retries := 0
if c == ESC.Int() || nonblock {
if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
@ -260,7 +276,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue
}
break
} else if c == ESC.Int() && pc != c {
} else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval
} else {
retries = 0
@ -272,19 +288,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
return buffer
return buffer, nil
}
func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 {
r.buffer = r.getBytes()
r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
}
if len(r.buffer) == 0 {
panic("Empty buffer")
return Event{Fatal, 0, nil}
}
sz := 1
@ -300,7 +320,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte():
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case 28:
@ -311,11 +331,13 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
case ESC.Byte():
case Esc.Byte():
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer = r.getBytes()
if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz)
}
return ev
@ -327,7 +349,7 @@ func (r *LightRenderer) GetChar() Event {
}
char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError {
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
sz = rsz
return Event{Rune, char, nil}
@ -335,7 +357,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
loc := offsetRegexpBegin.FindIndex(r.buffer)
@ -349,15 +371,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
}
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:]
alt = true
}
switch r.buffer[1] {
case ESC.Byte():
return Event{ESC, 0, nil}
case Esc.Byte():
return Event{Esc, 0, nil}
case 127:
return Event{AltBS, 0, nil}
return Event{AltBackspace, 0, nil}
case '[', 'O':
if len(r.buffer) < 3 {
return Event{Invalid, 0, nil}
@ -386,7 +408,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
return Event{Up, 0, nil}
case 'Z':
return Event{BTab, 0, nil}
return Event{ShiftTab, 0, nil}
case 'H':
return Event{Home, 0, nil}
case 'F':
@ -434,7 +456,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} // INS
case '3':
if r.buffer[3] == '~' {
return Event{Del, 0, nil}
return Event{Delete, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
@ -442,16 +464,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{SDelete, 0, nil}
return Event{ShiftDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4':
return Event{End, 0, nil}
case '5':
return Event{PgUp, 0, nil}
return Event{PageUp, 0, nil}
case '6':
return Event{PgDn, 0, nil}
return Event{PageDown, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
@ -489,16 +511,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
*sz = 6
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'
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5]
if altShift {
altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
}
*sz = 7
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 {
case 'A':
@ -506,33 +541,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil}
}
if altShift {
return Event{AltSUp, 0, nil}
return Event{AltShiftUp, 0, nil}
}
return Event{SUp, 0, nil}
return Event{ShiftUp, 0, nil}
case 'B':
if alt {
return Event{AltDown, 0, nil}
}
if altShift {
return Event{AltSDown, 0, nil}
return Event{AltShiftDown, 0, nil}
}
return Event{SDown, 0, nil}
return Event{ShiftDown, 0, nil}
case 'C':
if alt {
return Event{AltRight, 0, nil}
}
if altShift {
return Event{AltSRight, 0, nil}
return Event{AltShiftRight, 0, nil}
}
return Event{SRight, 0, nil}
return Event{ShiftRight, 0, nil}
case 'D':
if alt {
return Event{AltLeft, 0, nil}
}
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[3]
@ -694,6 +729,10 @@ func (r *LightRenderer) NeedScrollbarRedraw() bool {
return false
}
func (r *LightRenderer) ShouldEmitResizeEvent() bool {
return false
}
func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush()
}
@ -721,6 +760,7 @@ func (r *LightRenderer) Close() {
r.flush()
r.closePlatform()
r.restoreTerminal()
r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
@ -808,6 +848,7 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
}
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
@ -815,21 +856,20 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
}
func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2
if !left || !right {
width++
}
vw := runeWidth(w.border.left)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
}
w.CPrint(color, repeat(' ', width))
if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@ -851,7 +891,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@ -982,7 +1025,7 @@ func (w *LightWindow) Print(text string) {
}
func cleanse(str string) string {
return strings.Replace(str, "\x1b", "", -1)
return strings.ReplaceAll(str, "\x1b", "")
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {

@ -3,10 +3,11 @@
package tui
import (
"fmt"
"errors"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"github.com/junegunn/fzf/src/util"
@ -14,6 +15,13 @@ import (
"golang.org/x/term"
)
var (
tty string
ttyin *os.File
ttyout *os.File
mutex sync.Mutex
)
func IsLightRendererSupported() bool {
return true
}
@ -48,19 +56,48 @@ func (r *LightRenderer) closePlatform() {
// NOOP
}
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil {
tty := ttyname()
if len(tty) == 0 {
tty = ttyname()
}
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil
}
}
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
return nil, errors.New("failed to open " + consoleDevice)
}
return in, nil
}
func openTtyIn() (*os.File, error) {
mutex.Lock()
defer mutex.Unlock()
if ttyin != nil {
return ttyin, nil
}
in, err := openTty(syscall.O_RDONLY)
if err == nil {
ttyin = in
}
return in
return in, err
}
func openTtyOut() (*os.File, error) {
mutex.Lock()
defer mutex.Unlock()
if ttyout != nil {
return ttyout, nil
}
out, err := openTty(syscall.O_WRONLY)
if err == nil {
ttyout = out
}
return out, err
}
func (r *LightRenderer) setupTerminal() {
@ -86,9 +123,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
var err error
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0)
bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 {
// Add anything we skipped over to the input buffer

@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@ -91,9 +91,14 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
}
func openTtyIn() *os.File {
func openTtyIn() (*os.File, error) {
// not used
return nil
return nil, nil
}
func openTtyOut() (*os.File, error) {
// not used
return nil, nil
}
func (r *LightRenderer) setupTerminal() error {

@ -7,10 +7,9 @@ import (
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/uniseg"
"github.com/rivo/uniseg"
)
func HasFullscreenRenderer() bool {
@ -143,15 +142,16 @@ func (a Attr) Merge(b Attr) Attr {
var (
_screen tcell.Screen
_prevMouseButton tcell.ButtonMask
_initialResize bool = true
)
func (r *FullscreenRenderer) initScreen() {
func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
if e != nil {
errorExit(e.Error())
return e
}
if e = s.Init(); e != nil {
errorExit(e.Error())
return e
}
if r.mouse {
s.EnableMouse()
@ -159,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse()
}
_screen = s
return nil
}
func (r *FullscreenRenderer) Init() {
func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "")
}
encoding.Register()
r.initScreen()
if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
func (r *FullscreenRenderer) Top() int {
@ -202,6 +207,10 @@ func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
return true
}
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool {
return true
}
func (r *FullscreenRenderer) Refresh() {
// noop
}
@ -216,6 +225,12 @@ func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
// Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
if _initialResize {
_initialResize = false
return Event{Invalid, 0, nil}
}
return Event{Resize, 0, nil}
// process mouse events:
@ -309,16 +324,16 @@ func (r *FullscreenRenderer) GetChar() Event {
switch ev.Rune() {
case 0:
if ctrl {
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
case ctrl:
return keyfn('h')
case alt:
return Event{AltBS, 0, nil}
return Event{AltBackspace, 0, nil}
case none, shift:
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
}
}
case tcell.KeyCtrlI:
@ -371,17 +386,17 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2:
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)
case tcell.KeyUp:
if altShift {
return Event{AltSUp, 0, nil}
return Event{AltShiftUp, 0, nil}
}
if shift {
return Event{SUp, 0, nil}
return Event{ShiftUp, 0, nil}
}
if alt {
return Event{AltUp, 0, nil}
@ -389,10 +404,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil}
case tcell.KeyDown:
if altShift {
return Event{AltSDown, 0, nil}
return Event{AltShiftDown, 0, nil}
}
if shift {
return Event{SDown, 0, nil}
return Event{ShiftDown, 0, nil}
}
if alt {
return Event{AltDown, 0, nil}
@ -400,10 +415,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil}
case tcell.KeyLeft:
if altShift {
return Event{AltSLeft, 0, nil}
return Event{AltShiftLeft, 0, nil}
}
if shift {
return Event{SLeft, 0, nil}
return Event{ShiftLeft, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
@ -411,10 +426,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil}
case tcell.KeyRight:
if altShift {
return Event{AltSRight, 0, nil}
return Event{AltShiftRight, 0, nil}
}
if shift {
return Event{SRight, 0, nil}
return Event{ShiftRight, 0, nil}
}
if alt {
return Event{AltRight, 0, nil}
@ -431,17 +446,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlDelete, 0, nil}
}
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:
return Event{End, 0, nil}
case tcell.KeyPgUp:
return Event{PgUp, 0, nil}
return Event{PageUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
return Event{PageDown, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
return Event{ShiftTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
@ -487,7 +502,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc
case tcell.KeyEsc:
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
}
@ -533,7 +548,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height,
normal: normal,
borderStyle: borderStyle}
w.drawBorder(false)
w.Erase()
return w
}
@ -550,8 +565,12 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
w.drawBorder(false)
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
}
func (w *TcellWindow) EraseMaybe() bool {

@ -102,22 +102,22 @@ func TestGetCharEventKey(t *testing.T) {
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
// 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.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 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.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, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 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, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 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, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 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, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "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{Backspace, 0, nil}}, // actual "Ctrl+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{Backspace, 0, nil}}, // actual "Ctrl+Alt+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{AltBackspace, 0, nil}}, // actual "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 | 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
@ -126,8 +126,8 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 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.KeyUpRight, 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
// 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}}, // 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.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
{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.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.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
// section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
@ -182,6 +182,7 @@ func TestGetCharEventKey(t *testing.T) {
r.Init()
// run and evaluate the tests
initialResizeAsInvalid := true
for _, test := range tests {
// generate key event
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
@ -191,8 +192,9 @@ func TestGetCharEventKey(t *testing.T) {
// process the event in fzf and evaluate the test
gotEvent := r.GetChar()
// skip Resize events, those are sometimes put in the buffer outside of this test
for gotEvent.Type == Resize {
t.Logf("Resize swallowed")
if initialResizeAsInvalid && gotEvent.Type == Invalid {
t.Logf("Resize as Invalid swallowed")
initialResizeAsInvalid = false
gotEvent = r.GetChar()
}
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
@ -257,7 +259,7 @@ Quick reference
37 LeftClick
38 RightClick
39 BTab
40 BSpace
40 Backspace
41 Del
42 PgUp
43 PgDn
@ -270,7 +272,7 @@ Quick reference
50 Insert
51 SUp
52 SDown
53 SLeft
53 ShiftLeft
54 SRight
55 F1
56 F2
@ -286,15 +288,15 @@ Quick reference
66 F12
67 Change
68 BackwardEOF
69 AltBS
69 AltBackspace
70 AltUp
71 AltDown
72 AltLeft
73 AltRight
74 AltSUp
75 AltSDown
76 AltSLeft
77 AltSRight
76 AltShiftLeft
77 AltShiftRight
78 Alt
79 CtrlAlt
..

@ -36,15 +36,8 @@ func ttyname() string {
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
func TtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
}
}
return os.Stdin
if in, err := openTtyIn(); err == nil {
return in
}
return in
return os.Stdin
}

@ -1,15 +1,16 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
"github.com/junegunn/uniseg"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
)
// Types of user action
//
//go:generate stringer -type=EventType
type EventType int
const (
@ -41,7 +42,7 @@ const (
CtrlX
CtrlY
CtrlZ
ESC
Esc
CtrlSpace
CtrlDelete
@ -51,27 +52,12 @@ const (
CtrlCaret
CtrlSlash
Invalid
Resize
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
ShiftTab
Backspace
BTab
BSpace
Del
PgUp
PgDn
Delete
PageUp
PageDown
Up
Down
@ -81,11 +67,11 @@ const (
End
Insert
SUp
SDown
SLeft
SRight
SDelete
ShiftUp
ShiftDown
ShiftLeft
ShiftRight
ShiftDelete
F1
F2
@ -100,29 +86,50 @@ const (
F11
F12
Change
BackwardEOF
Start
Load
Focus
One
Zero
Result
AltBS
AltBackspace
AltUp
AltDown
AltLeft
AltRight
AltSUp
AltSDown
AltSLeft
AltSRight
AltShiftUp
AltShiftDown
AltShiftLeft
AltShiftRight
Alt
CtrlAlt
Invalid
Fatal
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
// Events
Resize
Change
BackwardEOF
Start
Load
Focus
One
Zero
Result
Jump
JumpCancel
ClickHeader
)
func (t EventType) AsEvent() Event {
@ -142,6 +149,31 @@ func (e Event) Comparable() Event {
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 {
return Event{Rune, r, nil}
}
@ -271,6 +303,9 @@ type ColorTheme struct {
Disabled ColorAttr
Fg ColorAttr
Bg ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
PreviewFg ColorAttr
PreviewBg ColorAttr
DarkBg ColorAttr
@ -282,7 +317,7 @@ type ColorTheme struct {
Spinner ColorAttr
Info ColorAttr
Cursor ColorAttr
Selected ColorAttr
Marker ColorAttr
Header ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
@ -336,6 +371,14 @@ const (
BorderRight
)
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
}
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
@ -484,7 +527,7 @@ type TermSize struct {
}
type Renderer interface {
Init()
Init() error
Resize(maxHeightFunc func(int) int)
Pause(clear bool)
Resume(clear bool, sigcont bool)
@ -494,6 +537,7 @@ type Renderer interface {
Close()
PassThrough(string)
NeedScrollbarRedraw() bool
ShouldEmitResizeEvent() bool
GetChar() Event
@ -562,12 +606,14 @@ var (
ColMatch ColorPair
ColCursor ColorPair
ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair
ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair
ColInfo ColorPair
@ -589,6 +635,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
@ -597,7 +646,7 @@ func EmptyTheme() *ColorTheme {
Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
@ -619,6 +668,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
@ -627,7 +679,7 @@ func NoColorTheme() *ColorTheme {
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined},
Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
@ -643,17 +695,15 @@ func NoColorTheme() *ColorTheme {
}
}
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() {
Default16 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined},
@ -662,7 +712,7 @@ func init() {
Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined},
@ -681,6 +731,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined},
@ -689,7 +742,7 @@ func init() {
Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined},
@ -708,6 +761,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined},
@ -716,7 +772,7 @@ func init() {
Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined},
@ -758,12 +814,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
@ -789,17 +848,23 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter)
if theme.SelectedBg.Color != theme.Bg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
}
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)

@ -0,0 +1,28 @@
package util
import (
"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]()
}
atExitFuncs = nil
}

@ -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")
}
}

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

@ -6,8 +6,8 @@ import (
"strings"
"time"
"github.com/junegunn/uniseg"
"github.com/mattn/go-isatty"
"github.com/rivo/uniseg"
)
// StringWidth returns string width where each CR/LF character takes 1 column
@ -176,3 +176,15 @@ func RepeatToFill(str string, length int, limit int) string {
}
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)
}

@ -3,31 +3,71 @@
package util
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd {
type Executor struct {
shell string
args []string
escaper *strings.Replacer
}
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
args = args[1:]
} else {
if len(shell) == 0 {
shell = "sh"
}
args = []string{"-c"}
}
return ExecCommandWith(shell, command, setpgid)
var escaper *strings.Replacer
tokens := strings.Split(shell, "/")
if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
return &Executor{shell, args, escaper}
}
// ExecCommandWith executes the given command with the specified shell
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(shell, "-c", command)
// ExecCommand executes the given command with $SHELL
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(x.shell, append(x.args, command)...)
if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
return cmd
}
func (x *Executor) QuoteEntry(entry string) string {
return "'" + x.escaper.Replace(entry) + "'"
}
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
shellPath, err := exec.LookPath(x.shell)
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
os.Exit(127)
}
args := append([]string{shellPath}, append(x.args, command)...)
SetStdin(stdin)
syscall.Exec(shellPath, args, environ)
}
// KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)

@ -6,60 +6,162 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync/atomic"
"syscall"
)
var shellPath atomic.Value
type shellType int
const (
shellTypeUnknown shellType = iota
shellTypeCmd
shellTypePowerShell
)
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
type Executor struct {
shell string
shellType shellType
args []string
shellPath atomic.Value
}
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
} else if len(shell) == 0 {
shell = "cmd"
}
shellType := shellTypeUnknown
basename := filepath.Base(shell)
if len(args) > 0 {
args = args[1:]
} else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd
args = []string{"/s/c"}
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
shellType = shellTypePowerShell
args = []string{"-NoProfile", "-Command"}
} else {
args = []string{"-c"}
}
return &Executor{shell: shell, shellType: shellType, args: args}
}
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd {
var shell string
if cached := shellPath.Load(); cached != nil {
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := x.shell
if cached := x.shellPath.Load(); cached != nil {
shell = cached.(string)
} else {
shell = os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
shell = strings.Trim(string(out), "\n")
}
}
shellPath.Store(shell)
x.shellPath.Store(shell)
}
return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with the specified shell
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
var cmd *exec.Cmd
if strings.Contains(shell, "cmd") {
if x.shellType == shellTypeCmd {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: 0,
}
} else {
cmd = exec.Command(shell, append(x.args, command)...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CreationFlags: 0,
}
return cmd
}
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else {
cmd = exec.Command(shell, "-c", command)
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
cmd := x.ExecCommand(command, false)
cmd.Stdin = stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = environ
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
os.Exit(127)
}
err = cmd.Wait()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
}
os.Exit(0)
}
func escapeArg(s string) string {
b := make([]byte, 0, len(s)+2)
b = append(b, '"')
slashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
default:
slashes = 0
case '\\':
slashes++
case '"':
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '\\')
}
b = append(b, c)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CreationFlags: 0,
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '"')
return escapeRegex.ReplaceAllStringFunc(string(b), func(match string) string {
return "^" + match
})
}
func (x *Executor) QuoteEntry(entry string) string {
switch x.shellType {
case shellTypeCmd:
/* Manually tested with the following commands:
fzf --preview "echo {}"
fzf --preview "type {}"
echo .git\refs\| fzf --preview "dir {}"
echo .git\refs\\| fzf --preview "dir {}"
echo .git\refs\\\| fzf --preview "dir {}"
reg query HKCU | fzf --reverse --bind "enter:reload(reg query {})"
fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!"
fd -H --no-ignore -td -d 4 | fzf --preview "dir {}"
fd -H --no-ignore -td -d 4 | fzf --preview "eza {}" --preview-window up
fd -H --no-ignore -td -d 4 | fzf --preview "eza --color=always --tree --level=3 --icons=always {}"
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
*/
return escapeArg(entry)
case shellTypePowerShell:
escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default:
return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
}
return cmd
}
// KillCommand kills the process for the given command

@ -18,7 +18,6 @@ UNSETS = %w[
FZF_ALT_C_COMMAND
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
FZF_API_KEY
fish_history
].freeze
DEFAULT_TIMEOUT = 10
@ -67,7 +66,7 @@ class Shell
end
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
@ -426,6 +425,25 @@ class TestGoFZF < TestBase
end
end
def test_multi_action
tmux.send_keys "seq 10 | #{FZF} --bind 'a:change-multi,b:change-multi(3),c:change-multi(xxx),d:change-multi(0)'", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 ') }
tmux.send_keys 'a'
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (0)') }
tmux.send_keys 'b'
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (0/3)') }
tmux.send_keys :BTab
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (1/3)') }
tmux.send_keys 'c'
tmux.send_keys :BTab
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (2/3)') }
tmux.send_keys 'd'
tmux.until do |lines|
assert lines[-2]&.start_with?(' 10/10 ') && !lines[-2]&.include?('(')
end
end
def test_with_nth
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
@ -539,7 +557,7 @@ class TestGoFZF < TestBase
def test_expect
test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter
tmux.send_keys "seq 1 100 | #{fzf(:expect, key, :prompt, "[#{key}]")}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
@ -964,26 +982,40 @@ class TestGoFZF < TestBase
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}"]
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
writelines(tempname, %w[foo'bar foo"bar foo$bar])
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
tmux.until { |lines| assert_equal ' 3/3', lines[-2] }
tmux.send_keys :Escape, :a
tmux.until { |lines| assert_equal 3, lines.item_count }
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
tmux.send_keys :Escape, :a
tmux.send_keys :Up
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Up
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Up
tmux.send_keys :C
ready.call('C')
tmux.send_keys 'barfoo'
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
wait do
assert_path_exists output
assert_equal %w[
/foo'bar/ /foo'bar/
/foo"barfoo"bar/ /foo"barfoo"bar/
/foo'bar/ /foo'barfoo'bar/
/foo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/
], File.readlines(output, chomp: true)
end
@ -993,17 +1025,28 @@ class TestGoFZF < TestBase
def test_execute_multi
output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})"]
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
writelines(tempname, %w[foo'bar foo"bar foo$bar foobar])
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Tab, :Tab
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
wait do
assert_path_exists output
assert_equal [
@ -1221,7 +1264,7 @@ class TestGoFZF < TestBase
end
def test_toggle_header
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border", :Enter
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border rounded", :Enter
before = <<~OUTPUT
@ -1444,6 +1487,19 @@ class TestGoFZF < TestBase
assert_equal '3', readonce.chomp
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
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed
@ -1695,7 +1751,7 @@ class TestGoFZF < TestBase
end
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] }
end
@ -1893,7 +1949,7 @@ class TestGoFZF < TestBase
end
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.send_keys 'a'
tmux.until do |lines|
@ -1918,6 +1974,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.item_count }
end
def test_reload_should_terminate_standard_input_stream
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
end
def test_clear_list_when_header_lines_changed_due_to_reload
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| assert_includes lines, ' 9' }
@ -2160,14 +2221,15 @@ class TestGoFZF < TestBase
file = Tempfile.new('fzf-follow')
file.sync = true
tmux.send_keys %(seq 100 | #{FZF} --preview 'tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~3'), :Enter
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
tmux.until { |lines| lines.item_count == 100 }
# Write to the temporary file, and check if the preview window is showing
# the last line of the file
tmux.until { |lines| assert_includes lines[1], 'start' }
3.times { file.puts _1 } # header lines
1000.times { file.puts _1 }
tmux.until { |lines| assert_includes lines[1], '/1003' }
tmux.until { |lines| assert_includes lines[1], '/1004' }
tmux.until { |lines| assert_includes lines[-2], '999' }
# Scroll the preview window and fzf should stop following the file content
@ -2175,7 +2237,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[-2], '998' }
file.puts 'foo', 'bar'
tmux.until do |lines|
assert_includes lines[1], '/1005'
assert_includes lines[1], '/1006'
assert_includes lines[-2], '998'
end
@ -2188,7 +2250,7 @@ class TestGoFZF < TestBase
end
file.puts 'baz'
tmux.until do |lines|
assert_includes lines[1], '/1006'
assert_includes lines[1], '/1007'
assert_includes lines[-2], 'baz'
end
@ -2197,7 +2259,7 @@ class TestGoFZF < TestBase
wait { assert_includes lines[-2], 'bar' }
file.puts 'aaa'
tmux.until do |lines|
assert_includes lines[1], '/1007'
assert_includes lines[1], '/1008'
assert_includes lines[-2], 'bar'
end
@ -2206,7 +2268,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
file.puts 'bbb'
tmux.until do |lines|
assert_includes lines[1], '/1008'
assert_includes lines[1], '/1009'
assert_includes lines[-2], 'bbb'
end
@ -2214,7 +2276,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Space
file.puts 'ccc', 'ddd'
tmux.until do |lines|
assert_includes lines[1], '/1010'
assert_includes lines[1], '/1011'
assert_includes lines[-2], 'bbb'
end
rescue StandardError
@ -2438,6 +2500,84 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.match_count }
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
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
@ -2553,9 +2693,17 @@ class TestGoFZF < TestBase
end
end
def test_change_preview_window_should_not_reset_change_preview
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
tmux.until { |lines| assert_includes lines, 'hello' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '│ hello' }
end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
3.times do
tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a'
@ -2616,7 +2764,7 @@ class TestGoFZF < TestBase
end
def test_height_range_fit
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border', :Enter
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
expected = <<~OUTPUT
3
@ -2629,7 +2777,7 @@ class TestGoFZF < TestBase
end
def test_height_range_fit_preview_above
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border --preview "seq {}" --preview-window up,60%', :Enter
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded --preview-window border-rounded --preview "seq {}" --preview-window up,60%', :Enter
expected = <<~OUTPUT
@ -2685,7 +2833,7 @@ class TestGoFZF < TestBase
end
def test_height_range_overflow
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border', :Enter
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
expected = <<~OUTPUT
2
@ -2750,7 +2898,7 @@ class TestGoFZF < TestBase
end
def test_labels_left
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos 2 --preview : --preview-label barfoo --preview-label-pos 2', :Enter
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos 2 --preview : --preview-label barfoo --preview-label-pos 2', :Enter
tmux.until do
assert_includes(_1[0], '╭foobar─')
assert_includes(_1[1], '╭barfoo─')
@ -2758,7 +2906,7 @@ class TestGoFZF < TestBase
end
def test_labels_right
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos -2 --preview : --preview-label barfoo --preview-label-pos -2', :Enter
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos -2 --preview : --preview-label barfoo --preview-label-pos -2', :Enter
tmux.until do
assert_includes(_1[0], '─foobar╮')
assert_includes(_1[1], '─barfoo╮')
@ -2766,13 +2914,34 @@ class TestGoFZF < TestBase
end
def test_labels_bottom
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos 2:bottom --preview : --preview-label barfoo --preview-label-pos -2:bottom', :Enter
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos 2:bottom --preview : --preview-label barfoo --preview-label-pos -2:bottom', :Enter
tmux.until do
assert_includes(_1[-1], '╰foobar─')
assert_includes(_1[-2], '─barfoo╯')
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
tmux.send_keys 'seq 100 | fzf -q55', :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 ─') }
@ -2808,6 +2977,13 @@ class TestGoFZF < TestBase
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
end
def test_info_inline_right_clearance
tmux.send_keys "seq 100000 | #{FZF} --info inline-right", :Enter
tmux.until { assert_match(%r{100000/100000}, _1[-1]) }
tmux.send_keys 'x'
tmux.until { assert_match(%r{ 0/100000}, _1[-1]) }
end
def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
@ -2895,7 +3071,7 @@ class TestGoFZF < TestBase
end
def test_no_extra_newline_issue_3209
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap,border-rounded --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
expected = <<~OUTPUT
@ -2976,7 +3152,7 @@ class TestGoFZF < TestBase
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
assert_includes lines[-2], '+t'
end
tmux.send_keys :BSpace
tmux.until do |lines|
@ -2988,7 +3164,7 @@ class TestGoFZF < TestBase
tmux.send_keys '4'
tmux.until do |lines|
assert_equal 28, lines.match_count
refute_includes lines[-2], '+T'
refute_includes lines[-2], '+t'
end
tmux.send_keys :BSpace
tmux.until do |lines|
@ -2997,11 +3173,11 @@ class TestGoFZF < TestBase
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
assert_includes lines[-2], '+t'
end
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[-2], '+T'
refute_includes lines[-2], '+t'
end
end
@ -3056,6 +3232,11 @@ class TestGoFZF < TestBase
end
def test_delete_with_modifiers
if ENV['GITHUB_ACTION']
# Expected: "[3]"
# Actual: "[]3;5~"
skip('CTRL-DELETE is not properly handled in GitHub Actions environment')
end
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'C-Delete'
@ -3083,6 +3264,17 @@ class TestGoFZF < TestBase
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' }
end
def test_fzf_pos
tmux.send_keys "seq 100 | #{FZF} --preview 'echo $FZF_POS / $FZF_MATCH_COUNT'", :Enter
tmux.until { |lines| assert(lines.any? { |line| line.include?('1 / 100') }) }
tmux.send_keys :Up
tmux.until { |lines| assert(lines.any? { |line| line.include?('2 / 100') }) }
tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('1 / 1') }) }
tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
end
end
module TestShell
@ -3276,7 +3468,10 @@ module CompletionTest
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
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.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
@ -3311,7 +3506,11 @@ module CompletionTest
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.send_keys :xx

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

Loading…
Cancel
Save