Compare commits

...

120 Commits

Author SHA1 Message Date
junegunn 3ee1fc2034 Deploying to master from @ junegunn/fzf@124cd70710 🚀 5 days ago
Konstantin-Glukhov 124cd70710
[vim] Do not prepend CWD to path starting with a backslash on Windows (#3820)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
1 week ago
junegunn 99bd6de541 Deploying to master from @ junegunn/fzf@daa602422d 🚀 2 weeks ago
junegunn daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 3 weeks ago
Zhizhen He 01e7668915
chore: use strings.ReplaceAll (#3801) 3 weeks 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.
3 weeks ago
LangLangBart 030428ba43
docs: update zsh integration instructions (#3794) 3 weeks ago
Junegunn Choi 8a110e02b9
Fix tcell test case 3 weeks ago
Junegunn Choi 86d92c17c4
Refactor tui.TtyIn() 3 weeks ago
Junegunn Choi c4cc7891b4
Revert "Close handles to /dev/tty", instead reuse handles 3 weeks ago
Junegunn Choi 218843b9f1
Close handles to /dev/tty 3 weeks 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.
3 weeks ago
Junegunn Choi 6432f00f0d
0.52.1 3 weeks ago
junegunn 4e9e842aa4 Deploying to master from @ junegunn/fzf@07880ca441 🚀 4 weeks ago
LangLangBart 07880ca441
chore: Update flags to include long-form options for case (#3785) 4 weeks ago
Junegunn Choi bcda25a513
0.52.0 1 month ago
Junegunn Choi 8256fcde15
Minor fixup 1 month ago
Junegunn Choi af65aa298a
Add color names: selected-{fg,bg,hl} 1 month ago
Junegunn Choi 6834d17844
[vim] Revert 7411da8d5a
Fix #3777
1 month ago
Junegunn Choi ed511d7867
[install] tar --no-same-owner
Close #3776
1 month ago
Junegunn Choi cd8d736a9f
[shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
1 month ago
Junegunn Choi 0952b2dfd4
Rename --cursor-line to --highlight-line 1 month ago
Junegunn Choi 4bedd33c59
Refactor the code to remove global variables 1 month ago
Junegunn Choi c5fb0c43f9
Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
1 month ago
Junegunn Choi 9e4780510e
Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 1 month ago
Junegunn Choi e8405f40fe
Refactor the code so that fzf can be used as a library (#3769) 1 month 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>
1 month 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>
1 month ago
Junegunn Choi 501577ab28
Fix flaky test case 1 month 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 "&|<>()@^%!"
1 month ago
Junegunn Choi 24ff66d4a9
Fix `change-preview` reset by `change-preview-window`
Fix #3770
1 month 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
1 month 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>
1 month 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
1 month 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
1 month 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 {}'
1 month ago
Junegunn Choi 9dee8edc0c
Clear characters on 1-column margin after the preview window on the left 1 month 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
1 month ago
cyqsimon dba1644518
Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
1 month ago
Junegunn Choi 260a65b0fb
0.51.0 1 month ago
Junegunn Choi 835d2fb98c
[vim] Fix argument escaping for Windows batch file
Fix #3620
1 month ago
Charlie Vieth a9811addaa
Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
1 month 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>
1 month ago
Junegunn Choi 194a763c46
Escaping for cmd.exe: always use double quotes 1 month ago
Junegunn Choi 8d74446bef
Fix escaping for cmd.exe
Close #3651
Close #2609
1 month ago
Junegunn Choi 7ed6c7905c
Determine shell type once by the basename 1 month ago
Junegunn Choi 159a37fa37
Restore CmdLine parameter when running commands using cmd.exe 1 month ago
junegunn f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 1 month ago
Junegunn Choi 4a68eac99b
Suggest using toggle+up instead of toggle-up 1 month ago
Junegunn Choi 2665580120
Add $FZF_POS environment variable
Close #2175
Close #3753
1 month ago
Junegunn Choi a4391aeedd
Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
1 month 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>
1 month ago
Junegunn Choi 608232568b
Add 'change-multi' action
Close #3754
1 month ago
Junegunn Choi 7f85beccb5
[completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
1 month 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
1 month ago
Junegunn Choi fddbfe7b0e
Fix 'reload' not terminating closed standard input stream
Fix #3750
1 month ago
Junegunn Choi 4ab7fdc28e
Merge identical case clauses 1 month ago
Junegunn Choi e352b68878
Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
1 month ago
Junegunn Choi 207deeadba
Add -trimpath to build command 1 month ago
Cheng d18d92f925
Replace fmt.Errorf with no parameters with errors.New (#3747) 2 months ago
junegunn af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 2 months 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
2 months ago
Junegunn Choi f864f8b5f7
Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
2 months ago
Junegunn Choi 31d72efba7
Describe how to build fzf from the latest source using brew 2 months ago
LangLangBart d169c951f3
fix: Move 'emulate' command outside interactive check (#3736) 2 months ago
Junegunn Choi 90d7e38909
[fzf-tmux] Replace `command -v` with `which`
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

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

Related #3412
2 months 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'
2 months 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>
2 months 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
2 months 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.
2 months ago
LangLangBart 69b9d674a3
chore: Add new option in issue checklist and modify requirements (#3715) 2 months ago
junegunn bad8061547 Deploying to master from @ junegunn/fzf@62963dcefd 🚀 2 months 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
3 months ago
Junegunn Choi 05453881c3
Set a 2-second timeout for POST requests
Close #3685
3 months ago
Junegunn Choi 5e47ab9431
README: Mention that you can source individual script files 3 months ago
LangLangBart ec70acd0b9
chore: transition from markdown to YAML for issue template (#3687) 3 months ago
zeertzjq 25e61056b6
[fish] Fix Ctrl-T and Alt-C not using last token as search root (#3684) 3 months ago
Junegunn Choi d579e335b5
0.48.1 3 months ago
Junegunn Choi 7e344ceb85
Update README 3 months ago
Junegunn Choi 0145b82ea0
Update README 3 months ago
Junegunn Choi b4efe7aab7
Show how to disable a key binding 3 months ago
Junegunn Choi 9ffe951f6d
Update Makefile target dependencies
Because shell integration scripts are now embedded in the binary
3 months ago
Brayden Hill a5ea4f57bd
Updated link for highlight command (#3680) 3 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>
3 months ago
Junegunn Choi c7ee071efa
Fix panic caused by invalid cursor index
Fix #3681
3 months ago
Junegunn Choi 0740ef7ceb
[bash] Fix default completion of unset, unalias, etc
Fix #3679
3 months ago
junegunn b29bd809ac Deploying to master from @ junegunn/fzf@8977c9257a 🚀 3 months ago
Junegunn Choi 8977c9257a
Limit the maximum number of focus events to process at once 3 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

@ -25,7 +25,7 @@ jobs:
- 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

@ -22,7 +22,7 @@ jobs:
- 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

@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@v1.19.0
- 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:

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

@ -1,6 +1,151 @@
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.

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

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=()

@ -1,13 +1,13 @@
module github.com/junegunn/fzf
require (
github.com/charlievieth/fastwalk v1.0.2
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/rivo/uniseg v0.4.7
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.0
)
require (
@ -17,4 +17,4 @@ require (
golang.org/x/text v0.14.0 // indirect
)
go 1.17
go 1.20

@ -1,5 +1,5 @@
github.com/charlievieth/fastwalk v1.0.2 h1:KYWo7xszmoldOGrwdNIeznSzhj9mhgk+6DwHunG99bc=
github.com/charlievieth/fastwalk v1.0.2/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
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.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.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.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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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=

@ -2,7 +2,7 @@
set -u
version=0.48.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"
@ -265,7 +265,11 @@ fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
echo "eval \"\$(fzf --$shell)\"" >> "$src"
if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else
cat >> "$src" << EOF
# Auto-completion

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

@ -3,14 +3,15 @@ package main
import (
_ "embed"
"fmt"
"os"
"strings"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.48"
var revision string = "devel"
var version = "0.52"
var revision = "devel"
//go:embed shell/key-bindings.bash
var bashKeyBindings []byte
@ -33,9 +34,21 @@ func printScript(label string, content []byte) {
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()
options := fzf.ParseOptions()
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)
@ -51,5 +64,19 @@ func main() {
fmt.Println("fzf_key_bindings")
return
}
fzf.Run(options, version, revision)
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 "Mar 2024" "fzf 0.48.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 "Mar 2024" "fzf 0.48.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
@ -46,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"
@ -191,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[%]"
@ -372,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
@ -463,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)
@ -533,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
@ -811,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.
@ -894,7 +911,7 @@ e.g. \fBeval "$(fzf --bash)"\fR
.B "--zsh"
Print script to set up Zsh shell integration
e.g. \fBeval "$(fzf --zsh)"\fR
e.g. \fBsource <(fzf --zsh)\fR
.TP
.B "--fish"
@ -931,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
@ -971,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
@ -1047,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
@ -1075,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
@ -1099,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
@ -1155,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
@ -1164,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.
@ -1171,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.
@ -1204,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
@ -1215,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
@ -1236,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.
@ -1254,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 '|')
@ -1278,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)
@ -1326,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)
@ -1338,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
@ -1410,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, ...)
@ -320,7 +327,7 @@ function! s:common_sink(action, lines) abort
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
for item in a:lines
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
let sep = s:is_win ? '\' : '/'
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
endif
@ -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,12 +4,14 @@
# / __/ / /_/ __/
# /_/ /___/_/ 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}
@ -32,6 +34,14 @@
# 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 "$@"
@ -92,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
@ -291,7 +250,7 @@ _fzf_handle_dynamic_completion() {
"$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
@ -308,6 +267,7 @@ _fzf_handle_dynamic_completion() {
eval "$orig_complete"
fi
fi
[[ $ret -eq 0 ]] && return 124
return $ret
fi
}
@ -334,13 +294,19 @@ __fzf_generic_path_completion() {
[[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2"
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
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir"
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
@ -398,7 +364,10 @@ _fzf_complete() {
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")
@ -502,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
@ -511,13 +483,22 @@ 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() {
@ -539,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
@ -567,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,6 +75,9 @@ 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}
#
@ -93,6 +96,14 @@ fi
###########################################################
__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 "$@"
@ -146,13 +157,19 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}"
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
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty
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} "
@ -217,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
@ -315,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
@ -345,6 +365,7 @@ fzf-completion() {
zle -N fzf-completion
bindkey '^I' fzf-completion
fi
} always {
# Restore the original options.

@ -11,15 +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 opts
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} 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
@ -37,23 +46,24 @@ fzf-file-widget() {
}
__fzf_cd__() {
local opts dir
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
local dir
dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)
) && printf 'builtin cd -- %q' "$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
@ -64,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 }
@ -82,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
@ -102,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"'
@ -112,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__'
@ -123,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,17 +18,27 @@ 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]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
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" ]
@ -49,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.)
@ -70,13 +81,14 @@ 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_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
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
@ -104,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,12 +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() {
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
local item
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do
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=$?
@ -57,43 +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() {
setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)"
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")
@ -110,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,7 +260,7 @@ func charClassOfNonAscii(char rune) charClass {
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return charClassOfAscii(char)
return asciiCharClasses[char]
}
return charClassOfNonAscii(char)
}
@ -287,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 {
@ -340,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) {
@ -412,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.
@ -420,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
@ -436,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)
@ -459,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
}
@ -488,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
}
@ -602,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
}
@ -615,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
@ -645,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 {
@ -683,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
}
@ -777,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

@ -14,6 +14,7 @@ const (
// Reader
readerBufferSize = 64 * 1024
readerSlabSize = 128 * 1024
readerPollIntervalMin = 10 * time.Millisecond
readerPollIntervalStep = 5 * time.Millisecond
readerPollIntervalMax = 50 * time.Millisecond
@ -66,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(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, 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(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, 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,7 @@ 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
@ -233,7 +248,7 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string, environ []string) {
restart := func(command commandSpec, environ []string) {
reading = true
chunkList.Clear()
itemIndex = 0
@ -241,15 +256,15 @@ func Run(opts *Options, version string, revision string) {
header = make([]string, 0, opts.HeaderLines)
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
@ -264,7 +279,11 @@ 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, nextEnviron)
@ -278,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)
@ -293,7 +314,7 @@ 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) {
@ -319,10 +340,13 @@ func Run(opts *Options, version string, revision string) {
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
}
@ -359,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,
@ -380,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
@ -282,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,7 +1,7 @@
package fzf
import (
"bufio"
"bytes"
"context"
"io"
"os"
@ -18,6 +18,7 @@ import (
// Reader reads from command or standard input
type Reader struct {
pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox
delimNil bool
event int32
@ -30,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() {
@ -85,18 +86,34 @@ func (r *Reader) terminate() {
r.mutex.Unlock()
}
func (r *Reader) restart(command string, environ []string) {
func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(command, environ)
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(root string, opts walkerOpts, ignores []string) {
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller()
var success bool
if util.IsTty() {
if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores)
@ -111,32 +128,90 @@ func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
}
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))
}
}
@ -184,7 +259,7 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
r.exec = util.ExecCommand(command, true)
r.exec = r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
}

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

@ -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
)
@ -72,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil
}
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
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 port, fmt.Errorf("FZF_API_KEY is required to allow remote access")
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 port, fmt.Errorf("failed to listen on %s", addrStr)
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 port, fmt.Errorf("cannot extract port: %s", addr)
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 port, err
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 port, nil
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
}

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,7 +29,7 @@ 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) {}

@ -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,6 +11,7 @@ import (
"time"
"unicode/utf8"
"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]
@ -725,6 +760,7 @@ func (r *LightRenderer) Close() {
r.flush()
r.closePlatform()
r.restoreTerminal()
r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
@ -808,44 +844,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
}
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
}
}
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))
}
}
@ -867,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))
}
}
@ -998,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,7 +7,6 @@ import (
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
@ -146,13 +145,13 @@ var (
_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()
@ -160,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 {
@ -320,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:
@ -382,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}
@ -400,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}
@ -411,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}
@ -422,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}
@ -442,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:
@ -498,7 +502,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc
case tcell.KeyEsc:
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
}
@ -561,7 +565,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
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)
}

@ -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
@ -259,7 +259,7 @@ Quick reference
37 LeftClick
38 RightClick
39 BTab
40 BSpace
40 Backspace
41 Del
42 PgUp
43 PgDn
@ -272,7 +272,7 @@ Quick reference
50 Insert
51 SUp
52 SDown
53 SLeft
53 ShiftLeft
54 SRight
55 F1
56 F2
@ -288,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/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)
@ -563,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
@ -590,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},
@ -598,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},
@ -620,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},
@ -628,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},
@ -644,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},
@ -663,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},
@ -682,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},
@ -690,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},
@ -709,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},
@ -717,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},
@ -759,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)
@ -790,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)
}
}

@ -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] }
@ -1469,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
@ -1720,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
@ -1943,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' }
@ -2464,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 }
@ -2579,6 +2693,13 @@ 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
@ -2800,6 +2921,27 @@ class TestGoFZF < TestBase
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 ─') }
@ -2835,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 }
@ -3003,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|
@ -3015,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|
@ -3024,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
@ -3115,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
@ -3308,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] }
@ -3343,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

Loading…
Cancel
Save