Compare commits

...

803 Commits

Author SHA1 Message Date
junegunn daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 2024-05-19 00:01:47 +00:00
Zhizhen He 01e7668915
chore: use strings.ReplaceAll (#3801) 2024-05-18 17:06:33 +09:00
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.
2024-05-15 17:26:49 +09:00
LangLangBart 030428ba43
docs: update zsh integration instructions (#3794) 2024-05-15 01:59:43 +09:00
Junegunn Choi 8a110e02b9
Fix tcell test case 2024-05-15 00:45:23 +09:00
Junegunn Choi 86d92c17c4
Refactor tui.TtyIn() 2024-05-15 00:28:56 +09:00
Junegunn Choi c4cc7891b4
Revert "Close handles to /dev/tty", instead reuse handles 2024-05-15 00:15:29 +09:00
Junegunn Choi 218843b9f1
Close handles to /dev/tty 2024-05-14 21:49:47 +09:00
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.
2024-05-14 16:32:26 +09:00
Junegunn Choi 6432f00f0d
0.52.1 2024-05-14 01:54:30 +09:00
junegunn 4e9e842aa4 Deploying to master from @ junegunn/fzf@07880ca441 🚀 2024-05-12 00:01:52 +00:00
LangLangBart 07880ca441
chore: Update flags to include long-form options for case (#3785) 2024-05-09 20:39:21 +09:00
Junegunn Choi bcda25a513
0.52.0 2024-05-08 00:15:30 +09:00
Junegunn Choi 8256fcde15
Minor fixup 2024-05-07 23:49:30 +09:00
Junegunn Choi af65aa298a
Add color names: selected-{fg,bg,hl} 2024-05-07 23:38:06 +09:00
Junegunn Choi 6834d17844
[vim] Revert 7411da8d5a
Fix #3777
2024-05-07 20:16:18 +09:00
Junegunn Choi ed511d7867
[install] tar --no-same-owner
Close #3776
2024-05-07 20:00:13 +09:00
Junegunn Choi cd8d736a9f
[shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2024-05-07 19:31:56 +09:00
Junegunn Choi 0952b2dfd4
Rename --cursor-line to --highlight-line 2024-05-07 19:22:39 +09:00
Junegunn Choi 4bedd33c59
Refactor the code to remove global variables 2024-05-07 16:58:17 +09:00
Junegunn Choi c5fb0c43f9
Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2024-05-07 01:34:35 +09:00
Junegunn Choi 9e4780510e
Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2024-05-07 01:26:25 +09:00
Junegunn Choi e8405f40fe
Refactor the code so that fzf can be used as a library (#3769) 2024-05-07 01:06:42 +09:00
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>
2024-05-06 22:51:10 +09:00
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>
2024-05-06 22:50:50 +09:00
Junegunn Choi 501577ab28
Fix flaky test case 2024-05-06 13:59:16 +09:00
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 "&|<>()@^%!"
2024-05-06 13:46:06 +09:00
Junegunn Choi 24ff66d4a9
Fix `change-preview` reset by `change-preview-window`
Fix #3770
2024-05-06 09:40:02 +09:00
Junegunn Choi bf184449bc
Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2024-05-06 09:27:58 +09:00
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>
2024-05-05 18:56:43 +09:00
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
2024-05-05 17:09:00 +09:00
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
2024-05-05 16:49:30 +09:00
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 {}'
2024-05-05 11:10:54 +09:00
Junegunn Choi 9dee8edc0c
Clear characters on 1-column margin after the preview window on the left 2024-05-05 11:06:50 +09:00
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
2024-05-03 12:18:34 +09:00
cyqsimon dba1644518
Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-02 20:52:27 +09:00
Junegunn Choi 260a65b0fb
0.51.0 2024-05-01 14:14:06 +09:00
Junegunn Choi 835d2fb98c
[vim] Fix argument escaping for Windows batch file
Fix #3620
2024-05-01 10:02:26 +09:00
Charlie Vieth a9811addaa
Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
2024-05-01 09:15:47 +09:00
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>
2024-04-29 23:10:50 +09:00
Junegunn Choi 194a763c46
Escaping for cmd.exe: always use double quotes 2024-04-28 23:30:44 +09:00
Junegunn Choi 8d74446bef
Fix escaping for cmd.exe
Close #3651
Close #2609
2024-04-28 22:03:00 +09:00
Junegunn Choi 7ed6c7905c
Determine shell type once by the basename 2024-04-28 20:11:05 +09:00
Junegunn Choi 159a37fa37
Restore CmdLine parameter when running commands using cmd.exe 2024-04-28 16:01:19 +09:00
junegunn f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 2024-04-28 00:01:30 +00:00
Junegunn Choi 4a68eac99b
Suggest using toggle+up instead of toggle-up 2024-04-27 19:04:30 +09:00
Junegunn Choi 2665580120
Add $FZF_POS environment variable
Close #2175
Close #3753
2024-04-27 18:57:22 +09:00
Junegunn Choi a4391aeedd
Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
2024-04-27 18:36:37 +09:00
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>
2024-04-27 17:57:11 +09:00
Junegunn Choi 608232568b
Add 'change-multi' action
Close #3754
2024-04-27 17:38:06 +09:00
Junegunn Choi 7f85beccb5
[completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
2024-04-27 14:14:30 +09:00
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
2024-04-25 16:54:51 +09:00
Junegunn Choi fddbfe7b0e
Fix 'reload' not terminating closed standard input stream
Fix #3750
2024-04-25 16:49:06 +09:00
Junegunn Choi 4ab7fdc28e
Merge identical case clauses 2024-04-25 16:34:05 +09:00
Junegunn Choi e352b68878
Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
2024-04-24 18:20:30 +09:00
Junegunn Choi 207deeadba
Add -trimpath to build command 2024-04-23 17:24:50 +09:00
Cheng d18d92f925
Replace fmt.Errorf with no parameters with errors.New (#3747) 2024-04-22 09:35:33 +09:00
junegunn af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 2024-04-21 00:01:49 +00:00
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
2024-04-20 14:42:43 +09:00
Junegunn Choi f864f8b5f7
Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
2024-04-19 22:40:38 +09:00
Junegunn Choi 31d72efba7
Describe how to build fzf from the latest source using brew 2024-04-18 23:37:12 +09:00
LangLangBart d169c951f3
fix: Move 'emulate' command outside interactive check (#3736) 2024-04-17 18:03:12 +09:00
Junegunn Choi 90d7e38909
[fzf-tmux] Replace `command -v` with `which`
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

Fix #3730
2024-04-17 17:29:45 +09:00
hidewrong 938f23e429
Fix typo in comment (#3734)
Signed-off-by: hidewrong <hidewrong@outlook.com>
2024-04-17 17:13:35 +09:00
Junegunn Choi f97d275413
0.50.0 2024-04-15 00:02:27 +09:00
Junegunn Choi 3acb4ca90e
Fix streaming filter mode by not running reader callback concurrently
Close #3728
2024-04-14 23:34:25 +09:00
Junegunn Choi e86b81bbf5
Improve search performance by limiting the search scope
Find the last occurrence of the last character in the pattern and
perform the search algorithm only up to that point.

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

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

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

    # Default behavior
    fzf --bind space:jump

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 08:50:49 +09:00
Konstantin Podsvirov 90b8187882
Add info about MSYS2 distro to README.md (#3610) 2024-02-04 19:02:32 +09:00
junegunn 1a43259989 Deploying to master from @ junegunn/fzf@3c0a630475 🚀 2024-02-04 00:01:39 +00:00
Junegunn Choi 3c0a630475
0.46.1 2024-02-01 18:13:00 +09:00
Junegunn Choi 2a1e5a9729
More test fixes for tcell on GitHub Actions 2024-02-01 17:39:18 +09:00
Junegunn Choi 413c66beba
Fix tests for tcell build 2024-02-01 16:25:53 +09:00
Junegunn Choi 1416e696b1
Avoid full redraw on 'preview' action when preview window exists 2024-02-01 15:50:48 +09:00
Junegunn Choi d373cf89c7
Retain preview window on resize after 'preview' action 2024-02-01 15:46:42 +09:00
dependabot[bot] dd886d22f0
Bump github.com/rivo/uniseg from 0.4.5 to 0.4.6 (#3605)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.4.5 to 0.4.6.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.4.5...v0.4.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 11:23:08 +09:00
junegunn 472569a27c Deploying to master from @ junegunn/fzf@76cf6559cc 🚀 2024-01-28 00:01:29 +00:00
Junegunn Choi 76cf6559cc
junegunn/uniseg -> rivo/uniseg
https://github.com/rivo/uniseg/pull/47
2024-01-27 22:18:43 +09:00
Junegunn Choi a34e8dcdc9
Downgrade Go version to keep support for old Windows (#3601)
Go 1.21 dropped support for older versions of Windows.

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

But there is no absolute reason for fzf to use Go 1.21, so we downgrade
the dependency.
2024-01-26 13:07:35 +09:00
Junegunn Choi da752fc9a4
Fix Windows build
Fix #3598
2024-01-24 15:59:54 +09:00
Junegunn Choi beb2de2dd9
0.46.0 2024-01-23 23:47:24 +09:00
Junegunn Choi 2a8b65e105
Fix highlighting of regions that are matched multiple times
Fix #3596
2024-01-23 12:19:32 +09:00
dependabot[bot] 62a916bc24
Bump crate-ci/typos from 1.16.4 to 1.17.2 (#3595)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.16.4 to 1.17.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.16.4...v1.17.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 10:21:01 +09:00
dependabot[bot] c47b833e7b
Bump actions/dependency-review-action from 3 to 4 (#3594)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3 to 4.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 10:20:39 +09:00
LangLangBart 09b0958b5f
docs(ADVANCED.md): replace placeholder with env variable for toggle single key binding (#3593) 2024-01-22 13:17:03 +09:00
Junegunn Choi 3a4c3d3e58
Add link to fzf Theme Playground by @vitormv
https://vitormv.github.io/fzf-themes/
2024-01-21 23:12:57 +09:00
Junegunn Choi 7484292e63
Avoid deadlocks by adding a 2 second timeout to GET / endpoint
Because fzf processes HTTP GET requests in the main event loop,
accessing the endpoint from within execute/transform actions would
result in a deadlock and hang fzf indefinitely. This commit sets
a 2 second timeout to avoid the deadlock.
2024-01-21 23:04:37 +09:00
Junegunn Choi 687c2741b8
Add 'resize' event
Close #3570
2024-01-21 15:30:59 +09:00
junegunn 2fb285e530 Deploying to master from @ junegunn/fzf@16f6473938 🚀 2024-01-21 00:01:45 +00:00
Junegunn Choi 16f6473938
Change mattn/go-runewidth dependency to rivo/uniseg for accurate results
Related #3588 #3588 #3567
2024-01-21 02:54:41 +09:00
Junegunn Choi 66546208b2
Update goreleaser flags 2024-01-20 14:36:52 +09:00
Junegunn Choi 532274045e
Update to the latest go 2024-01-20 14:35:00 +09:00
LangLangBart 9347c72fb6
docs(ADVANCED.md): Add fzf example switching ripgrep/fzf with single hotkey (#3590) 2024-01-20 13:47:54 +09:00
Junegunn Choi e90bb7169c
[zsh] Handle '*' suffix in history line numbers
Fix #3591
2024-01-20 13:43:15 +09:00
Junegunn Choi 8a2c41e183
Handle ambiguous emoji width
Fix #3588
2024-01-19 16:41:50 +09:00
Junegunn Choi 59fb65293a
README.md: More information on image support 2024-01-17 13:21:00 +09:00
Junegunn Choi e7718b92b7
Kitty image support improvements
* Use `--unicode-placeholder` for consistent result in and out of tmux
* Use updated version of junegunn/go-runewidth that handles diacritics
  used in Kitty Unicode placeholder

Close #3567
2024-01-17 00:17:22 +09:00
Junegunn Choi cdfaf761df
Expose state information via environment variables to child processes
Close #3582
2024-01-16 14:18:31 +09:00
Junegunn Choi 1a9ea6f738
Remove 'replace' directive for 'go install' compatibility
Close #3577
2024-01-14 17:12:24 +09:00
junegunn 945c1c8597 Deploying to master from @ junegunn/fzf@e4d0f7acd5 🚀 2024-01-14 00:01:41 +00:00
dependabot[bot] e4d0f7acd5
Bump golang.org/x/term from 0.15.0 to 0.16.0 (#3564)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/term/compare/v0.15.0...v0.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 12:31:14 +09:00
Junegunn Choi 250496c953
Add 'result' event that is triggered when the result list is ready
Close #3560
2024-01-07 17:46:21 +09:00
Junegunn Choi e47dc758c9
Fix focus event not triggered in certain cases 2024-01-07 15:43:17 +09:00
Junegunn Choi b92a843c5f
Use Ubuntu 22 to match GitHub Actions environment 2024-01-07 15:10:06 +09:00
Junegunn Choi 91bea9c5b3
Use forked version of go-runewidth
Fix #3558

  go get github.com/junegunn/go-runewidth@fzf
2024-01-07 15:09:46 +09:00
junegunn d75bb5cbe1 Deploying to master from @ junegunn/fzf@2671259fdb 🚀 2024-01-07 00:01:39 +00:00
danztran 2671259fdb
[zsh] Make CTRL-R compatible with accept-or-print-query (#3557)
Fix #3556

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-01-05 10:47:54 +09:00
Junegunn Choi 2024010119
0.45.0 2024-01-01 15:38:35 +09:00
Junegunn Choi 412040f77e
Enable preview if 'transform' action is bound to a key 2023-12-31 20:56:33 +09:00
Junegunn Choi d210660ce8
Add actions: show-header and hide-header 2023-12-31 16:01:00 +09:00
Junegunn Choi 863a12562b
Trigger focus actions synchronously 2023-12-31 15:54:37 +09:00
junegunn 5da606a9ac Deploying to master from @ junegunn/fzf@8d20f3d5c4 🚀 2023-12-31 00:01:48 +00:00
Junegunn Choi 8d20f3d5c4
ADVANCED.md: Add toggling example with `transform` and `{fzf:prompt}`
Courtesy of @LangLangBart
2023-12-29 12:27:12 +09:00
Junegunn Choi 5d360180af
Add {fzf:prompt} placeholder expression
Close #3354
2023-12-28 17:10:06 +09:00
Junegunn Choi f0fbed6007
Fix RuboCop error 2023-12-27 01:33:34 +09:00
Junegunn Choi 519de7c833
Fix unexpected result of --tiebreak=end
See https://github.com/junegunn/fzf/issues/3255#issuecomment-1869580320
2023-12-26 23:42:14 +09:00
Junegunn Choi 97ccef1a04
{fzf:query} should trigger preview update
fzf --preview 'echo {fzf:query}'
    fzf --preview 'echo {q}'
2023-12-26 16:51:41 +09:00
Junegunn Choi c4df0dd06e
Add TRANSFORM ACTIONS section to man page 2023-12-26 12:21:06 +09:00
Junegunn Choi cd114c6818
Change transform action to directly execute actions
To avoid filling up input channel for HTTP server
2023-12-26 10:15:53 +09:00
Junegunn Choi 1707b8cdba
Add 'transform' action to conditionally perform a series of actions
'transform' action runs an external command that prints a series of
actions to perform.

  # Disallow selecting an empty line
  echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
    fzf --reverse --header 'Select one' \
        --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'

  # Move cursor past the empty line
  echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
    fzf --reverse --header 'Select one' \
        --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
        --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'

Close #3368
Close #2980
2023-12-26 00:14:05 +09:00
Junegunn Choi 41d4d70b98
Fix shell escaping for fish
Fix #3224
2023-12-25 17:35:44 +09:00
Junegunn Choi 0e999482cb
Fix handling of empty ANSI color sequence
Fix #3320
2023-12-25 17:05:54 +09:00
junegunn 65b2c06027 Deploying to master from @ junegunn/fzf@d7b61ede07 🚀 2023-12-24 00:01:38 +00:00
Junegunn Choi d7b61ede07
Add support for negative --height
fzf --height=-1

Close #3487
2023-12-21 18:42:23 +09:00
dependabot[bot] 87fc1c84b8
Bump actions/setup-go from 4 to 5 (#3537)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 10:50:47 +09:00
dependabot[bot] d4b5f12383
Bump github/codeql-action from 2 to 3 (#3544)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 09:59:59 +09:00
junegunn eb62b0d665 Deploying to master from @ junegunn/fzf@91387a741b 🚀 2023-12-17 00:01:44 +00:00
Jan Verbeek 91387a741b
Terminate simple server success response with double CRLF (#3542)
The simple success case had only the status line plus a single CRLF,
and pedantic HTTP client implementations (`hyper`) stumbled over
this. A double CRLF makes it OK.

Fixes #3541.
2023-12-16 15:15:00 +09:00
Junegunn Choi e8b34cb00d
Clarification on accept-or-print-query vs. become 2023-12-14 23:13:52 +09:00
Alec Scott 82954258c1
Add Spack installation instructions to README (#3526) 2023-12-10 16:08:54 +09:00
Junegunn Choi 50f092551b
Lint: RuboCop 2023-12-10 16:04:30 +09:00
Junegunn Choi c36a64be68
Add accept-or-print-query
Close #3528
2023-12-10 15:59:45 +09:00
dependabot[bot] a343b20775
Bump golang.org/x/term from 0.13.0 to 0.15.0 (#3525)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.13.0 to 0.15.0.
- [Commits](https://github.com/golang/term/compare/v0.13.0...v0.15.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>
2023-12-10 15:16:47 +09:00
junegunn a714e76ae1 Deploying to master from @ junegunn/fzf@d21d5c9510 🚀 2023-12-10 00:01:46 +00:00
junegunn d21d5c9510 Deploying to master from @ junegunn/fzf@cd6788a2bb 🚀 2023-12-03 00:01:38 +00:00
Junegunn Choi cd6788a2bb
Increase buffer size of event channel to avoid freeze on zero event
Fix #3516
2023-11-30 17:23:46 +09:00
junegunn 6b99399c41 Deploying to master from @ junegunn/fzf@952b6af445 🚀 2023-11-26 00:01:42 +00:00
Laurent Cheylus 952b6af445
Allow files creation in /tmp on OpenBSD (#3512)
- src/protector/protector_openbsd.go: add tmppath for pledge
    permissions
  - fix junegunn/fzf#3511

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2023-11-21 16:03:12 +09:00
junegunn 7c674ad7fa Deploying to master from @ junegunn/fzf@d7d2ac3951 🚀 2023-11-19 00:01:41 +00:00
Junegunn Choi d7d2ac3951
0.44.1 2023-11-17 19:17:00 +09:00
Junegunn Choi 29e67d307a
Fix crash when preview window is hidden on focus event 2023-11-17 19:13:37 +09:00
Junegunn Choi 7320b7df62
0.44.0 2023-11-12 22:08:08 +09:00
Tomáš Janoušek 11fb4233f7
Fix Home, End on rxvt-unicode (#3507) 2023-11-12 22:06:38 +09:00
Junegunn Choi 84bb350b14
Reset horizontal offset of the prompt on 'beginning-of-line'
https://github.com/junegunn/fzf/issues/3498#issuecomment-1806651174
2023-11-12 21:41:34 +09:00
Junegunn Choi 38e3694d1c
Revert "Sixel and Kitty image support on Windows binary (#2544)"
This reverts commit 68db9cb499.
2023-11-10 13:16:11 +09:00
dependabot[bot] 1084935241
Bump golang.org/x/sys from 0.13.0 to 0.14.0 (#3503)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/sys/compare/v0.13.0...v0.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-08 12:03:04 +09:00
Junegunn Choi f5f0b9ecaa
Fix a typo 2023-11-08 11:18:36 +09:00
Junegunn Choi 230fc49ae2
(Experimental) Add support for iTerm2 inline image protocol
Close #1102

  fzf --preview 'imgcat -W $FZF_PREVIEW_COLUMNS -H $FZF_PREVIEW_LINES {}'

Notes:
* There is no good way to determine the height of the rendered image,
  so we assume that the image takes the full height of the preview
  window. So the image cannot be displayed with the other text.
* fzf-preview.sh script was updated to use `imgcat` if it's available
  but `chafa` is not.
* iTerm2 also supports Sixel, so adding support for this protocol is not
  quite necessary but it renders animated GIFs much better (e.g. looping).
2023-11-07 11:51:21 +09:00
Junegunn Choi 250d507bdf
Fix a typo on CHANGELOG 2023-11-07 10:50:08 +09:00
Junegunn Choi a818653174
Add --listen-unsafe=ADDR to allow remote process execution (#3498) 2023-11-05 10:53:46 +09:00
junegunn 5c3b044740 Deploying to master from @ junegunn/fzf@c5aa8729a1 🚀 2023-11-05 00:01:39 +00:00
Junegunn Choi c5aa8729a1
Fix failing test case 2023-11-04 16:27:24 +09:00
Junegunn Choi 3f78d76da1
Allow accepting remote connections
Close #3498

  # FZF_API_KEY is required for a non-localhost listen address
  FZF_API_KEY=xxx fzf --listen 0.0.0.0:6266
2023-11-04 16:19:16 +09:00
Junegunn Choi 70c19ccf16
Fix CTRL-Z handling: Signal SIGSTOP to PGID
Fix #3501
2023-11-04 13:46:29 +09:00
Junegunn Choi 68db9cb499
Sixel and Kitty image support on Windows binary (#2544) 2023-11-03 00:09:02 +09:00
Junegunn Choi d0466fa777
Fix regression where tcell renderer not clearing the preview window 2023-11-02 21:00:07 +09:00
Junegunn Choi 21ab64e962
sixel: Export $FZF_PREVIEW_TOP to the preview command (#2544)
So that it can determine if it should subtract 1 from $FZF_PREVIEW_LINES
to avoid scrolling issue of Sixel image that touches the bottom of the
screen.
2023-11-02 01:35:36 +09:00
Junegunn Choi a0145cebf2
sixel: Better handling of animated GIFs (#2544) 2023-11-02 00:15:42 +09:00
Junegunn Choi 69176fc5f4
fzf-preview.sh: Fall back to `stty size` (#2544) 2023-11-02 00:15:39 +09:00
Junegunn Choi 278dce9ba6
Restore scroll after rendering full-height Sixel image (#2544)
When a Sixel image touches the bottom of the screen, the whole screen
scrolls up one line to make room for the cursor. Add an ANSI escape
code to compensate for the movement. Unfortunately, the movement of the
screen is sometimes noticeable.

  fzf --preview='fzf-preview.sh {}' --preview-window border-left
2023-10-31 23:38:01 +09:00
Junegunn Choi 1cfa3ee4c7
fzf-preview.sh: Check the number of arguments 2023-10-30 00:05:54 +09:00
Junegunn Choi 9a95cd5794
Fix Sixel height calculation (#2544) 2023-10-29 23:34:33 +09:00
akdevservices a62fe3df6f
[completion] Handle all hostaliases in /etc/hosts (#3495)
* Fix #3488
* Handle inline comments in hosts file
2023-10-29 09:05:30 +09:00
junegunn 7701244a08 Deploying to master from @ junegunn/fzf@96e31e4b78 🚀 2023-10-29 00:01:39 +00:00
Junegunn Choi 96e31e4b78
Fix Sixel issues (#2544)
* Fix regression where previous image is not properly cleared
* Change the way fzf calculates the number of required lines to display
  an image (ceil -> floor) to fix the issue where an image is always
  rendered as a wireframe.
2023-10-27 14:36:14 +09:00
Junegunn Choi ec208af474
Go 1.18 or above is required
Close #3492
2023-10-26 23:30:46 +09:00
Junegunn Choi 242641264d
Clear previous non-Sixel text before rendering Sixel image (#2544) 2023-10-26 22:28:44 +09:00
Junegunn Choi d3a9a0615b
Fix kitty icat handling 2023-10-26 22:28:44 +09:00
Junegunn Choi 3277e8c89c
Remove $FZF_PREVIEW_PIXEL_{WIDTH,HEIGHT} (#2544)
They are not neccessary because we can use a program such as chafa that
can resize images by the terminal columns and lines.
2023-10-26 22:28:15 +09:00
Junegunn Choi d02b9442a5
(Experimental) Improve Sixel graphics support (#2544)
Progress:

* Sixel image can now be displayed with other text, and is scrollable
* If an image can't be displayed entirely due to the scroll offset, fzf
  will render a wireframe to indicate that an image should be displayed
* Renamed $FZF_PREVIEW_{WIDTH,HEIGHT} to $FZF_PREVIEW_PIXEL_{WIDTH,HEIGHT}
  for clarity
* Added bin/fzf-preview.sh script to demonstrate how to display an image
  using Kitty or Sixel protocol

An example:

  ls *.jpg | fzf --preview='seq $((FZF_PREVIEW_LINES*9/10)); fzf-preview.sh {}; seq 100'

A known issue:

* If you reduce the size of the preview window, the image may extend
  beyond the preview window
2023-10-26 00:49:16 +09:00
Junegunn Choi bac385b59c
Simplify LightRenderer.Size() 2023-10-23 23:40:56 +09:00
Junegunn Choi b1a0ab8086
Experimental Sixel support (#2544) 2023-10-23 01:05:30 +09:00
junegunn a33749eb71 Deploying to master from @ junegunn/fzf@f5e4ee90e4 🚀 2023-10-22 00:01:50 +00:00
Junegunn Choi f5e4ee90e4
Fix bug where top section of the previous preview content appearing
when the preview window is re-enabled and the current preview process is
taking more than 500ms and previewDelayed is triggered

  fzf --preview 'sleep 1; date; seq 1000' --bind space:toggle-preview
2023-10-21 16:11:15 +09:00
Junegunn Choi 690d5e6dbd
Fix scrollability of the preview window when preview offset is specified
This should not be scrollable

  fzf --preview 'seq $FZF_PREVIEW_LINES' --preview-window '~5'
2023-10-20 17:37:08 +09:00
Junegunn Choi a76c055b63
Fix inconsistent preview window width with --border
fzf --preview 'cat {}' --bind 'space:change-preview-window:up|right' --border
2023-10-20 16:03:41 +09:00
Junegunn Choi 70c461c60b
[bash] Preserve existing completion for ssh
Fix #3484
2023-10-19 09:58:36 +09:00
Laurent Cheylus d51b71ee80
Fix crash on OpenBSD with --listen (#3483)
- src/protector/protector_openbsd.go: add inet permissions for pledge
  - fix #3481

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2023-10-17 08:46:43 +09:00
junegunn 3666448ca6 Deploying to master from @ junegunn/fzf@d3311d9f43 🚀 2023-10-15 00:01:43 +00:00
Junegunn Choi d3311d9f43
0.43.0 2023-10-15 01:56:05 +09:00
LangLangBart 3e1735b06e
[zsh] Fix 'emulate: unknown argument -o' error on old zsh (#3465)
Fix #2094
2023-10-14 17:41:01 +09:00
Junegunn Choi de7ef7eace
[fzf-tmux] Fix 'empty command' error on tmux 3.2
Fix #3474
2023-10-13 20:13:28 +09:00
Christoph Anton Mitterer 7e89458a3b
[fish] exit as well when called from non-interactive shell (#3467)
Just like with the other shells, exit fish to, if called from a non-interactive
shell.

We cannot use `return`, as older versions of fish (namely < 3.4.0) did not
support to use `return` in `.`-scripts (this was only added with fish commit
3359e5d2e9bcbf19d1652636c8e448a6889302ae).

Unlike in POSIX, fish’s `exit` is however documented to no cause the calling
shell to exit when executed in a sourced script (see:
0f70b2c0d3/doc_src/cmds/exit.rst?plain=1#L20
)

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-10-13 01:06:55 +09:00
Junegunn Choi f212bafe46
[bash] Remove implicit bash-completion dependency 2023-10-13 01:00:43 +09:00
Christoph Anton Mitterer 86fe40708b [bash] statically define __fzf_list_hosts() with either method
When bash-completion (and thus `_known_hosts_real()`) is / is not available this
will typically not change during the lifetime of a shell.

The only exception is if the user would unset `_known_hosts_real()`, but well,
that would be his problem.

So we can easily define `__fzf_list_hosts()` either using `_known_hosts_real()`
or using the old code, and avoid checking every time whether
`_known_hosts_real()` is defined.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Christoph Anton Mitterer d718747c5b [bash] try to use bash-completions’s _known_hosts_real() for getting hostnames
If defined, use bash-completions’s `_known_hosts_real()`-function to create the
list of hostnames.
This obviously requires bash-completion to be sourced before fzf.

If not defined, fall back to the previous code.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Christoph Anton Mitterer 46ee9ac41c [shell] make `__fzf_list_hosts()` definable by the user
Just like it’s already done for `_fzf_compgen_path()` and `_fzf_compgen_dir()`
allow a user to easily define his own version of `__fzf_list_hosts()`.

Also add some documentation on the expected “interface” of such custom function.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Christoph Anton Mitterer f1d306feab [shell] move username prefixing code where needed
`__fzf_list_hosts()` seems like a function a user may want to override with some
custom code.
For that reason it should be kept as simple as possible, that is printing only
hostnames, one per line, optionally in some sorting.

The handling of adding a `username@` (which is then the same for each line), if
any, would unnecessarily complicate that for people who want to override the
function.
Therefore this commit moves that to the places where it's actually used (as of
now only `_fzf_complete_ssh()`).

This also saves any such handling for `_fzf_host_completion()`, where this isn’t
needed at all.

Right now it comes at a cost, namely an extra invocation of `awk` in the
`_fzf_complete_ssh()`-case.
However, it should be easily possible to improve `__fzf_list_hosts()` to no
longer need the final `awk` in the pipeline there.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Christoph Anton Mitterer 2d0db98e83 [shell] don’t print error on non-existent SSH files
Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Junegunn Choi 3df06a1c68
Fix offset-up and offset-down with --layout=reverse (#3456) 2023-10-12 19:14:03 +09:00
dependabot[bot] a8f9432a3a
Bump golang.org/x/term from 0.12.0 to 0.13.0 (#3469)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/term/compare/v0.12.0...v0.13.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>
2023-10-11 13:17:47 +09:00
Christoph Anton Mitterer 561e0b04a8
[bash] Use `command` to “protect” further commands (#3462)
This commit causes all simple commands that are not built-ins or functions to be
invoked via `command` in order to protect them from alias substitution or from
accidentally taking functions of the same name.

It was decided to not “protect” `fzf` and `fzf-tmux` for now.
Maybe a better solution should be implemented for that in the future.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-11 13:07:47 +09:00
Junegunn Choi 404b6a864b
Add offset-up and offset-down
# Scrolling will behave similarly to CTRL-E and CTRL-Y of vim
  fzf --bind scroll-up:offset-up,scroll-down:offset-down \
      --bind ctrl-y:offset-up,ctrl-e:offset-down \
      --scroll-off=5

Close #3456
2023-10-11 12:53:51 +09:00
Christoph Anton Mitterer 4feaf31225
[bash] bring fzf’s own bash completion up to date (#3471)
* [bash] bring fzf’s own bash completion up to date

This orders and groups completed options and values in just as they appear in
the code respectively, for some option values, as they’d be printed in the
`--help`-output.

It does not add support for completion of `:` right after values that support an
optional `:some-further-value` postfix.
Neither does it add support for the `--option=value`-style.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>

* [bash] drop unnecessary code in handling `history`

Presumably, the dropped code is not needed for any effect, thus drop it.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>

---------

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-11 10:54:50 +09:00
Junegunn Choi 391aa14845
Add mouse events for --bind
Close #3473
2023-10-11 09:42:12 +09:00
Junegunn Choi a0d61b4c37
[install] Remove redundant interactiveness check
Related #3449

/cc @calestyo
2023-10-09 10:00:55 +09:00
Junegunn Choi 2952737755
Update README: Experimental support for Kitty graphics protocol 2023-10-09 01:29:03 +09:00
Christoph Anton Mitterer f103aa4753
Improve interactiveness checks (#3449)
* [bash] return instead of not executing an if-block, when non-interactive

This should keep the code more readable, be less error prone (accidentally doing
something outside the if-block and aligns the code with what’s already done for
zsh.

`0` is returned, because it shall not be considered an error when the script is
(accidentally) sourced from a non-interactive shell.

If executed as a script (rather than sourced), the results are not specified by
POSIX but depend on the shell, with bash giving an error in that case.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>

* [shell] exit immediately when called from non-interactive shell

The shell execution environment shouldn’t be modified at all, when called from a
non-interactive shell.

It shall be noted that the current check may become error prone for bash, namely
in case there should ever be a differentiation between `i` and `I` in the
special variable `-` and bash’s `nocasematch`-shell-option be used.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-09 01:19:28 +09:00
junegunn 884856023a Deploying to master from @ junegunn/fzf@d8188fce7b 🚀 2023-10-08 00:01:43 +00:00
Junegunn Choi d8188fce7b
Experimental support for Kitty image protocol in preview window
Close #3228

* Works inside and outside of tmux
* There is a problem where fzf unnecessarily displays the scroll offset
  indicator at the topbright of the screen when the image just fits the
  preview window. This is because `kitty icat` generates an extra line
  after the image area.

    # A 5-row images; an extra row at the end confuses fzf
    ["\e_Ga ... \e[9C􎻮̅̅ࠪ􎻮̅̍ࠪ􎻮̅̎ࠪ􎻮̅̐ࠪ􎻮̅̒ࠪ􎻮̅̽ࠪ􎻮̅̾ࠪ􎻮̅̿ࠪ􎻮̅͆ࠪ􎻮̅͊ࠪ􎻮̅͋ࠪ\n",
     "\r\e[9C􎻮̍̅ࠪ􎻮̍̍ࠪ􎻮̍̎ࠪ􎻮̍̐ࠪ􎻮̍̒ࠪ􎻮̍̽ࠪ􎻮̍̾ࠪ􎻮̍̿ࠪ􎻮̍͆ࠪ􎻮̍͊ࠪ􎻮̍͋ࠪ\n",
     "\r\e[9C􎻮̎̅ࠪ􎻮̎̍ࠪ􎻮̎̎ࠪ􎻮̎̐ࠪ􎻮̎̒ࠪ􎻮̎̽ࠪ􎻮̎̾ࠪ􎻮̎̿ࠪ􎻮̎͆ࠪ􎻮̎͊ࠪ􎻮̎͋ࠪ\n",
     "\r\e[9C􎻮̐̅ࠪ􎻮̐̍ࠪ􎻮̐̎ࠪ􎻮̐̐ࠪ􎻮̐̒ࠪ􎻮̐̽ࠪ􎻮̐̾ࠪ􎻮̐̿ࠪ􎻮̐͆ࠪ􎻮̐͊ࠪ􎻮̐͋ࠪ\n",
     "\r\e[9C􎻮̒̅ࠪ􎻮̒̍ࠪ􎻮̒̎ࠪ􎻮̒̐ࠪ􎻮̒̒ࠪ􎻮̒̽ࠪ􎻮̒̾ࠪ􎻮̒̿ࠪ􎻮̒͆ࠪ􎻮̒͊ࠪ􎻮̒͋ࠪ\n",
     "\r\e[39m\e8"]

* Example:

  fzf --preview='
    if file --mime-type {} | grep -qF 'image/'; then
      # --transfer-mode=memory is the fastest option but if you want fzf to be able
      # to redraw the image on terminal resize or on 'change-preview-window',
      # you need to use --transfer-mode=stream.
      kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {}
    else
      bat --color=always {}
    fi
  '
2023-10-07 18:36:33 +09:00
step 0f15f1ab73
[bash] Improve mawk detection (#3463)
* Use the all-compatible mawk `-W version` option.
  https://github.com/junegunn/fzf/pull/3313#issuecomment-1747934690.
* Run the command and not a function consistently with #3462.

The version check bash code relies on the following mawk source code,
extracted from mawk 1.3.4 20230322.

```
version.c:
18-  #include "init.h"
19-  #include "patchlev.h"
20-
21:  #define	 VERSION_STRING	 \
22-    "mawk %d.%d%s %s\n\
23-  Copyright 2008-2022,2023, Thomas E. Dickey\n\
24-  Copyright 1991-1996,2014, Michael D. Brennan\n\n"
....
30-  void
31-  print_version(FILE *fp)
32-  {
33:      fprintf(fp, VERSION_STRING, PATCH_BASE, PATCH_LEVEL, PATCH_STRING, DATE_STRING);
34-      fflush(fp);
35-
36-  #define SHOW_RANDOM "random-funcs:"

patchlev.h:
13-  /*
14-   * $MawkId: patchlev.h,v 1.128 2023/03/23 00:23:57 tom Exp $
15-   */
16:  #define  PATCH_BASE	1
17-  #define  PATCH_LEVEL	3
18-  #define  PATCH_STRING	".4"
19-  #define  DATE_STRING    "20230322"
```

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-10-07 17:26:46 +09:00
Junegunn Choi 488a236b7a
[shell] Avoid side-effects during eval (#3459)
Take two.

* Avoid eval if the prefix contains `:=`
    * This is not to evaluate variable assignment. e.g. ${FOO:=BAR}
* [zsh] Prevent `>(...)` form
* Suppress error message from prefix evaluation
* Stop completion when prefix evaluation failed

Thanks to @calestyo
2023-10-04 21:43:11 +09:00
Christoph Anton Mitterer e833823e15
[bash] Don’t print function definition when checking for existence (#3448)
When just checking whether a function is already defined or not, it’s not
necessary to print out it’s definition (should it be defined).

bash’s `declare` provides the `-F`-option (which implies `-f`), which should
give a minor performance improvement

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-02 21:02:35 +09:00
Junegunn Choi ee4ba104e7
[completion] Prevent running a command during 'eval'
Do not attempt to provide fuzzy completion if the prefix contains a
pattern that may start an arbitraty command.

* $(...)
* `...`
* <(...)

Close #3459
2023-10-02 20:40:49 +09:00
junegunn 4fdc08295b Deploying to master from @ junegunn/fzf@a3ff49aaf1 🚀 2023-10-01 00:01:42 +00:00
Junegunn Choi a3ff49aaf1
[bash] CTRL-R on bash 3: Use backticks to avoid delay
e0b29e437b
2023-09-27 09:16:16 +09:00
Junegunn Choi 76364ea767 Remove unnecessary escaping in the default command 2023-09-24 13:23:40 +09:00
Christoph Anton Mitterer 8eec50d764 [shell] don’t needlessly escape `.` in shell pattern
`find`’s `-path`-option is described to use shell patterns (i.e. POSIX’ pattern
matching notation).

In that, `.` is not a special character, thus escaping it shouldn’t be
necessary.

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-09-24 13:23:40 +09:00
junegunn 32b659b346 Deploying to master from @ junegunn/fzf@00809909ae 🚀 2023-09-24 00:01:56 +00:00
Junegunn Choi 00809909ae
Update CHANGELOG 2023-09-22 21:59:47 +09:00
step 9f7684f6fe
[bash] History, use perl if installed otherwise awk (#3313)
While awk is POSIX, perl isn't pre-installed on all *nix flavors.
This commit eliminates the mandatory dependency on perl by using awk
when perl is not available.

Related: #3295, #3309, #3310.

Test suite passed:
* `make error` all test sections 'PASS'
* `make docker-test` 215 runs, 1884 assertions, 0 failures, 0 errors, 0 skips.

Manually tested in the following environments:
* Linux amd64 with bash 3.2, 4.4, 5.2; gawk -P, one true awk, mawk, busybox awk.
* macOS Catalina, bash 3.2, macOS awk 20070501.

**Performance comparison:**

Mawk turned out the fastest, then perl.
One true awk's implementation should be the closest to macOS awk.
Test data: 230 KB history, 15102 entries, including multi-line and duplicates.
Linux, bash 4.4. Times in milliseconds.

| Command                 | Mean | Min  | Max  | Relative |
| :---                    | ---: | ---: | ---: | -------: |
| `mawk 1.3.4`            | 22.9 | 22.3 | 25.6 | **1.00** |
| `perl 5.26.1`           | 34.3 | 33.6 | 35.1 |   1.49   |
| `one true awk 20221215` | 41.9 | 40.6 | 46.3 |   1.83   |
| `gawk 5.1.0`            | 46.1 | 44.4 | 50.3 |   2.01   |
| `busybox awk 1.27.0`    | 64.8 | 63.2 | 70.0 |   2.82   |

**Other Notes**

A bug affects bash, which fails restoring a saved multi-line history entry as a single entry. Bug fixed in version 5.0.[^1]

While developing this PR I discovered two unsubmitted issues affecting the current perl script. The output stream ends with `$'\n\0000'` instead of `$'\0000'`. Because of this, the script does not deduplicate a duplicated entry located at the end of the history list; therefore fzf displays two identical (not necessarily adjacent) entries. A minor point about the first issue is that the top fzf entry ends with a dangling line feed symbol, which is visible in the terminal.

[^1]: ec8113b986/CHANGES (L1511)
  To enable: `shopt -s cmdhist lithist; HISTTIMEFORMAT='%F %T '`.
2023-09-22 17:37:34 +09:00
Junegunn Choi 2bed7d370e
[shell] Use --scheme=path when appropriate
Without the option, you may get suboptimal results if you have many
paths with spaces in their names.

e.g. https://github.com/junegunn/fzf/issues/2909#issuecomment-1207690770

Close #3433
2023-09-19 13:39:57 +09:00
dependabot[bot] d2b852f7cb
Bump golang.org/x/term from 0.11.0 to 0.12.0 (#3426)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/term/compare/v0.11.0...v0.12.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>
2023-09-18 09:20:46 +09:00
Junegunn Choi 901939bd96
Add support for limit and offset parameters for GET / endpoint
Related #3372
2023-09-18 00:55:20 +09:00
Timofei Bredov edfdcc8cee
Basic context-aware completion for ssh command (#3424)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-09-18 00:15:04 +09:00
Junegunn Choi 3982c9a552
[vimdoc] Replace unicode figure space (U+2007) with regular space
Related: https://github.com/junegunn/fzf.vim/pull/1507

Thanks to @balki
2023-09-17 23:55:39 +09:00
Junegunn Choi 4490b2d209
Respect ANSI codes to reset properties
Fix #3441
2023-09-16 19:50:37 +09:00
Sam James eb4bbf3294
Makefile: build 32-bit binary on armv8l (#3434)
armv8l is always 32-bit and should implement the armv7 ISA, so
just use the same filename as for armv7.

This avoids wrongly building a 64-bit binary because of an incorrect assumption
of what 'armv8l' is (a 32-bit system).

Obviously, we should not then build a 64-bit (arm64) binary. Especially
given armv8l is often used in arm32-on-arm64 chroots to build stuff for
weaker-powered arm32 devices.

Signed-off-by: Sam James <sam@gentoo.org>
2023-09-11 19:32:45 +09:00
junegunn dc97d48491 Deploying to master from @ junegunn/fzf@0f50dc848e 🚀 2023-09-10 00:01:48 +00:00
Junegunn Choi 0f50dc848e
Add 'GET /' endpoint for getting the program state (experimental)
Related #3372
2023-09-03 16:30:35 +09:00
Junegunn Choi c5e4b83de3
Update sponsor list once a week 2023-09-02 20:48:10 +09:00
junegunn a08ab46713 Deploying to master from @ junegunn/fzf@f50a7058d6 🚀 2023-09-02 00:03:23 +00:00
Junegunn Choi f50a7058d6
Fix center-alignment of border/preview label
Fix #3421
2023-09-01 20:30:44 +09:00
junegunn 2c74f0a040 Deploying to master from @ junegunn/fzf@58835e40f3 🚀 2023-09-01 00:03:34 +00:00
Junegunn Choi 58835e40f3
Run GitHub Sponsors action once a day 2023-08-29 19:42:30 +09:00
junegunn 8befa5918a Deploying to master from @ junegunn/fzf@df80f7ff2a 🚀 2023-08-28 08:05:07 +00:00
junegunn df80f7ff2a Deploying to master from @ junegunn/fzf@5f66786ef1 🚀 2023-08-28 07:04:03 +00:00
Junegunn Choi 5f66786ef1
[install] Replace go get with go install
Fix #3365
2023-08-26 20:00:16 +09:00
Junegunn Choi 3a965856a5
[vim] Keep jump list unaffected when calling term_start
Fix #3415
2023-08-26 19:53:57 +09:00
junegunn 03df609d77 Deploying to master from @ junegunn/fzf@178581b560 🚀 2023-08-26 02:06:35 +00:00
junegunn 178581b560 Deploying to master from @ junegunn/fzf@ffd2314120 🚀 2023-08-25 15:04:07 +00:00
Junegunn Choi ffd2314120
Restore --no-clear option in man page
Close #3411
2023-08-25 17:59:50 +09:00
Chandan Mangu 815b595d2f
[fzf-tmux] Turn off remain-on-exit only on fzf-tmux pane (#3410)
* fix: turn off remain-on-exit only on fzf-tmux pane

Using `fzf-tmux` overwrites `remain-on-exit` for all panes in a window,
if it is only set globally or at a higher scope than window.

	set-option -wg remain-on-exit on
	set-option -s remain-on-exit on

This makes other panes in that window close immediately on exit after
using `fzf-tmux`, even though I expect them to remain open.

Since TMux 3.0, `remain-on-exit` is a pane option that can be set via
`set-option -p`. This will limit the option's scope to just the
`fzf-tmux` pane, thus allowing us to close it immediately without
overriding `remain-on-exit` on other panes in the window.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
Link: 11e69f6025/CHANGES (L753-L760)
Link: https://github.com/tmux/tmux/releases/tag/3.0
Related: https://github.com/junegunn/fzf/issues/3397

* fix: turn off synchronize-panes only on fzf-tmux pane

Similar reason to 482fd2b (fix: turn off remain-on-exit only on fzf-tmux
pane, 2023-08-24).

	Limit scope on which option is set to bare minimum.

Have confirmed this will not feed input back to other panes which are
set to be synchronized. However, note that this will not stop `fzf-tmux`
from being launched by two synchronized panes in parallel.

Link: https://github.com/junegunn/fzf/issues/3397#issuecomment-1689295351

---------

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-08-24 16:31:38 +09:00
Junegunn Choi 11e56403dd
[man] Clarify --scheme option
Close #3387
2023-08-22 15:16:51 +09:00
junegunn 4baadecda5 Deploying to master from @ junegunn/fzf@cf552b5f3b 🚀 2023-08-21 21:04:10 +00:00
junegunn cf552b5f3b Deploying to master from @ junegunn/fzf@1894304d33 🚀 2023-08-18 15:04:01 +00:00
Junegunn Choi 1894304d33
[bash] Disable pipefail in command substitution
Fix #3382
2023-08-18 13:37:56 +09:00
sitiom 9d5392fb02
Change Winget Releaser job to `ubuntu-latest` (#3403) 2023-08-17 17:17:38 +09:00
dependabot[bot] c280645671
Bump crate-ci/typos from 1.15.0 to 1.16.4 (#3400)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.15.0 to 1.16.4.
- [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.15.0...v1.16.4)

---
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>
2023-08-17 11:51:31 +09:00
dependabot[bot] 45f92e6b38
Bump actions/checkout from 2 to 3 (#3399)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 11:51:13 +09:00
Junegunn Choi 6509f09961
Set up GitHub Sponsors Readme Action (#3398) 2023-08-14 16:39:39 +09:00
junegunn 3c279a6f0e Deploying to master from @ junegunn/fzf@40515f11a4 🚀 2023-08-14 07:11:33 +00:00
Nikita Kouevda 9ec3f03871
doc: Add different prefix for Homebrew on Apple Silicon (#3396)
Follow-up to d42e708d31.

Fixes #3095.
2023-08-12 23:29:00 +09:00
dependabot[bot] 84a9c2c112
Bump golang.org/x/term from 0.10.0 to 0.11.0 (#3393)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/term/compare/v0.10.0...v0.11.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>
2023-08-08 14:10:03 +09:00
dependabot[bot] af368119cb
Bump golang.org/x/sys from 0.10.0 to 0.11.0 (#3392)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/sys/compare/v0.10.0...v0.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 00:25:19 +09:00
Junegunn Choi 89b9189efa
[fzf-tmux] Pass $RUNEWIDTH_EASTASIAN
Close #3385
2023-08-03 16:47:19 +09:00
Junegunn Choi dd59b8c7b9
Fix ANSI color continuation in --header
# Both lines should be in red
  fzf --header $'\x1b[31mfoo\nbar'
2023-07-25 22:20:31 +09:00
Junegunn Choi f83491274f
Add toggle-header option
Close #3358
2023-07-25 22:11:15 +09:00
Boaz Yaniv c0435fdff4
Add API Keys for fzf --listen (#3374) 2023-07-20 23:42:09 +09:00
Bart 3c09c77269
Fix deprecations of ioutil (#3370) 2023-07-16 17:14:22 +09:00
Junegunn Choi 547e101f1d
Use $SHELL instead of bash if it's known to support 'pipefail'
when running the default find command

Close #3339
Close #3364
2023-07-12 13:55:59 +09:00
dependabot[bot] 0130f64934
Bump golang.org/x/term from 0.9.0 to 0.10.0 (#3360)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/term/compare/v0.9.0...v0.10.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>
2023-07-11 00:35:44 +09:00
dependabot[bot] 361e0543ee
Bump golang.org/x/sys from 0.9.0 to 0.10.0 (#3361)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/sys/compare/v0.9.0...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 22:43:45 +09:00
Junegunn Choi 63aa5d3b4e
Correct outdated comment 2023-07-05 23:42:34 +09:00
dependabot[bot] 01302d097c
Bump golang.org/x/term from 0.8.0 to 0.9.0 (#3338)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/term/compare/v0.8.0...v0.9.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>
2023-06-22 17:00:06 +09:00
dependabot[bot] e6095cb7e8
Bump golang.org/x/sys from 0.8.0 to 0.9.0 (#3337)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/sys/compare/v0.8.0...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-21 13:41:21 +09:00
dependabot[bot] b876b8af11
Bump crate-ci/typos from 1.14.10 to 1.15.0 (#3331)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.14.10 to 1.15.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.14.10...v1.15.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>
2023-06-21 13:40:48 +09:00
Junegunn Choi a7c41f3fcd
Add '--info=right' to the man page
Close #3333
2023-06-17 19:15:29 +09:00
guangwu 4772bd8d4c
Use strings.ContainsRune instead (#3335) 2023-06-17 19:10:12 +09:00
Junegunn Choi d471067e3f
0.42.0 2023-06-15 00:37:41 +09:00
Junegunn Choi d0b7780239
Add --info=right
Related: #3322
2023-06-11 16:09:15 +09:00
Junegunn Choi e627ca6bd7
Add --info=inline-right
Close #3322
2023-06-10 23:11:05 +09:00
Junegunn Choi c97172bdd4
Fix background color of spinner on the preview window 2023-06-10 14:48:47 +09:00
Mike ce8a745fb4
Add new border style: 'thinblock' (#3327)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-06-10 14:48:29 +09:00
Junegunn Choi 3e9efd1401
[vim] Only prepend --border option in $FZF_DEFAULT_OPTS
Fix #3318
2023-06-03 09:03:04 +09:00
Junegunn Choi 20340190b5
[fzf-tmux] Pass $BAT_THEME
This may anger some purists, but bat is widely used as the previewer so
I think it's worth it.
2023-05-31 20:16:30 +09:00
Junegunn Choi 265040a78c
[vim] Respect --border optin in $FZF_DEFAULT_OPTS 2023-05-31 20:09:14 +09:00
Junegunn Choi 448d7e0c5a
Update test case 2023-05-27 16:01:30 +09:00
Junegunn Choi 6eb1874c5a
0.41.1 2023-05-27 15:54:22 +09:00
Junegunn Choi 4c70745cc1
Fix bug where preview is not updated after reload when --disabled is set
Fix #3311
2023-05-27 15:51:04 +09:00
Junegunn Choi 7795748a3f
Remove dead code 2023-05-27 15:10:47 +09:00
Junegunn Choi 098ef4d7cf
0.41.0 2023-05-26 00:25:09 +09:00
Junegunn Choi e3f91bfe1b
Use Golang 1.20.4 2023-05-26 00:08:20 +09:00
Junegunn Choi 7374fe73a3
Avoid setting $FZF_DEFAULT_COMMAND
So that it's not propagated to the child processes and affect the
behavior of fzf started by them.

fzf 0.41.0 or above is required as it fixed the issue where
'become' process is not given a proper tty device.

Close #3299
2023-05-26 00:08:20 +09:00
dependabot[bot] d2bde205f0
Bump golang.org/x/term from 0.7.0 to 0.8.0 (#3285)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/term/compare/v0.7.0...v0.8.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>
2023-05-23 23:55:57 +09:00
dependabot[bot] 5620f70f9a
Bump crate-ci/typos from 1.13.16 to 1.14.10 (#3306)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.13.16 to 1.14.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.13.16...v1.14.10)

---
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>
2023-05-23 23:55:34 +09:00
Syphdias 37f258b1bf
Add key combinations for ctrl-delete and shift-delete (#3284)
Currently there is not option to bind ctrl-delete and shift-delete. As
suggested by issue #3240, shift-delete could be used to bind "delete
entry from history" as it is a common way to do so in other
applications, e.g. browsers.

This, however, does only implement to use the key combination itself and
does not assign a default action to any of them. This does enable to
call one's all predefined actions. With the exec action this can
expanded like the issue #3240 suggested.
If desirable, the key combinations could later get a default behavior.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-05-21 18:40:05 +09:00
Junegunn Choi e2dd2a133e
Skip post hooks on 'make build'
https://github.com/goreleaser/goreleaser/issues/1469
2023-05-21 18:00:17 +09:00
Junegunn Choi 7514644e07
Update Dockerfile: --platform=linux/amd64 2023-05-21 17:59:18 +09:00
Junegunn Choi 16b0aeda7d
Make sure 'become' process is given a proper tty device 2023-05-21 14:01:27 +09:00
Junegunn Choi 86e4f4a841
Update tcell renderer to support block border 2023-05-20 18:24:23 +09:00
Junegunn Choi 607eacf8c7
Allow unbind(focus)
Fix #3279
2023-05-17 10:58:03 +09:00
Junegunn Choi 7a049644a8
Fix panic when trying to render preview window of a negative height
Fix #3292
2023-05-17 00:06:35 +09:00
Junegunn Choi 17a13f00f8
Allow customizing scrollbar of the preview window via --scrollbar=xy 2023-05-16 23:59:08 +09:00
Junegunn Choi 43436e48e0
Add new border style: 'block' 2023-05-16 23:45:31 +09:00
Junegunn Choi 5a39102405
Allow customizing the color of preview scrollbar via 'preview-scrollbar' 2023-05-16 23:24:05 +09:00
Junegunn Choi 94999101e3
Fix the behavior of change-preview-window action (#3280)
* change-preview-window restores the initial preview window options,
  and overrides the properties that are specified
* However, 'hidden' property is treated differently. It is set to
  'false' if the specified properties of the action is non-empty.
* cf. toggle-preview takes the "current" preview window options and
  toggles the 'hidden' property.
2023-05-05 15:33:03 +09:00
Junegunn Choi e619b7c4f4
Fix the background color of the scrollbar inside the preview window 2023-05-01 16:18:26 +09:00
Junegunn Choi b7c2e8cb67
Fix caching when reload and query change triggered by the same binding 2023-05-01 13:53:34 +09:00
Junegunn Choi fb76893e18
0.40.0 2023-05-01 01:59:21 +09:00
Junegunn Choi 88d812fe82
Do not display trailing carriage returns in the preview window
Close #3269
2023-04-30 18:14:40 +09:00
Junegunn Choi 77f9f4664a
Fix search not triggered when query change and reload happen at the same time
Fix #3268
2023-04-30 18:14:40 +09:00
dependabot[bot] 5c2f85c39e
Bump golang.org/x/term from 0.6.0 to 0.7.0 (#3249)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.6.0...v0.7.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>
2023-04-28 23:24:56 +09:00
dependabot[bot] ac4d22cd12
Bump golang.org/x/sys from 0.6.0 to 0.7.0 (#3248)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/compare/v0.6.0...v0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 23:20:28 +09:00
Junegunn Choi cf95e44cb4
Add 'zero' event
Close #3263
2023-04-26 15:13:08 +09:00
Junegunn Choi 65dd2bb429
Add 'track' action 2023-04-22 23:42:09 +09:00
Junegunn Choi 6be855be6a
Add change-header and transform-header
Close #3237
2023-04-22 22:01:37 +09:00
Junegunn Choi b6e3f4423b
[man] Suggest setting RUNEWIDTH_EASTASIAN to 0 or 1
Close #2389
2023-04-22 16:35:46 +09:00
Junegunn Choi 0c61d81713
Add toggle-track action 2023-04-22 15:48:51 +09:00
Junegunn Choi 7c6f5dba63
Fixed --track when used with --tac
Fix #3234
2023-04-22 15:09:43 +09:00
psarlov 44cfc7e62a
[vim] Add check for powershell 7 users (#3257)
Co-authored-by: Pavel Sarlov <psarlov@asteasolutions.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-04-21 20:23:03 +09:00
Junegunn Choi 96670d5f16
Disallow using --track with --tac
Close #3234
2023-04-12 13:23:10 +09:00
Junegunn Choi 36b971ee4e
[fzf-tmux] Try awk before bc 2023-04-11 16:29:29 +09:00
Junegunn Choi f1a9629652
[fzf-tmux] Use awk if bc is not found
Fix #3235
2023-04-06 13:43:18 +09:00
Junegunn Choi 20230402d0
0.39.0 2023-04-02 23:33:37 +09:00
Junegunn Choi 5c2c3a6c88
Use Go 1.20.2 2023-04-02 23:27:22 +09:00
tyama711 fb019d43bf
Fix a bug of height range with -1 or -0 (#3226)
Fixed a bug that when both heightUnknown and deferred are true, deferred is not properly reset and the program terminates abnormally.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-04-02 23:26:13 +09:00
Junegunn Choi 025aa33773
[fzf-tmux] Disallow popup mode on tmux 3.1 or below
Close #3198
2023-04-02 00:16:00 +09:00
Junegunn Choi 302e21fd58
[shell] Update kill completion
* Explicitly specify the list of fields for consistent experience
* Add fallback command for BusyBox (Close #3219)
* Apply `--header-lines=1` to show the column header
2023-04-01 19:52:34 +09:00
Junegunn Choi 211512ae64
Fix Rubocop error 2023-04-01 17:29:13 +09:00
Junegunn Choi 8ec917b1c3
Add 'one' event
Close #2629
Close #2494
Close #459
2023-04-01 17:25:47 +09:00
Junegunn Choi 1c7534f009
Add --track option to track the current selection
Close #3186
Related #1890
2023-04-01 12:59:44 +09:00
Sten Arthur Laane ae745d9397
Add bat to bash autocomplete commands (#3223)
Bat is a common alternative to cat, it's even referenced multiple times
in fzf docs. This makes `bat **` work by default.
2023-03-27 12:21:37 +09:00
Junegunn Choi 60f37aae2f
Respect 'regular' attribute in 'bw' base theme
Don't make the text bold if an element is explicitly specified as
'regular'.

Fix #3222
2023-03-26 23:39:05 +09:00
Junegunn Choi d7daf5f724
Render CR and LF as ␍ and ␊
Close #2529
2023-03-25 10:41:19 +09:00
Vitaly Zdanevich e5103d9429
README.md: package managers: add Portage/Gentoo (#3205) 2023-03-22 09:57:50 +09:00
dependabot[bot] 8fecb29848
Bump github.com/rivo/uniseg from 0.4.2 to 0.4.4 (#3192)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.4.2 to 0.4.4.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.4.2...v0.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:53:20 +09:00
dependabot[bot] 290ea6179d
Bump golang.org/x/term from 0.0.0-20210927222741-03fcf44c2211 to 0.6.0 (#3203)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.0.0-20210927222741-03fcf44c2211 to 0.6.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/commits/v0.6.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>
2023-03-21 16:19:55 +09:00
dependabot[bot] 9695a40fc9
Bump golang.org/x/sys from 0.0.0-20220811171246-fbc7d0a398ab to 0.6.0 (#3202)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.0.0-20220811171246-fbc7d0a398ab to 0.6.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/commits/v0.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:14:14 +09:00
dependabot[bot] 1913b95227
Bump actions/setup-go from 3 to 4 (#3216)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:13:02 +09:00
Junegunn Choi a874aea692
[vim] More explanation on 'set rtp+=~/.fzf' instruction
Close #3171
2023-03-20 22:33:14 +09:00
Michael Vorburger ⛑️ 69c52099e7
docs: Fix intention of README (#3214)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-03-20 13:00:33 +09:00
Junegunn Choi cfc0747d5d
Follow Rubocop suggestion 2023-03-19 15:53:31 +09:00
Junegunn Choi fcd7e8768d
Omit port number in `--listen` for automatic port assignment
Close #3200
2023-03-19 15:48:39 +09:00
Junegunn Choi 3c34dd8275
Fix extra new line in the preview window
When a colored text ends at the right end of the window

Fix #3209
2023-03-17 13:22:20 +09:00
Junegunn Choi 1116e481be
[vim] Update setqflist example
Without 'lnum', cfdo doesn't work

Close https://github.com/junegunn/fzf.vim/issues/1435
2023-03-10 22:22:22 +09:00
dependabot[bot] 63cf9d04de
Bump crate-ci/typos from 1.13.10 to 1.13.16 (#3194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-03-07 01:21:23 +09:00
Zhizhen He 3364d4d147
Add spell check workflow (#3183) 2023-02-23 00:36:04 +09:00
Junegunn Choi 57ad21e4bd Build and release s390x binaries 2023-02-22 14:31:41 +09:00
Julian Ruess 414f87981f Add support for s390x architecture
Signed-off-by: Julian Ruess <julianr@linux.ibm.com>
2023-02-22 14:31:41 +09:00
Junegunn Choi b1459c79cf
Make sure that the query before the cursor is not hidden
Close #3176
2023-02-19 16:38:26 +09:00
Junegunn Choi 352ea07226
0.38.0 2023-02-15 23:24:42 +09:00
Junegunn Choi 27018787af
Describe become(...) action and use it to simplify examples 2023-02-15 23:24:42 +09:00
Junegunn Choi 4e305eca26
become: Set stdin to /dev/tty 2023-02-15 23:24:42 +09:00
sitiom 9e9c0ceaf4
Add Winget Releaser workflow (#3164) 2023-02-15 16:47:12 +09:00
Junegunn Choi b3bf18b1c0
[fzf-tmux] Fix version check
The output of `tmux -V` starts with "tmux ".
2023-02-13 15:25:39 +09:00
Junegunn Choi b1619f675f
[fzf-tmux] Do not set --margin 0,1 on tmux 3.3 or above
Close #3162
2023-02-13 14:49:02 +09:00
Junegunn Choi 96c3de12eb
Run 'become' only when the command template is properly evaluated 2023-02-12 22:06:21 +09:00
Junegunn Choi 719dbb8bae
Update ADVANCED.md: transform-query to restore the query string
Close #2961
2023-02-12 17:23:17 +09:00
Junegunn Choi f38a7f7f8f
[bash] Enable environment variable completion for printenv
Close #3145
2023-02-12 16:58:36 +09:00
Junegunn Choi 6ea38b4438
Add become(...) action that replaces current fzf process
Close #3159
2023-02-11 20:26:31 +09:00
Junegunn Choi f7447aece1
Code cleanup 2023-02-01 18:16:58 +09:00
Junegunn Choi aa2b9ec476
Add 'show-preview' and 'hide-preview'
For cases where 'toggle-preview' is not enough
2023-01-31 17:34:11 +09:00
Junegunn Choi 3ee00f8bc2
toggle-preview should not show empty preview window 2023-01-30 22:13:29 +09:00
Junegunn Choi fccab60a5c
`--preview-window 0,hidden` should not execute the preview command
Until `toggle-preview` action is triggered

Fix #3149
2023-01-30 21:39:18 +09:00
Junegunn Choi 0f4af38457
[vim] Simplify --border injection
Prepend the border options so that the user can override them in
'options' entry of the spec.
2023-01-27 14:00:22 +09:00
Junegunn Choi aef39f1160
[vim] Fix missing --border when --border-label is present 2023-01-27 11:00:59 +09:00
Junegunn Choi 2023012408
0.37.0 2023-01-24 22:11:14 +09:00
Junegunn Choi 95a7661bb1
Sanitize input strings that should be a single line 2023-01-24 19:35:29 +09:00
Junegunn Choi 618d317803
Support custom separator of inline info
Close #2030
Close #3084
2023-01-24 17:55:06 +09:00
Junegunn Choi ae897c8cdb
No need to touch mouse flag if it's already false 2023-01-24 12:45:07 +09:00
Junegunn Choi d0a0f3c052
Temporarily disable mouse mode when switching to an external command 2023-01-24 12:41:40 +09:00
Junegunn Choi 91b9591b10
Reenable mouse mode when coming back from an external program
Close #3141
2023-01-24 12:00:41 +09:00
Junegunn Choi aa7361337d
Make test case pass on 32-bit platforms
Close #3127
2023-01-23 18:30:36 +09:00
Junegunn Choi 284d77fe2e
Add 'focus' event
Can we find a better name? I have considered the followings.

* 'point', because "the pointer" points to the current item.
* 'shift', 'switch', 'move', etc. These are not technically correct
  because the current item can change without cursor movement (--tac,
  reload, search update)
* 'change' is already taken. 'change-current' feels a bit wordy and
  sounds wrong, 'current-changed' is wordy and doesn't go well with the
  other event names
* 'target', not straightforward

Close #3053
2023-01-23 16:38:24 +09:00
Junegunn Choi 826178f1e2
Do not restore terminal state while running an external command 2023-01-23 02:24:13 +09:00
Junegunn Choi acccf8a9b8
Fix TOC 2023-01-23 02:24:13 +09:00
Francesco Bigagnoli 57c066f0be
Fix bat url in README (#3129) 2023-01-23 02:21:16 +09:00
Nachum Barcohen e44f64ae92
Add Helix editor to bash autocompletion (#3137) 2023-01-23 02:21:04 +09:00
Junegunn Choi d51980a3f5 Add 'transform-border-label' and 'transform-preview-label' 2023-01-22 02:18:19 +09:00
jpcrs c3d73e7ecb Add change-border-label and change-preview-label actions, update man 2023-01-22 02:18:19 +09:00
Junegunn Choi b077f6821d
Action argument in enclosed form should allow new lines
Close #3138
2023-01-21 22:20:26 +09:00
Junegunn Choi a79de11af7
README: Add FZF_TMUX_OPTS example for tmux popup 2023-01-19 13:25:08 +09:00
Junegunn Choi 2023011763
0.36.0 2023-01-17 01:33:05 +09:00
Junegunn Choi b46e40e86b
[vim] Automatically set RUNEWIDTH_EASTASIAN=1 when &ambiwidth == double 2023-01-17 01:03:16 +09:00
Junegunn Choi a6d6cdd165
[vim] Use system-default border style
* 'rounded' on non-Windows platforms
* 'sharp' on Windows
2023-01-16 22:44:22 +09:00
Junegunn Choi dc8da605f9
Fix rendering of double-column borders on light renderer 2023-01-16 19:55:44 +09:00
Junegunn Choi 8b299a29c7
Fix rendering of double-column borders 2023-01-16 19:34:28 +09:00
Junegunn Choi 3109b865d2
Fix typo on man page 2023-01-16 01:32:48 +09:00
Junegunn Choi 0c5956c43c
Better support for Windows terminals
* Default border style on Windows is changed to `sharp` because some
  Windows terminals are not capable of displaying `rounded` border
  characters correctly.
* If your terminal emulator renders each box-drawing character with
  2 columns, set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
2023-01-16 01:26:39 +09:00
Junegunn Choi 1c83b39691
Update README examples 2023-01-13 21:15:03 +09:00
Junegunn Choi 77874b473c
Update Rubocop dependencies 2023-01-12 23:37:23 +09:00
Junegunn Choi b7cce7be15
Remove unused block argument 2023-01-12 23:24:39 +09:00
Junegunn Choi 3cd3362417
Fix test failure 2023-01-12 23:23:29 +09:00
Junegunn Choi e97e925efb
Resume preview following if the user scrolls the window to the bottom 2023-01-12 23:18:41 +09:00
Farooq Karimi Zadeh 0f032235cf
Correct package manager commands for apt (#3117) 2023-01-10 10:46:13 +09:00
Junegunn Choi e0f0984da7
Allow re-enabling preview follow on change-preview-window 2023-01-09 11:08:06 +09:00
Junegunn Choi 4d22b5aaef
Disable preview follow after dragging the scrollbar
TBD: Should we re-enable follow once the offset reaches the bottom?
2023-01-09 11:07:31 +09:00
Junegunn Choi 80b8846318
Run preview command when preview window appears after resize (#3113)
# Start fzf in a small screen so that the preview window is hidden
  fzf --bind 'ctrl-p:toggle-preview' --preview 'stat {}' --preview-window='right,50%,<100(down,50%,hidden)'

  # Enlarge the screen until the preview window appears. It should not be empty.
2023-01-07 16:11:35 +09:00
Junegunn Choi bf641faafa
Prevent fzf crashing on malformed remote action 2023-01-07 15:38:02 +09:00
Junegunn Choi 23d8b78ce1
Allow toggling of alternative preview window layout that is hidden
Fix #3113
2023-01-07 15:12:31 +09:00
Junegunn Choi 3b2244077d
Add scrollbar to the preview window 2023-01-06 15:36:12 +09:00
Junegunn Choi ee5cdb9713
Reduce flickering of the scroll info panel on the preview window 2023-01-05 01:59:22 +09:00
Junegunn Choi 03d02d67f7
Fix cyclic scrolling with non-zero preview header lines
e.g. fzf --preview-window 'cycle,~2' --preview 'echo foo; echo bar; seq 100'
2023-01-04 22:00:00 +09:00
Junegunn Choi 5798145581
Fix preview border on tcell renderer 2023-01-04 10:09:39 +09:00
dependabot[bot] 51ef0b7f66 Bump github.com/gdamore/tcell/v2 from 2.5.3 to 2.5.4
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.5.3 to 2.5.4.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.5.3...v2.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-03 02:58:09 +09:00
dependabot[bot] 97b4542c73 Bump github.com/mattn/go-isatty from 0.0.16 to 0.0.17
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.16 to 0.0.17.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.16...v0.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-03 02:38:57 +09:00
Junegunn Choi c1cd0c09a2
Allow dragging of the preview window 2023-01-03 01:46:37 +09:00
Junegunn Choi 1fc1f47d80
Fix double-click on light renderer 2023-01-03 01:39:16 +09:00
Junegunn Choi ec471a5bc2
Make sure two consecutive double clicks require four clicks 2023-01-03 01:36:51 +09:00
Junegunn Choi a893fc0ca2
Clicks with different x coordinates shouldn't be seen as a double-click 2023-01-03 01:21:40 +09:00
Junegunn Choi 3761dc0433
Avoid superfluous update of scrollbar 2023-01-02 01:10:01 +09:00
Junegunn Choi aa71a07fbe
Fix scrollbar rendering (#3096) 2023-01-01 21:18:20 +09:00
Junegunn Choi 088293f5e7
Restore mouse drag mode (#3096) 2023-01-01 21:04:40 +09:00
Junegunn Choi 7c660aa86e
Allow dragging of scrollbar 2023-01-01 19:01:56 +09:00
Junegunn Choi 435d8fa0a2
Colors for 'separator' and 'scrollbar' will default to that for 'border' 2023-01-01 19:01:56 +09:00
Junegunn Choi 5cd6f1d064
Add scrollbar
Close #3096
2023-01-01 14:48:14 +09:00
Junegunn Choi ec20dfe312
Only allow local requests 2022-12-31 23:13:14 +09:00
Junegunn Choi 924ffb5a35
Fix cache being immediately cleared on reload-sync 2022-12-31 09:33:23 +09:00
Junegunn Choi 62c7f59b94
Add transform-prompt(...) action 2022-12-31 09:27:11 +09:00
Junegunn Choi e97176b1d7
Update transform-query examples for zsh
Close #3107
2022-12-30 17:12:55 +09:00
Junegunn Choi d649f5d826
Always execute preview command if {q} is in the template
Even when {q} is empty. Because, why not?

While this can be seen as a breaking change, there is an easy workaround
to keep the old behavior.

    # This will show // even when the query is empty
    : | fzf --preview 'echo /{q}/'

    # But if you don't want it,
    : | fzf --preview '[ -n {q} ] || exit; echo /{q}/'

Close #2759
2022-12-30 14:29:17 +09:00
Junegunn Choi 6c37177cf5
Add reload-sync action
Close #2816
2022-12-29 20:04:33 +09:00
Junegunn Choi 14775aa975
Add 'load' event that is triggered when the input stream is complete
and the first search (with or without query) is complete
2022-12-29 20:01:50 +09:00
Junegunn Choi 44b6336372
Make server channel buffered
Not to block an action that calls the API

  fzf --listen 6266 --bind 'space:execute-silent:curl localhost:6266 -d up'
2022-12-28 12:52:19 +09:00
Junegunn Choi 36d2bb332b
Add transform-query(...) action
Test case authored by @SpicyLemon

Close #1930
Close #2465
Close #2559
Close #2509 (e.g. fzf --bind 'space:transform-query:printf %s%s {q} {}')
2022-12-28 00:05:31 +09:00
Junegunn Choi 4dbe45640a
Remove $FZF_LISTEN_PORT
It is not worth the added complexity.
2022-12-27 22:12:06 +09:00
Junegunn Choi 4b3f0b9f08
Allow put action with an argument i.e. put(...) 2022-12-27 19:54:46 +09:00
Junegunn Choi 12af069dca
Add pos(...) action to move the cursor to the numeric position
# Put the cursor on the 10th item
  seq 100 | fzf --sync --bind 'start:pos(10)'

  # Put the cursor on the 10th to last item
  seq 100 | fzf --sync --bind 'start:pos(-10)'

Close #3069
Close #395
2022-12-27 01:08:42 +09:00
Junegunn Choi d42e708d31
Update README-VIM: Different homebrew prefix on Apple Silicon
Close #3095
2022-12-26 16:25:36 +09:00
Junegunn Choi b7bb973118
Revert "Add GET endpoints for getting the state of the finder"
This reverts commit 750b2a6313.

This can cause a deadlock if the endpoints are accessed in the core event
loop via execute action.

  fzf --listen 6266 --bind 'space:execute:curl localhost:6266'

Technically, there's no reason to use the API because the information is
already available via `{}` and `{q}`, but I'd like to completely remove
the risk of misuse.
2022-12-25 20:00:00 +09:00
Junegunn Choi 750b2a6313
Add GET endpoints for getting the state of the finder
* GET / (or GET /current)
* GET /query
2022-12-25 16:27:02 +09:00
Philipp Wagner de0da86bd7
Add ppc64le binaries (#3067)
Little-endian 64 bit PowerPC (ppc64le) is the "normal" PowerPC
architecture supported by standard Linux distributions (RedHat, SUSE,
Ubuntu, etc.).

Add support for this architecture in the install script, and add binary
builds for it as well.
2022-12-23 15:41:13 +09:00
Junegunn Choi 8e283f512a
Fix bind spec parser 2022-12-23 15:37:39 +09:00
Junegunn Choi 73162a4bc3
Rewrite bind spec parser 2022-12-23 03:28:16 +09:00
Junegunn Choi 1a9761736e
Add time and size limit to remote requests 2022-12-22 20:44:49 +09:00
Junegunn Choi fd1f7665a7
Abort fzf if --listen port is unavailable 2022-12-21 13:02:25 +09:00
Junegunn Choi 6d14573fd0 Add test case for --listen 2022-12-21 01:35:08 +09:00
Junegunn Choi cf69b836ac Only trim CR and NF from the submitted expression
So the trailing space in the following case is respected.

  curl -XPOST localhost:6266 -d "change-prompt:$(date)> "
2022-12-21 01:35:08 +09:00
Junegunn Choi a7a771b92b Break out of jump mode when any action is submitted to the server 2022-12-21 01:35:08 +09:00
Junegunn Choi def011c029 Fix parse error of actions with arguments 2022-12-21 01:35:08 +09:00
Junegunn Choi 4b055bf260 Rewrite HTTP server without net/http
This cuts down the binary size from 5.7MB to 3.3MB.
2022-12-21 01:35:08 +09:00
Junegunn Choi 1ba7484d60 Add --listen=HTTP_PORT option to receive actions
Supersedes #2019

See also:
* #1728
* https://github.com/junegunn/fzf.vim/pull/1044
2022-12-21 01:35:08 +09:00
Junegunn Choi 51c518da1e
Add change-query(...) action 2022-12-18 00:26:31 +09:00
polluks2 a3b6b03dfb
Fix typo (#3093)
Co-authored-by: polluks <polluks@sdf.lonestar.org>
2022-12-17 23:58:43 +09:00
Junegunn Choi 18e3b38c69
Add 'next-selected' and 'prev-selected' actions
Close #2749
2022-12-11 00:59:34 +09:00
Junegunn Choi 0ad30063ff
Rename previous-history to prev-history
previous-history is still supported for backward compatibility
2022-12-10 22:42:56 +09:00
Junegunn Choi 7812c64a31
Fix uninitialized colors in base themes
Fix #3079
2022-12-10 16:55:19 +09:00
Junegunn Choi 3d2376ab52
Add color name 'preview-label' (#3053) 2022-12-09 12:05:27 +09:00
Junegunn Choi 6b207bbf2b
Fix inconsistent bonus points in exact match
Exact match would assign a different bonus point to the first character
when non-default --scheme was used.

Fix #3073
2022-12-04 22:17:39 +09:00
Bjørn Forsman 3f079ba7c6
README.md: Clarify on FZF_*_OPTS (#3064)
At first I thought they were appended to FZF_*_COMMAND. Let's make it
clear that these are passed to `fzf` itself.
2022-12-02 14:57:32 +09:00
Junegunn Choi 8f4c89f50e
Make 'double-click' behave the same as 'enter' by default
Close #3061
2022-11-29 20:27:29 +09:00
OHZEKI Naoki 6b7a543c82
Add more util tests (#3062)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-11-27 16:30:59 +09:00
Junegunn Choi 2ba68d24f2
Do not erase info separator before redrawing it 2022-11-25 23:51:49 +09:00
Bruno Heridet 46877e0a92
test(eventbox): remove obsolete EvtClose const (#3059) 2022-11-23 19:38:14 +09:00
Junegunn Choi b55f555487
0.35.1 2022-11-18 20:42:56 +09:00
Junegunn Choi a38b63be18
Fix mouse event above fzf finder
Fix #2949
2022-11-18 20:32:59 +09:00
Junegunn Choi 1bebd6f4f5
Fix panic on inverse match query with `--tiebreak=chunk`
Fix #3055
2022-11-18 20:16:43 +09:00
Bruno Heridet 3da63f394d
doc(man): complete the definition of what --no-unicode impacts (#3054) 2022-11-18 10:23:21 +09:00
Zhizhen He 2a54e3d770
Fix typos in the source code (#3048) 2022-11-18 10:23:04 +09:00
dependabot[bot] 06b02ba46e
Bump actions/dependency-review-action from 2 to 3 (#3046)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 2 to 3.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 10:22:08 +09:00
Junegunn Choi d01ae55109
0.35.0 2022-11-12 00:58:06 +09:00
Junegunn Choi 8868d7d188
Add --separator to customize the info separator 2022-11-10 16:23:33 +09:00
Junegunn Choi 2eec9892be
[neovim] Use Normal group colors for floating window
Instead of NormalFloat.

https://github.com/junegunn/fzf/issues/3035#issuecomment-1305094043
2022-11-08 00:32:41 +09:00
Junegunn Choi 01ae621f11
Add --border=[bold|double] and --preview-window=border-[bold|double] 2022-11-06 14:38:31 +09:00
Junegunn Choi f984aa0d2c
Fix --border-label and --preview-label on tcell renderer 2022-11-06 14:35:20 +09:00
Junegunn Choi 0881a6bc17
[neovim] Do not use Pmenu group colors for floating window
In Neovim, the foreground and background colors of a floating window
defaults to those of Pmenu highlight group, which yields unexpected
results.

This commit makes the colors of fzf window defaults to those of 'Normal'
group (or 'NormalFloat' if defined), by ignoring Pmenu group.

Then the colors can be configured via --color option of fzf.

NOTE: An error from setwinvar call is ignored because the exact
behavior of &winhighlight with an empty target group is not clearly
documented.

Close #3035
Close https://github.com/junegunn/fzf.vim/issues/1431
See https://github.com/neovim/neovim/pull/9722#discussion_r264777602
2022-11-05 16:52:23 +09:00
Junegunn Choi 2c6a73546d
Fix rubocop check 2022-11-01 13:59:17 +09:00
Junegunn Choi a29944660e
Fix typo in CHANGELOG 2022-11-01 13:33:09 +09:00
Junegunn Choi f6ce624c6f
Add tests for --border-label and --preview-label
Also fix failing tests due to info separator

Related #3022 #3029
2022-11-01 13:30:41 +09:00
Junegunn Choi c09ec8e4d1
Allow putting border label on the bottom line
Related #3022
2022-11-01 13:30:40 +09:00
Junegunn Choi 31bbaad06e
Add --preview-label and --preview-label-pos
Close #3022
2022-11-01 13:27:22 +09:00
Junegunn Choi b9ca1fe830
Add horizontal separator after info panel (counter)
Close #3029
2022-11-01 13:27:11 +09:00
Junegunn Choi e61585f2f3
Add --border-label and --border-label-pos
Close #3022
2022-11-01 13:27:00 +09:00
Junegunn Choi 0de1aacb0c
[vim] Fix version check on Windows when shellslash is set 2022-10-31 10:57:05 +09:00
Junegunn Choi 168829b555
Add 'start' event that is triggered once when fzf finder starts
Close #1622
2022-10-27 00:38:38 +09:00
dependabot[bot] 170fc517d0
Use actions/setup-go v3 (#3021)
* Bump actions/setup-go from 3.3.0 to 3.3.1

Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](268d8c0ca0...c4a742cab1)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Apply suggestions from code review

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-25 15:54:44 +09:00
Naveen 0fbf1c7c71
Add dependency review (#2817)
* chore(deps): Included dependency review

> Dependency Review GitHub Action in your repository to enforce dependency
> reviews on your pull requests.
> The action scans for vulnerable versions of dependencies introduced by package version
> changes in pull requests,
> and warns you about the associated security vulnerabilities.
> This gives you better visibility of what's changing in a pull request,
> and helps prevent vulnerabilities being added to your repository.

https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>

* Update .github/workflows/depsreview.yaml

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-25 09:34:58 +09:00
dependabot[bot] 694be39c71
Use ruby/setup-ruby v1 (#3012)
* Bump ruby/setup-ruby from 1.117.0 to 1.118.0

Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.117.0 to 1.118.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](3068fa83f9...eae47962ba)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Apply suggestions from code review

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-24 20:07:31 +09:00
dependabot[bot] dad26d81df
Use github/codeql-action@v2 (#2998)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-16 22:36:59 +09:00
dependabot[bot] bcaea097ea
Bump actions/checkout from 61b9e3751b92087fd0b06925ba6dd6314e06f089 to v3 (#2997)
* Bump actions/checkout

Bumps [actions/checkout](https://github.com/actions/checkout) from 61b9e3751b92087fd0b06925ba6dd6314e06f089 to 3.1.0. This release includes the previously tagged commit.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](61b9e3751b...93ea575cb5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Apply suggestions from code review

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-16 22:33:49 +09:00
Junegunn Choi d56fe74e24
Add checksums of Darwin binaries
Close #2989
2022-10-16 22:06:40 +09:00
John Fred Fadrigalan 4603d540c3
[shell] Make bash/zsh completion and bindings work with 'set -u' (#2999)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-10-16 17:15:19 +09:00
Junegunn Choi f9d53303bb
[vim] Remove unnecessary powershell check
&shell is guaranteed to be cmd.exe on windows because we call s:use_sh()
2022-10-13 14:48:32 +09:00
Junegunn Choi d04faa6505
[vim] Fix escaping of fzf binary path containing spaces on Windows
Fix #2992
2022-10-12 20:07:58 +09:00
Kyle L. Davis 07da058eae
[vim] Update fzf#install to handle spaces on Windows (#2993) 2022-10-08 19:20:39 +09:00
Bruno Heridet cefa6b9878
doc(man): add a hint about which UI element is the finder info (#2991)
While reading the description of the --info flag, it's not
immediately obvious that the "finder info" is in fact the
UI element representing the match counters.
2022-10-04 09:52:28 +09:00
Junegunn Choi 04d0b0223f
0.34.0 2022-09-28 23:22:31 +09:00
Junegunn Choi 78ad6d2d88
Phase out --no-clear in favor of bindable 'reload' action 2022-09-28 23:22:31 +09:00
Junegunn Choi 22cbd9fa58
Implement height range (--height ~[VALUE][%])
Close #2953
2022-09-28 23:22:31 +09:00
dependabot[bot] 984049586a
Bump github.com/mattn/go-runewidth from 0.0.13 to 0.0.14 (#2984)
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.13 to 0.0.14.
- [Release notes](https://github.com/mattn/go-runewidth/releases)
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.13...v0.0.14)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 17:03:19 +09:00
dependabot[bot] cdfc2b92e3
Bump github.com/rivo/uniseg from 0.2.0 to 0.4.2 (#2964)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.2.0 to 0.4.2.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.2.0...v0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 17:02:55 +09:00
dependabot[bot] 4530abe8df
Bump ruby/setup-ruby from 1.101.0 to 1.117.0 (#2974)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.101.0 to 1.117.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](ebaea52cb2...3068fa83f9)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 16:55:35 +09:00
dependabot[bot] 586020b8b6
Bump actions/checkout from 629c2de402a417ea7690ca6ce3f33229e27606a5 to 61b9e3751b92087fd0b06925ba6dd6314e06f089 (#2965)
* Bump actions/checkout

Bumps [actions/checkout](https://github.com/actions/checkout) from 629c2de402a417ea7690ca6ce3f33229e27606a5 to 61b9e3751b92087fd0b06925ba6dd6314e06f089.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](629c2de402...61b9e3751b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update .github/workflows/codeql-analysis.yml

* Update .github/workflows/macos.yml

* Update .github/workflows/linux.yml

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-09-28 16:55:13 +09:00
dependabot[bot] 3a8626fd04
Bump github.com/saracen/walker from 0.1.2 to 0.1.3 (#2880)
Bumps [github.com/saracen/walker](https://github.com/saracen/walker) from 0.1.2 to 0.1.3.
- [Release notes](https://github.com/saracen/walker/releases)
- [Commits](https://github.com/saracen/walker/compare/v0.1.2...v0.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 16:53:56 +09:00
dependabot[bot] a6e483a434
Bump actions/setup-go from 3.0.0 to 3.3.0 (#2946)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.0.0 to 3.3.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](f6164bd8c8...268d8c0ca0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 16:53:04 +09:00
dependabot[bot] 6a942e56b1
Bump github/codeql-action from 2.1.8 to 2.1.25 (#2985)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.8 to 2.1.25.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](1ed1437484...86f3159a69)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 16:50:36 +09:00
dependabot[bot] 87c91550ad
Bump github.com/mattn/go-isatty from 0.0.14 to 0.0.16 (#2926)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.14 to 0.0.16.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.14...v0.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-28 16:50:19 +09:00
Junegunn Choi 731daf0f37
Fix tcell renderer
Fix #2954
2022-09-26 14:09:38 +09:00
Junegunn Choi f931e53890
[fish] Do not use `builtin cd`
`builtin cd` of fish doesn't support `cd -`

Close #2967
2022-09-20 16:59:07 +09:00
Junegunn Choi b5efc68737
Revert "Add Sponsor Labels action"
This reverts commit 845034c81c.
2022-09-13 09:43:11 +09:00
knutze b9e6e7926c
[bash] Fix completion of var or alias containing newlines (#2952)
* Fix bash completion var or aliase containing newlines

* Support for various bash declare options

Co-authored-by: knutze <shakte@gmail.com>
2022-09-10 11:38:41 +09:00
Junegunn Choi 845034c81c
Add Sponsor Labels action 2022-09-10 11:25:32 +09:00
Abirdcfly 54d42e3f40
Fix typo in CHANGELOG (#2948) 2022-08-30 13:28:01 +09:00
Junegunn Choi e03ac3136e
0.33.0 2022-08-29 07:23:14 +09:00
Junegunn Choi 6fb41a202a
Add --scheme=[default|path|history] option to choose scoring scheme
Close #2909
Close #2930
2022-08-28 22:22:39 +09:00
Emil Vanherp 4bef330ce1
Add support for ANSI strike-through (#2932)
Close #2932

Co-authored-by: Emil Vanherp <emil@vanherp.me>
2022-08-26 09:27:49 +09:00
Junegunn Choi 8a5f719964
ADVANCED: fzf-git.sh 2022-08-25 17:54:35 +09:00
Charlie Vieth 209d5e8e90
ansi: speed up escape sequence parsing (#2927) 2022-08-25 14:02:08 +09:00
Junegunn Choi 9d041aa582
Update README 2022-08-23 13:29:41 +09:00
Junegunn Choi 6532b3e655
[completion] Remove extra trailing slash on directory completion
Fix #2931
2022-08-22 22:29:51 +09:00
Junegunn Choi c1c355160d
Support border-{up,down} as the synonyms for border-{top,bottom} 2022-08-20 00:11:17 +09:00
Junegunn Choi 83515d5610
Update ANSI test cases 2022-08-13 22:40:37 +09:00
Junegunn Choi aa10dccf90
Support colon delimiter in ANSI escape sequences
# Both should work
    printf "\e[38;5;208mOption 1\e[m\nOption 2" | fzf --ansi
    printf "\e[38:5:208mOption 1\e[m\nOption 2" | fzf --ansi

This change makes ANSI parsing slightly slower.

    cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz

    Before:
      BenchmarkNextAnsiEscapeSequence-12            992.22 MB/s
      BenchmarkExtractColor-12                      174.35 MB/s

    After:
      BenchmarkNextAnsiEscapeSequence-12            925.05 MB/s
      BenchmarkExtractColor-12                      163.33 MB/s

Fix #2913
2022-08-13 22:30:50 +09:00
Junegunn Choi f4fd53211a
Reformat comments adhere to gofmt 2022-08-12 22:11:15 +09:00
Junegunn Choi 4993d19466
0.32.1 2022-08-08 23:57:32 +09:00
Junegunn Choi 19f9bbca0d
Allow specifying fzf options in $FZF_TMUX_OPTS without '--' 2022-08-08 23:50:47 +09:00
Junegunn Choi 779d8e1627
Use go 1.19 2022-08-08 13:39:00 +09:00
Junegunn Choi bb07410448
Add --strip-cwd-prefix to fd examples 2022-08-08 13:38:21 +09:00
Yang Tian d826f9e72f
[fzf-tmux] Use fzf border instead of tmux popup border (#2908)
Co-authored-by: Yang Tian <yang.tian@getcruise.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-08-07 10:28:45 +09:00
Junegunn Choi 6a6130615d
[fzf-tmux] Remove code for tmux 3.2 beta compatibility 2022-08-06 19:08:13 +09:00
lbesnard a8e7021be2
[completion] ssh: Remove values with '%' (#2548)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-08-03 22:39:42 +09:00
Junegunn Choi 38259d0382
Fix incorrect ordering of `--tiebreak=chunk` 2022-08-03 22:18:26 +09:00
Junegunn Choi f7e7259910
0.32.0 2022-08-02 21:56:14 +09:00
Junegunn Choi f0bfeba733
Add new tiebreak: 'chunk'
Favors the line with shorter matched chunk. A chunk is a set of
consecutive non-whitespace characters.

Unlike the default `length`, this new scheme works well with tabular input.

  # length prefers item #1, because the whole line is shorter,
  # chunk prefers item #2, because the matched chunk ("foo") is shorter
  fzf --height=6 --header-lines=2 --tiebreak=chunk --reverse --query=fo << "EOF"
  N | Field1 | Field2 | Field3
  - | ------ | ------ | ------
  1 | hello  | foobar | baz
  2 | world  | foo    | bazbaz
  EOF

If the input does not contain any spaces, `chunk` is equivalent to
`length`. But we're not going to set it as the default because it is
computationally more expensive.

Close #2285
Close #2537
- Not the exact solution to --tiebreak=length not taking --nth into account,
  but this should work. And the added benefit is that it works well even
  when --nth is not provided.
- Adding a bonus point to the last character of a word didn't turn out great.
  The order of the result suddenly changes when you type in the last
  character in the word producing a jarring effect.
2022-08-02 21:48:19 +09:00
Junegunn Choi c3a7a24eea
Tweak bonus points to word boundaries
Close https://github.com/junegunn/fzf.vim/issues/1004

  # jobs/latency.js is favored over job_latency.js
  printf 'job_latency.js\njobs/latency.js' | fzf -qlatency
2022-08-02 20:57:13 +09:00
Junegunn Choi bbbcd780c9
Allow "--version" to be used as the argument to --query, --header, et al.
But why?
2022-08-02 20:57:12 +09:00
kissge 475469a2e7
[zsh] Make awk regex compatible with both GNU and macOS (#2906) 2022-08-02 20:56:25 +09:00
Junegunn Choi 3a7447dcb6
Update FUNDING.yml 2022-07-30 19:35:41 +09:00
Junegunn Choi e5d8cbd383
[vim] Fix version check on windows/powershell (addendum)
Should handle powershell.exe as well

Fix https://github.com/junegunn/fzf.vim/issues/1411
2022-07-30 19:08:04 +09:00
Junegunn Choi 3c08dca7e7
Fix README examples so that they work both on bash and zsh
Close #2887
2022-07-29 22:03:02 +09:00
Carl Kamholtz d083f01d22
[vim] Add option to force 24 bit colors on Windows (#2889) 2022-07-29 21:51:35 +09:00
Bob Matcuk 68cf393644
[bash] Fix 'possible retry loop' problem of bash-completion (#2891)
Close #2474
Close #2583
2022-07-29 21:50:59 +09:00
Junegunn Choi 18f7230662
Fix mouse location in --height mode
Fix #2900
2022-07-29 15:42:44 +09:00
Carl Kamholtz 728f735281
[vim] Fix version check on windows/powershell (#2894)
- Replace fzf#shellescape with shellescape
- Prepend command with '&' in powershell to deal with quoted exe
2022-07-29 08:18:06 +09:00
Junegunn Choi ecc418ba77
0.31.0 2022-07-21 23:04:57 +09:00
Junegunn Choi 3af5b7f2ac
Do not validate other options when --version is present
Close #2690
2022-07-21 22:36:52 +09:00
Junegunn Choi 7a7cfcacbe
Lift unicode.IsGraphic constraint for pointer, marker, and ellipsis
Use at your own risk.

Close #2709
Close #2055
2022-07-21 22:30:01 +09:00
Junegunn Choi 52594355bf
[shell] 'kill' completion will now require trigger sequence (**)
'kill **<tab>' instead of 'kill <tab>' just like any other completions.

Close #2716
Close #385
2022-07-21 22:21:11 +09:00
Junegunn Choi 0d06c28b19
Fix delimiter regex to properly support caret (^)
Fix #2861
2022-07-21 21:21:06 +09:00
Junegunn Choi ccc4677252
[vim] fzf#exec: Shell-escape fzf binary path
Fix #2877
2022-07-20 14:29:52 +09:00
Junegunn Choi 821fc9feed
Fix failing test case 2022-07-20 12:29:45 +09:00
Junegunn Choi 82b46726fc
Add support for an alternative preview window layout
Close #2804
Close #2844

Related #2277
2022-07-20 12:08:54 +09:00
Jakub Jirutka 8df872a482
[zsh] Replace perl with awk (#2777)
Unlike awk, which is even defined in POSIX, perl is not pre-installed
on all *nix systems. This awk command is functionally equivalent to
the original perl command.
2022-07-20 11:53:34 +09:00
Jonathan Zacsh c79c306adb
[bash] Fix `fzf-tmux` to have `fzf`'s completion (#2871) 2022-07-15 15:53:23 +09:00
Junegunn Choi 51fdaad002
[uninstall] Remove readlink to support relative symlinks of dotfiles
Close #2853
2022-07-01 16:32:32 +09:00
znley 885cd8ff04
[make] Add loongarch64 support (#2857) 2022-06-28 09:16:26 +09:00
Tanish Yadav 2707af403a
[shell] Don't export PATH in ~/.fzf.{bash,zsh} (#2852)
There is no use exporting PATH when it is already exported. Moreover, it
causes things like `typeset -U path` in zsh to break if done before
sourcing "~/.fzf.zsh".
2022-06-24 16:53:51 +09:00
Junegunn Choi 2d227e5222
[man] Fix unescaped backslash in awk example
Close #2854
2022-06-23 15:45:36 +09:00
Junegunn Choi 70529878e2
Use SGR mouse mode for larger terminals
Fix #2840
2022-06-17 12:48:04 +09:00
Daniel Zhang 3b7a962dc6
[vim] Fix fzf#shellescape when shell=fish (#2828)
`shellescape()` behavior is different when `shell=fish`, so we should set `shell` before calling `shellescape()`, otherwise an unexpected result may occur (e.g. https://github.com/kevinhwang91/nvim-bqf/issues/56).

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-05-25 09:50:10 +09:00
Jan Warchoł 6dcf5c3d7d
[bash] Make complex commands slightly more friendly to work with (#2784)
- extract logical parts to separate variables (e.g. $opts)
- put options in $opts in similar order
- move +/-m into $opts (at the end, so they won't be overridden)
- split pipelines into multiple lines
- remove "echo" that seems to be redundant

All this should help with readability and also result in cleaner diffs
when changes are made.
2022-04-29 19:04:16 +09:00
Junegunn Choi b089bb5e7b
Fix scrollability of the preview window in certain cases
Fix #2683

This commit fixes the cases where fzf incorrectly determines the
scrollability of the preview window when `--preview-window-wrap` is set.

Wrapping of the preview content happens during the rendering phase, so
it's currently not possible to know how many lines are actually needed
to display the content beforehand. So `preview-bottom` still may not
move to the very bottom with wrapping enabled.
2022-04-28 11:46:24 +09:00
dependabot[bot] a91a67668e
Bump github/codeql-action from 2.1.6 to 2.1.8 (#2787)
* Bump github/codeql-action from 2.1.6 to 2.1.8

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.6 to 2.1.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](28eead2408...1ed1437484)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Delete incorrect comments

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-04-22 22:36:56 +09:00
dependabot[bot] 220a908118
Bump ruby/setup-ruby from 1.100.0 to 1.101.0 (#2795)
* Bump ruby/setup-ruby from 1.100.0 to 1.101.0

Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.100.0 to 1.101.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](bd94d6a504...ebaea52cb2)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Delete incorrect comments

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-04-22 22:35:51 +09:00
Ajeet D'Souza 54841248e7
[shell] ALT-C: Use builtin cd to avoid conflicts (#2799) 2022-04-22 21:58:32 +09:00
Junegunn Choi a0b42e6538
Require Go 1.17 or above 2022-04-07 20:31:19 +09:00
Jan Warchoł 3312cf525d
[bash] Allow passing args to __fzf_select__ via fzf-file-widget (#2783)
This makes it easier to make customizations, for example instead of

    bind -x '"\C-o\C-i": FZF_CTRL_T_COMMAND="fasd -Rl" FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --tiebreak=index " fzf-file-widget'

it's enough to just

    bind -x '"\C-o\C-i": FZF_CTRL_T_COMMAND="fasd -Rl" fzf-file-widget --tiebreak=index'
2022-04-06 11:29:01 +09:00
Junegunn Choi 2093667548
0.30.0 2022-04-04 23:01:43 +09:00
Junegunn Choi 3c868d7961
ADVANCED.md: Add rebind example 2022-04-04 22:59:26 +09:00
dependabot[bot] 707f4f5816
Bump github/codeql-action from 1.1.5 to 2.1.6 (#2782)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1.1.5 to 2.1.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8834766498...28eead2408)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 22:44:10 +09:00
Junegunn Choi b3ab6311c5
Hide cursor while rendering the screen
Fix #2781
Fix #2588
Fix #1805

Fix https://github.com/junegunn/fzf.vim/issues/1370
Fix https://github.com/junegunn/fzf.vim/issues/1060
2022-04-04 22:06:16 +09:00
Junegunn Choi d56f605b63
Add `rebind` action for restoring bindings after `unbind`
Fix #2752
Close #2564
2022-04-04 21:54:22 +09:00
Junegunn Choi f8b713f425
Remove redundant state update on reload
Related: 5209e95
2022-03-31 10:05:28 +09:00
Junegunn Choi 5209e95bc7
Make preview updated when reload and change-query are combined
Fix #2744
2022-03-29 22:27:03 +09:00
Junegunn Choi ef67a45702
Add --ellipsis=.. option
Close #2432

Also see
- #1769
- https://github.com/junegunn/fzf/pull/1844#issuecomment-586663660
2022-03-29 21:35:36 +09:00
Junegunn Choi b88eb72ac2
Modernize build tags 2022-03-29 21:23:45 +09:00
dependabot[bot] 32847f7254
Bump actions/setup-go from 2.2.0 to 3 (#2776)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](bfdd3570ce...f6164bd8c8)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-29 21:17:44 +09:00
dependabot[bot] 71df93b534
Bump ruby/setup-ruby from 1.62.0 to 1.100.0 (#2775)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.62.0 to 1.100.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](5aaa89ff0d...bd94d6a504)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  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>
2022-03-29 21:17:29 +09:00
Naveen bb028191f8
Set up dependabot for GitHub actions (#2764) 2022-03-29 21:08:41 +09:00
Naveen 19af8fc7d8
Pin actions to a full length commit SHA (#2765)
- Pinned actions by SHA https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies
- Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

>Pin actions to a full length commit SHA

>Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload.

https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions

Also, dependabot supports upgrade based on SHA.

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>s
2022-03-29 21:08:01 +09:00
Junegunn Choi a06671b47f
Increase TTY buffer limit
Kitty's shell intergration generates a long sequence of key presses in
certain cases. As long as the length of the sequence is finite, fzf can
process it.

Close #2748
2022-03-09 17:02:06 +09:00
Junegunn Choi 5f385d88e0
[zsh] Set up bindings for all three keymaps: emacs, vicmd, and viins
Fix #2694
2022-02-23 15:36:49 +09:00
Junegunn Choi 9cb7a364a3
[install] Remove code that might delete user fish script
Fix #2703
2022-01-05 21:48:20 +09:00
Junegunn Choi f68cbc577d
Add link to ADVANCED.md
Related #2701
2022-01-03 13:52:46 +09:00
Junegunn Choi dc975e8974
0.29.0 2021-12-25 01:46:01 +09:00
Junegunn Choi 4311ade535
ADVANCED.md: Add change-preview-window example 2021-12-24 14:41:47 +09:00
Junegunn Choi cd23401411
Fix rendering of the prompt line when overflow occurs with `--info=inline`
Fix #2692
2021-12-22 23:23:50 +09:00
Martin Jindra 176ee6910f
Update Dockerfile (#2662)
`archlinux/base:latest`cannot be found
2021-12-09 11:05:12 +09:00
Junegunn Choi 13c8f3d3aa
[vim] Handle writefile() failure gracefully
Fix #2676
2021-12-07 17:33:26 +09:00
Junegunn Choi ce9af687bc
Remove unused code 2021-12-05 21:17:38 +09:00
Junegunn Choi 43f0d0cacd
change-preview-window to take multiple option sets separated by '|'
So you can "rotate" through the different options with a single binding.

  fzf --preview 'cat {}' \
      --bind 'ctrl-/:change-preview-window(70%|down,40%,border-horizontal|hidden|)'

Close #2376
2021-12-05 21:13:10 +09:00
Junegunn Choi 20b4e6953e
Implement change-preview and change-preview-window actions
The new actions are named with 'change-' prefix to differentiate from
the pre-existing, one-off 'preview(...)' action.

Fix #2360
Fix #2505
Fix #2666

Related #2435
Related #2376
  - Can set up multiple bindings with different change-preview-window actions
  - Not possible to "rotate" through the options with a single binding
  - Enlarge or shrink not possible
2021-11-30 23:57:46 +09:00
Kai 7da287e3aa
README.md: HTTP => HTTPS (#2673) 2021-11-28 22:28:32 +09:00
zsugabubus 205f885d69
[shell] Use cd -- (#2659)
Otherwise directories starting with '-' may treated as options.
2021-11-19 10:36:28 +09:00
Junegunn Choi 3715cd349d
Add repology packaging status badge 2021-11-18 15:47:06 +09:00
Junegunn Choi e4c3ecc57e
0.28.0 2021-11-04 01:05:07 +09:00
Junegunn Choi 673c5d886d
Add 'put' action for putting the character to the prompt
fzf --bind 'space:preview(date)+put'

Close #2456
2021-11-04 00:49:05 +09:00
Junegunn Choi f799b568d1
[bash] Suppress error message from 'bind'
Fix #2618
2021-11-03 23:26:25 +09:00
Junegunn Choi 7bff4661f6
Add --header-first option to display header before prompt line
Close #2422
2021-11-03 21:19:22 +09:00
Junegunn Choi ffd8bef808
Update CHANGELOG 2021-11-02 21:48:19 +09:00
Junegunn Choi 02cee2234d
Implement --scroll-off=LINES
Close #2533
2021-11-02 21:48:19 +09:00
Vlastimil Ovčáčík e0dd2be3fb Document escaping and expanding of quotes on Windows
Parsers included:
- go parser (well, this is easily dealt with using `` strings)
- win32 (shell-api) parser
- powershell parser (for powershell commands)
- powershell parsing rules for calling native commands
- internal parsers of select regex applications (like grep)
2021-11-02 15:56:20 +09:00
Vlastimil Ovčáčík a33c011c21 Test escaping of powershell commands on Windows 2021-11-02 15:56:20 +09:00
Rashil Gandhi 7c3f42bbba Fix powershell escaping 2021-11-02 15:56:20 +09:00
Junegunn Choi edac9820b5
Cache cygpath result
No need to repeatedly run cygpath process because $SHELL never changes.
2021-10-25 18:46:59 +09:00
Rashil Gandhi 84a47f7102
Respect SHELL env var on Windows (#2641)
This makes fzf respect SHELL environment variable on Windows, like it does on *nix, whenever defined.

Close #2638
2021-10-23 01:09:47 +09:00
Junegunn Choi 97ae8afb6f
Reload should update preview window
Fix #2644
2021-10-23 01:06:15 +09:00
Junegunn Choi 4138333f5c
0.27.3 2021-10-15 23:59:56 +09:00
Vlastimil Ovčáčík 61339a8ae2
Add more tests of placeholder flags and simplify its logic (#2624)
* [tests] Test fzf's placeholders and escaping on practical commands

This tests some reasonable commands in fzf's templates (for commands,
previews, rebinds etc.), how are those commands escaped (backslashes,
double quotes), and documents if the output is executable in cmd.exe.
Both on Unix and Windows.

* [tests] Add testing of placeholder parsing and matching

Adds tests and bit of docs for the curly brackets placeholders in fzf's
template strings. Also tests the "placeholder" regex.

* [tests] Add more test cases of replacing placeholders focused on flags

Replacing placeholders in templates is already tested, this adds tests
that focus more on the parameters of placeholders - e.g. flags, token
ranges.

There is at least one test for each flag, not all combinations are
tested though.

* [refactoring] Split OS-specific function quoteEntry() to corresponding source file

This is minor refactoring, and also the function's test was made
crossplatform.

* [refactoring] Simplify replacePlaceholder function

Should be equivalent to the original, but has simpler structure.
2021-10-15 22:31:59 +09:00
Junegunn Choi 50eb2e3855
Render spinner on info line during "reload"
Fix #2637
2021-10-15 22:13:57 +09:00
Xeonacid 5fc78e4584
Add riscv64 build target (#2626)
Build successfully on Arch Linux RISC-V.
2021-10-10 21:33:06 +09:00
Junegunn Choi 2736a2f69e
[vim] Empty out $FZF_DEFAULT_COMMAND before unletting it
For Vim 8.0.1831 and below
* https://github.com/vim/vim/issues/1116

Fix https://github.com/junegunn/fzf.vim/issues/1301
2021-10-10 20:19:48 +09:00
Vlastimil Ovčáčík 179993f0cd
Enable manually trigger on GitHub Workflows (#2620) 2021-10-04 21:41:54 +09:00
dependabot[bot] b734f657f9
Bump github.com/mattn/go-isatty from 0.0.12 to 0.0.14 (#2612)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.12 to 0.0.14.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.12...v0.0.14)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-04 21:13:46 +09:00
dependabot[bot] c29d7d02c2
Bump github.com/mattn/go-shellwords from 1.0.11 to 1.0.12 (#2592)
Bumps [github.com/mattn/go-shellwords](https://github.com/mattn/go-shellwords) from 1.0.11 to 1.0.12.
- [Release notes](https://github.com/mattn/go-shellwords/releases)
- [Commits](https://github.com/mattn/go-shellwords/compare/v1.0.11...v1.0.12)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-shellwords
  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>
2021-10-04 21:12:40 +09:00
dependabot[bot] 3df6b2a58c
Bump github.com/mattn/go-runewidth from 0.0.12 to 0.0.13 (#2591)
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.12 to 0.0.13.
- [Release notes](https://github.com/mattn/go-runewidth/releases)
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.12...v0.0.13)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  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>
2021-10-04 21:01:03 +09:00
Vlastimil Ovčáčík b8aa2d2c32 Minor refactoring tcell library from tui.go to tcell.go
To prevent including tcell library in non-windows builds.
2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík 0ff885461b Add mouse support to the FullscreenRenderer 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík ca43f95fb1 Fix Backspace key to emit BSpace and AltBS events instead of CtrlH
CtrlH events are still sent when appropriate. I have adjusted
FullscreenRenderer to match the LightRenderer's behaviour, which seems
to be correct.
2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík 09700f676b Add CtrlCaret keyboard event to FullscreenRenderer 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík 4271e9cffa Fix Ctrl+Space key combination to emit CtrlSpace instead of Rune ' ' 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík f3dc8a10d5 Add ability to type AltGr characters in FullscreenRenderer on Windows. 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík 00fb486f6a [tests] Add testing of keyboard events in FullscreenRenderer.GetChar()
This contains one test case of each tcell.Key* event type that can be
sent to and subsequently processed in fzf's GetChar(). The test cases
describe status quo, and all of them PASS.

Small function util.ToTty() was added. It is similar to util.IsTty(),
but for stdout (hence the To preposition).
2021-10-03 01:39:30 +09:00
Junegunn Choi 4173e94c6f
Do not check for --height support on --version
https://github.com/junegunn/fzf.vim/issues/1329
2021-09-29 20:17:44 +09:00
Hiroki Konishi 261d3d3340 fix: replace broken links with archived ones 2021-09-28 18:07:22 +09:00
Hiroki Konishi 15e20fcae1 fix: spelling `Refence` -> `Reference` 2021-09-28 18:07:22 +09:00
Vlastimil Ovčáčík f4f47f5fe3 Minor changes
- obsolete todo removed, I tested the ev.ch for " " char and it works just
fine
2021-09-24 16:04:36 +09:00
Vlastimil Ovčáčík 71d11de7ca [tests] Change tests to output to stdout only with verbose flag
This hides stdout output unless "go test -v" was run.
2021-09-24 16:04:36 +09:00
Vlastimil Ovčáčík 88d74a15aa
Change the tests to run on Windows (#2615)
Most of the "expected" strings in terminal.go test were changed to
"text/template" values. Quotes in those string were parametrized in
the templates. Two functions handling templates were added
for convenience.

Templates has the advantage of:
- parametrize repetitive strings inside "expected" values
  - inner and outer quotes were parametrized in templates
  - long and confusing test values are more readable
- templates can be localized for other operating systems
2021-09-24 09:45:06 +09:00
Junegunn Choi 0f02fc0c77
Reset {n} after reload
Fix #2611
2021-09-14 20:36:10 +09:00
Keating950 3f90fb42d8
Fix spelling error (Extention -> Extension) (#2589) 2021-08-17 16:40:24 +09:00
Daniel Bast 9bd8988300
Add dependabot config for dependency updates (#2573) 2021-08-17 16:27:50 +09:00
a1346054 3c804bcfec fix spelling 2021-08-15 16:03:26 +09:00
a1346054 cca4cdc4f1 improve test logic and be explicit about the test 2021-08-15 16:03:26 +09:00
a1346054 8f899aaf8a use proper bash-style notation 2021-08-15 16:03:26 +09:00
a1346054 e53b4bb439 always use [[ ... ]] and not [ ... ] in bash completions 2021-08-15 16:03:26 +09:00
a1346054 ab247a1309 use consistent style for bash [[ ... ]] 2021-08-15 16:03:26 +09:00
Michael Kelley c21e9edad4
Restore VT hack for Windows (#2580)
- restore VT enable hack
- resolve an issue reported in https://github.com/kelleyma49/PSFzf
2021-08-15 16:01:50 +09:00
Leon Tepe 9c21a20f8b
Minor readme change (#2578)
`CTRL-K` moves up and `CTRL-J` moves down, not the other way around (same for `CTRL-P` and `CTRL-N`
2021-08-15 15:57:07 +09:00
Junegunn Choi 7191ebb615
Do not show preview window by default if `--preview` is empty
Close #2516
2021-06-08 08:53:29 +09:00
Junegunn Choi a74731d7f5
[vim] Add 'sinklist' as a synonym to 'sink*'
So that it's easier to add a sinklist function to a spec dictionary.

  let spec = { 'source': source, 'options': ['--preview', preview] }
  function spec.sinklist(matches)
    echom string(a:matches)
  endfunction

  call fzf#run(fzf#wrap(spec))
2021-06-04 22:07:29 +09:00
Junegunn Choi e086f0b3fe
0.27.2 2021-06-01 17:00:24 +09:00
Junegunn Choi 8255aa23f4
Fix bug where `--read0` not properly displaying long lines
Fix #2508
2021-06-01 16:55:51 +09:00
Junegunn Choi a4bc08f5a3
Allow specifying 16 base ANSI colors by their names
Close #2502
2021-05-26 19:35:26 +09:00
Thomas Klausner 7e5aa1e2a5
Mention NetBSD package and how to install it (#2499)
Close #2487
2021-05-23 11:31:55 +09:00
Junegunn Choi 0818dbc36a
0.27.1 2021-05-22 13:19:57 +09:00
Junegunn Choi 347c4b2625
Add 'unbind' action
Fix #2486
2021-05-22 13:16:39 +09:00
Junegunn Choi 34f0d4d0c4
[man] Clarification on --select-1 and --exit-0 2021-05-22 09:47:02 +09:00
Junegunn Choi cbedb57511
[vim] Workaround for Neovim bug of unconditionally evaluating `unlet $ENV_VAR`
See #2495
2021-05-21 13:17:19 +09:00
Junegunn Choi 9ef825d2fd
[vim] Update README-VIM 2021-05-19 18:43:50 +09:00
Junegunn Choi 85ae745910
[vim] Use terminal buffer on 'down' layout on regular Vim on terminal
When 'down' layout was used on regular Vim on terminal, fzf would open
below the editor using `--height` option. This was the only case where
terminal buffer was not used (the code was written when Vim didn't have
builtin terminal) and this exception has been a constant source of
confusion.

This commit makes fzf open in a terminal buffer even in that case.
2021-05-19 18:39:12 +09:00
Junegunn Choi 7411da8d5a
[vim] Use FZF_DEFAULT_COMMAND instead of STDIN pipe
So that fzf can finish immediately even when the input process doesn't
handle SIGPIPE and keeps running.

Fix #2481
2021-05-19 18:37:13 +09:00
Junegunn Choi 3f75a8369f
Replace RuneWidth to StringWidth to handle grapheme clusters
Fix #2482
2021-05-14 11:44:44 +09:00
Junegunn Choi 4cd621e877
ADVANCED.md: tmux 3.2 is officially released 2021-04-29 09:37:17 +09:00
Junegunn Choi 6e3a2fe0bf
[vim] Fix screen offset of relatively positioned popup window
Fix #2461
2021-04-28 10:17:46 +09:00
Tom Picton 8b0e1f941a
[vim] Support relative-to-window positioning of popup (#2443)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-04-17 20:48:10 +09:00
Junegunn Choi c7c5e7670a
Fix goreleaser.yml 2021-04-10 14:19:19 +09:00
Junegunn Choi f6c621ef1b
Update ADVANCED.md
Remove unnecessary --color option
2021-04-09 22:58:31 +09:00
Junegunn Choi faf32d451d
Update ADVANCED.md 2021-04-09 14:14:53 +09:00
Junegunn Choi 252fd7ecb1
Update ADVANCED.md 2021-04-09 13:43:16 +09:00
Junegunn Choi 7fa89dddb4
Update README.md: Examples page 2021-04-08 10:06:26 +09:00
Junegunn Choi fefdb8c84e
Fix typo 2021-04-07 13:28:26 +09:00
Junegunn Choi a6cc05936e
ADVANCED.md: Clarification on {q} 2021-04-07 07:12:39 +09:00
Junegunn Choi b209843545
Advanced fzf examples 2021-04-07 06:46:17 +09:00
Junegunn Choi 19759ed36e
0.27.0 2021-04-06 22:53:59 +09:00
Junegunn Choi 1a7ae8e7b9
Update dependencies
go get: upgraded github.com/lucasb-eyer/go-colorful v1.0.3 => v1.2.0
go get: upgraded github.com/mattn/go-runewidth v0.0.9 => v0.0.12
go get: upgraded github.com/mattn/go-shellwords v1.0.10 => v1.0.11
go get: added github.com/rivo/uniseg v0.2.0
go get: upgraded github.com/saracen/walker v0.1.1 => v0.1.2
go get: upgraded golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 => v0.0.0-20210220032951-036812b2e83c
go get: upgraded golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 => v0.0.0-20210403161142-5e06dd20ab57
go get: upgraded golang.org/x/text v0.3.3 => v0.3.6
2021-04-06 20:32:18 +09:00
Junegunn Choi da1f645670
Change --preview-window delimiter from : to , for consistency
Delimiter : was chosen when --preview-option only supported position and
size attributes. e.g. up:50%
2021-04-06 20:10:55 +09:00
Junegunn Choi 3a2015ee26
Fix minimum preview window height 2021-04-06 20:05:54 +09:00
Junegunn Choi c440418ce6
Sign and notarize macOS binaries
Close #2408
2021-04-06 18:09:06 +09:00
Junegunn Choi 3d37a5ba1d
Apply preview-bg color to preview border of all shapes 2021-04-06 18:01:29 +09:00
Junegunn Choi 15f4cfb6d9
More border optins for preview window
Close #2431
2021-04-06 17:37:11 +09:00
Junegunn Choi be36de2482
Ignore more ANSI escape sequences
Fix #2420
2021-04-06 00:51:39 +09:00
Junegunn Choi 391237f7df
[vim] Compare binary versions
Close #2410
2021-04-06 00:24:20 +09:00
Junegunn Choi 977e5effd9
[vim] Fix paste on MacVim
Close https://github.com/junegunn/fzf.vim/issues/1233
2021-04-05 17:28:18 +09:00
Junegunn Choi 8b36a4cb19
Speed up preview switching when doing partial rendering
Fix #2417
2021-04-04 13:43:16 +09:00
Michael Kelley c8cd94a772
Ensure proper ESC seq handling under Windows preview mode (#2430)
- Increase go routine buffer size
- Add time wait for nonblock getchr()
- Resolve #2429
2021-04-04 13:19:43 +09:00
Junegunn Choi 764316a53d
Fix flaky test case: test_interrupt_execute
Try to avoid extraneous INT signal
2021-03-26 17:40:12 +09:00
Philipp Schmitt 2048fd4042
Update README (--phony -> --disabled) (#2404) 2021-03-25 20:36:01 +09:00
Junegunn Choi f84b3de24b
Automatically set /dev/tty as STDIN on execute action
https://github.com/junegunn/fzf/issues/1360#issuecomment-788178140

  # Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal"
  ls | fzf --bind "enter:execute(vim {} < /dev/tty)"

  # With this change, we can omit "< /dev/tty" part
  ls | fzf --bind "enter:execute(vim {})"
2021-03-25 20:00:09 +09:00
Junegunn Choi 6a1f3ec08b
[install] Download Darwin arm64 binary (#2400 #2401) 2021-03-25 10:56:21 +09:00
Mitsuo Heijo 2e353aee96
Replace golang.org/x/crypto/ssh/terminal with golang.org/x/term (#2395)
See https://github.com/golang/go/issues/31044
2021-03-20 14:38:34 +09:00
Mitsuo Heijo 8edfd14a37
Test against Golang 1.14 and 1.16 (#2396)
1.14 for 32-bit binaries
2021-03-20 12:32:44 +09:00
Junegunn Choi 1a191ec6f7
Update FUNDING.yml 2021-03-14 12:03:11 +09:00
Junegunn Choi e7171e94b4
Update FUNDING.yml 2021-03-14 12:01:57 +09:00
Junegunn Choi 398d937419
Create FUNDING.yml 2021-03-14 11:59:56 +09:00
Junegunn Choi 34fe5ab143
0.26.0 2021-03-13 15:13:31 +09:00
Junegunn Choi 1b08f43f82
Advanced preview scroll offset expression to better support fixed header 2021-03-13 02:26:41 +09:00
Junegunn Choi b24a2e2fdc
Fix regression in preview window rendering 2021-03-12 21:23:16 +09:00
Junegunn Choi 4c4c6e626e
Add support for preview window header
Fix #2373

  # Display top 3 lines as the fixed header
  fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
2021-03-12 20:32:27 +09:00
Junegunn Choi 7310370a31
Fix truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 20:31:27 +09:00
Junegunn Choi 8ae94f0059
Fix premature truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 11:05:51 +09:00
Junegunn Choi 8fccf20892
Fix incorrect tab character handling
Fix #2372
2021-03-12 10:08:18 +09:00
Charlie Vieth 5a874ae241
Speed up ANSI code processing (#2368)
This commit speeds up the parsing/processing of ANSI escape codes by
roughly 7.5x. The speedup is mostly accomplished by replacing the regex
with dedicated parsing logic (nextAnsiEscapeSequence()) and reducing the
number of allocations in extractColor().

#### Benchmarks
```
name             old time/op    new time/op     delta
ExtractColor-16    4.89µs ± 5%     0.64µs ± 2%   -86.87%  (p=0.000 n=9+9)

name             old speed      new speed       delta
ExtractColor-16  25.6MB/s ± 5%  194.6MB/s ± 2%  +661.43%  (p=0.000 n=9+9)

name             old alloc/op   new alloc/op    delta
ExtractColor-16    1.37kB ± 0%     0.31kB ± 0%   -77.31%  (p=0.000 n=10+10)

name             old allocs/op  new allocs/op   delta
ExtractColor-16      48.0 ± 0%        4.0 ± 0%   -91.67%  (p=0.000 n=10+10)
```
2021-03-11 19:34:50 +09:00
Jannik Vieten f4e1ed25f2
[fish] Make widgets work with --option= prefix (#2383)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-03-08 22:59:56 +09:00
Junegunn Choi cbfbb49ab4
[vim] Vim 8.0 compatibility
Fix #2367
2021-03-08 12:56:06 +09:00
solarizedalias 489b16efce
[fzf-tmux] Adapt to tmux latest changes (#2379) 2021-03-08 12:44:36 +09:00
Junegunn Choi b82c1693c0
Fix deadlocks 2021-03-08 00:08:10 +09:00
Junegunn Choi 019bfc4e35
Fix yet another deadlock
EventBox.Set should not be called while holding the terminal mutex

  goroutine 1 [semacquire]:
  sync.runtime_SemacquireMutex(0xc0001923bc, 0x1000001066200, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc0001923b8)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(...)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81
  github.com/junegunn/fzf/src.(*Terminal).Input(0xc000192000, 0x0, 0x0, 0x0, 0x0)
          /fzf/src/terminal.go:581 +0x145
  github.com/junegunn/fzf/src.Run.func10(0xc00010c8a0, 0xc000092050, 0xa)
          /fzf/src/core.go:245 +0x37
  github.com/junegunn/fzf/src.Run.func11(0xc00011a4e0)
          /fzf/src/core.go:295 +0x5ce
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a4e0, 0xc000127ec8)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.Run(0xc000180000, 0x11ac014, 0x6, 0x11ac158, 0x7)
          /fzf/src/core.go:251 +0xdac
  main.main()
          /fzf/main.go:13 +0x5a

  goroutine 11 [semacquire]:
  sync.runtime_SemacquireMutex(0xc00012c31c, 0xc00010e800, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81 +0x47
  github.com/junegunn/fzf/src/util.(*EventBox).Set(0xc00011a4e0, 0x7, 0x114eb40, 0x1265460)
          /fzf/src/util/eventbox.go:40 +0x3b
  github.com/junegunn/fzf/src.(*Terminal).killPreview(0xc000192000, 0x0)
          /fzf/src/terminal.go:1831 +0xa5
  github.com/junegunn/fzf/src.(*Terminal).exit(0xc000192000, 0xc000106e58)
          /fzf/src/terminal.go:1847 +0x75
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8.1(0xc00011a540)
          /fzf/src/terminal.go:2148 +0x38f
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a540, 0xc000106f90)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8(0xc000192000, 0xc00010a2c0)
          /fzf/src/terminal.go:2077 +0xa5
  created by github.com/junegunn/fzf/src.(*Terminal).Loop
          /fzf/src/terminal.go:2072 +0x3e8
2021-03-07 23:35:19 +09:00
Junegunn Choi dfda5c054a
[actions] Install fish using apt-get
For some reason, `test_ctrl_r` and `test_ctrl_r_abort` are not passing
on GitHub Action runner with Fish 3.2.0.
2021-03-07 22:41:27 +09:00
Junegunn Choi f657169616
Fix deadlock on exit 2021-03-07 21:44:08 +09:00
Junegunn Choi 4c06da8b70
Fix GitHub Action build
$USER is missing
2021-03-07 18:05:39 +09:00
yoshida.shinya 9fe2393a00
Add test cases for killing input command on terminate (#2381 #2382) 2021-03-07 11:36:00 +09:00
Junegunn Choi e2e8d94b14
Kill input command on terminate
Fix #2381
Close #2382
2021-03-07 11:30:26 +09:00
bitterfox 4f9a7f8c87
Don't exit fzf by SIGINT while executing command (#2375)
Fix #2374

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-02-28 21:01:03 +09:00
Junegunn Choi bb0502ff44
Check gofmt in `make test` 2021-02-28 18:28:21 +09:00
Junegunn Choi c256442245
Fix typo 2021-02-28 18:27:21 +09:00
Jonathan Müller 1137404190
[vim] Add keepjump to switch_back() function (#2363)
Otherwise, the jump list will contain a (hidden) entry for the FZF buffer if `window: enew` is used.
2021-02-25 21:58:13 +09:00
Junegunn Choi d57c6d0284
Update build script to build macOS arm64 binary
Close #2361
2021-02-25 21:31:15 +09:00
Junegunn Choi 76bbf57b3d
Add select and deselect actions
Close #2358
2021-02-25 21:23:05 +09:00
Hiroki Konishi 806a47a7cc
[vim] Remove unnecessary `border` management in nvim floating window (#2370) 2021-02-25 14:41:23 +09:00
Junegunn Choi 29851c18aa
[vim] Force redraw by exiting and re-entering terminal mode
Workaround for Neovim v0.5.0-dev

https://github.com/junegunn/fzf/issues/2352#issuecomment-782894123
2021-02-22 21:46:28 +09:00
Junegunn Choi dea950c2c8
[vim] Call feedkeys only when the destination buffer is a terminal
Fix #2352
Fix https://github.com/junegunn/fzf.vim/issues/1216

Close #2364
2021-02-22 00:22:12 +09:00
Junegunn Choi a367dfb22e
README.md: Better example 2021-02-17 16:44:54 +09:00
odeson24 9fe1a7b373
Remove redundant assignment (#2356)
Co-authored-by: Ryan Ou <ryanou@aetherai.com>
2021-02-17 10:28:43 +09:00
Hussein Esmail 8e2d21c548
Update README.md (#2353)
Remove Linuxbrew links since Linuxbrew has been merged into Homebrew

* https://brew.sh/2019/02/02/homebrew-2.0.0/
2021-02-17 10:24:35 +09:00
Junegunn Choi bedf1cd357
[vim] Use tnoremap only when it's available
Fix #2357
2021-02-17 10:04:38 +09:00
Junegunn Choi 13f180a70c
[vim] Stay in terminal mode if fzf#run is called from sink
Fix #2352
2021-02-15 13:58:49 +09:00
Junegunn Choi 6654239c94
0.25.1 2021-02-03 22:32:52 +09:00
Junegunn Choi 1b61e5e9e9
Clarification on FZF_DEFAULT_COMMAND 2021-02-03 19:40:05 +09:00
Marlon Richert 43b3b907f8
[zsh] Don't run precmd hooks in cd widget (#2340)
`precmd` hooks expect the Zsh Line Editor to not be active.
Running these when the ZLE is active can lead to unpredictable results.
See https://github.com/marlonrichert/zsh-autocomplete/issues/180
2021-02-03 19:26:17 +09:00
Junegunn Choi fcd896508b
[vim] fzf#run should ignore empty 'dir' argument
Fix #2343
2021-02-03 13:51:56 +09:00
Junegunn Choi f55c990e86
Add `close` action
Close #2331
2021-02-02 00:11:05 +09:00
Naveen d110372f99
Create codeql-analysis.yml (#2338) 2021-02-01 23:54:45 +09:00
Junegunn Choi c862af09f2
Fix toggle-preview-wrap action
Fix #2336
2021-02-01 23:14:21 +09:00
Junegunn Choi 1cfeec0ca3
Fix segmentation fault on \x1b[0K
Fix #2339
2021-02-01 22:59:11 +09:00
step a0649edc1e
[man] Clarify that $SHELL is used to run commands (#2334)
SHELL is used for execute actions and the preview and default commands.
2021-02-01 20:07:42 +09:00
Kovarththanan Rajaratnam 0e0bcb3e10
Update README.md (#2337)
--phony was renamed to --disabled in d779ff7e6d
2021-02-01 20:02:20 +09:00
Nanda Lopes 686528d627
BUILD.md: Update Go version requirement (#2332)
src/options.go:463:9: undefined: strings.ReplaceAll
    note: module requires Go 1.13make: *** [Makefile:122: target/fzf-linux_amd64] Erro 2
2021-01-28 11:43:15 +09:00
jiangjianshan 3afa920151
[install.ps1] Change permission of the downloaded binary (#2308)
Fix #2256
2021-01-28 11:41:23 +09:00
Junegunn Choi 32c493e994
[Makefile] Restore 32-bit targets
Close #2328
2021-01-20 18:40:17 +09:00
Olivier Roques 1a76bdf891
[vim] Exit terminal mode before closing FZF window (#2326)
Fix https://github.com/junegunn/fzf.vim/issues/1216
2021-01-17 22:39:40 +09:00
Junegunn Choi af48b3df29
Replace Travis CI badge 2021-01-15 10:21:36 +09:00
freddii 58ac1fb2fa
Fix typos in source code (#2322) 2021-01-15 10:10:09 +09:00
Junegunn Choi e922704f72
Migrate to GitHub Actions 2021-01-13 19:10:24 +09:00
Vlad Doster c6115735c7
Update README.md (#2321)
- Correct spelling/grammar
2021-01-13 04:26:10 +09:00
Ruslan Sayfutdinov 9ddf5c72be
[zsh] Properly reset prompt after completion (#2318) 2021-01-13 04:09:34 +09:00
Junegunn Choi cc5640326b
[man] Fix typo 2021-01-10 04:46:31 +09:00
calvin ardi bf447d7703
Update default number version (#2307) 2021-01-08 13:35:46 +09:00
nicolasbarra cbb008c938
Update README to upgrade using brew upgrade (#2309) 2021-01-07 16:50:08 +09:00
E.L.K eaa0c52b45
Fix selection changed on terminal resize (#2306) 2021-01-04 04:20:31 +09:00
Elliott Sales de Andrade 82791f7efc Use more explicit int-to-string conversion.
This fixes the following errors with Go 1.15:
```
src/options.go:452:69: conversion from untyped int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)
src/options.go:463:33: conversion from untyped int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)
```
2021-01-03 14:38:09 +09:00
100 changed files with 14936 additions and 3632 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: junegunn

View File

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

View File

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

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

44
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,44 @@
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning
name: CodeQL
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
jobs:
analyze:
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['go']
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

14
.github/workflows/depsreview.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

51
.github/workflows/linux.yml vendored Normal file
View File

@ -0,0 +1,51 @@
---
name: Test fzf on Linux
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.20"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.0
- name: Install packages
run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
- name: Integration test (tcell)
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose

45
.github/workflows/macos.yml vendored Normal file
View File

@ -0,0 +1,45 @@
---
name: Test fzf on macOS
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.20"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0.0
- name: Install packages
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
- name: Install Ruby gems
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose

24
.github/workflows/sponsors.yml vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 0
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_TOKEN }}
file: 'README.md'
- name: Deploy to GitHub Pages 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
folder: '.'

10
.github/workflows/typos.yml vendored Normal file
View File

@ -0,0 +1,10 @@
name: "Spell Check"
on: [pull_request]
jobs:
typos:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@v1.21.0

15
.github/workflows/winget.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Publish to Winget
on:
release:
types: [released]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: junegunn.fzf
version: ${{ github.event.release.tag_name }}
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@ -1,11 +0,0 @@
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Apple Development: junegunn.c@gmail.com"
}

View File

@ -12,12 +12,54 @@ builds:
- darwin - darwin
goarch: goarch:
- amd64 - amd64
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
post: gon .gon.hcl post: |
sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
}
EOF
gon /tmp/fzf-gon-amd64.hcl
'
- goos: - id: fzf-macos-arm
binary: fzf
goos:
- darwin
goarch:
- arm64
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
post: |
sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
}
EOF
gon /tmp/fzf-gon-arm64.hcl
'
- id: fzf
goos:
- linux - linux
- windows - windows
- freebsd - freebsd
@ -26,10 +68,15 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- loong64
- ppc64le
- s390x
goarm: goarm:
- 5 - 5
- 6 - 6
- 7 - 7
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
ignore: ignore:
@ -44,6 +91,8 @@ builds:
archives: archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
builds:
- fzf
format: tar.gz format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
@ -51,12 +100,18 @@ archives:
files: files:
- non-existent* - non-existent*
checksum:
extra_files:
- glob: ./dist/fzf-*darwin*.zip
release: release:
github: github:
owner: junegunn owner: junegunn
name: fzf name: fzf
prerelease: auto prerelease: auto
name_template: '{{ .Tag }}' name_template: '{{ .Tag }}'
extra_files:
- glob: ./dist/fzf-*darwin*.zip
snapshot: snapshot:
name_template: "{{ .Tag }}-devel" name_template: "{{ .Tag }}-devel"

View File

@ -26,3 +26,7 @@ Style/OptionalBooleanParameter:
Enabled: false Enabled: false
Style/WordArray: Style/WordArray:
MinSize: 1 MinSize: 1
Minitest/AssertEqual:
Enabled: false
Naming/VariableNumber:
Enabled: false

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
golang 1.20.13

View File

@ -1,28 +0,0 @@
language: go
go:
- "1.14"
env: GO111MODULE=on
os:
- linux
- osx
dist: bionic
osx_image: xcode12.2
addons:
apt:
packages:
- fish
- zsh
sources:
sourceline: ppa:fish-shell/release-3
homebrew:
packages:
- fish
- tmux
install: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
script:
- make test
# LC_ALL=C to avoid escape codes in
# printf %q $'\355\205\214\354\212\244\355\212\270' on macOS. Bash on
# macOS is built without HANDLE_MULTIBYTE?
- make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
- rubocop --require rubocop-minitest --require rubocop-performance

680
ADVANCED.md Normal file
View File

@ -0,0 +1,680 @@
Advanced fzf examples
======================
* *Last update: 2024/01/20*
* *Requires fzf 0.46.0 or above*
---
<!-- vim-markdown-toc GFM -->
* [Introduction](#introduction)
* [Screen Layout](#screen-layout)
* [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux)
* [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources)
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
* [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
* [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status)
* [Branches](#branches)
* [Commit hashes](#commit-hashes)
* [Color themes](#color-themes)
* [fzf Theme Playground](#fzf-theme-playground)
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
<!-- vim-markdown-toc -->
Introduction
------------
fzf is an interactive [Unix filter][filter] program that is designed to be
used with other Unix tools. It reads a list of items from the standard input,
allows you to select a subset of the items, and prints the selected ones to
the standard output. You can think of it as an interactive version of *grep*,
and it's already useful even if you don't know any of its options.
```sh
# 1. ps: Feed the list of processes to fzf
# 2. fzf: Interactively select a process using fuzzy matching algorithm
# 3. awk: Take the PID from the selected line
# 3. kill: Kill the process with the PID
ps -ef | fzf | awk '{print $2}' | xargs kill -9
```
[filter]: https://en.wikipedia.org/wiki/Filter_(software)
While the above example succinctly summarizes the fundamental concept of fzf,
you can build much more sophisticated interactive workflows using fzf once you
learn its wide variety of features.
- To see the full list of options and features, see `man fzf`
- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md)
This document will guide you through some examples that will familiarize you
with the advanced features of fzf.
Screen Layout
-------------
### `--height`
fzf by default opens in fullscreen mode, but it's not always desirable.
Oftentimes, you want to see the current context of the terminal while using
fzf. `--height` is an option for opening fzf below the cursor in
non-fullscreen mode so you can still see the previous commands and their
results above it.
```sh
fzf --height=40%
```
![image](https://user-images.githubusercontent.com/700826/113379893-c184c680-93b5-11eb-9676-c7c0a2f01748.png)
You might also want to experiment with other layout options such as
`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc.
```sh
fzf --height=40% --layout=reverse
fzf --height=40% --layout=reverse --info=inline
fzf --height=40% --layout=reverse --info=inline --border
fzf --height=40% --layout=reverse --info=inline --border --margin=1
fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
```
![image](https://user-images.githubusercontent.com/700826/113379932-dfeac200-93b5-11eb-9e28-df1a2ee71f90.png)
*(See `Layout` section of the man page to see the full list of options)*
But you definitely don't want to repeat `--height=40% --layout=reverse
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
could write a wrapper script or shell alias, but there is an easier option.
Define `$FZF_DEFAULT_OPTS` like so:
```sh
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
```
### `fzf-tmux`
Before fzf had `--height` option, we would open fzf in a tmux split pane not
to take up the whole screen. This is done using `fzf-tmux` script.
```sh
# Open fzf on a tmux split pane below the current pane.
# Takes the same set of options.
fzf-tmux --layout=reverse
```
![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png)
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
`--height` option. But the advantage of it is that it's more flexible.
(See `man fzf-tmux` for available options.)
```sh
# On the right (50%)
fzf-tmux -r
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png)
![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png)
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
```sh
# Open tmux in a tmux popup window (default size: 50% of the screen)
fzf-tmux -p
# 80% width, 60% height
fzf-tmux -p 80%,60%
```
![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png)
> You might also want to check out my tmux plugins which support this popup
> window layout.
>
> - https://github.com/junegunn/tmux-fzf-url
> - https://github.com/junegunn/tmux-fzf-maccy
Dynamic reloading of the list
-----------------------------
fzf can dynamically update the candidate list using an arbitrary program with
`reload` bindings (The design document for `reload` can be found
[here][reload]).
[reload]: https://github.com/junegunn/fzf/issues/1750
### Updating the list of processes by pressing CTRL-R
This example shows how you can set up a binding for dynamically updating the
list without restarting fzf.
```sh
(date; ps -ef) |
fzf --bind='ctrl-r:reload(date; ps -ef)' \
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
--preview='echo {}' --preview-window=down,3,wrap \
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
```
![image](https://user-images.githubusercontent.com/700826/113465047-200c7c00-946c-11eb-918c-268f37a900c8.png)
- The initial command is `(date; ps -ef)`. It prints the current date and
time, and the list of the processes.
- With `--header` option, you can show any message as the fixed header.
- To disallow selecting the first two lines (`date` and `ps` header), we use
`--header-lines=2` option.
- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that
runs `date; ps -ef`, so we can update the list of the processes by pressing
CTRL-R.
- We use simple `echo {}` preview option, so we can see the entire line on the
preview window below even if it's too long
### Toggling between data sources
You're not limited to just one reload binding. Set up multiple bindings so
you can switch between data sources.
```sh
find * | fzf --prompt 'All> ' \
--header 'CTRL-D: Directories / CTRL-F: Files' \
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
```
![image](https://user-images.githubusercontent.com/700826/113465073-4af6d000-946c-11eb-858f-2372c0955f67.png)
![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png)
### Toggling with a single key binding
The above example uses two different key bindings to toggle between two modes,
but can we just use a single key binding?
To make a key binding behave differently each time it is pressed, we need:
1. a way to store the current state. i.e. "which mode are we in?"
2. and a way to dynamically perform different actions depending on the state.
The following example shows how to 1. store the current mode in the prompt
string, 2. and use this information (`$FZF_PROMPT`) to determine which
actions to perform using the `transform` action.
```sh
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
```
Ripgrep integration
-------------------
### Using fzf as the secondary filter
* Requires [bat][bat]
* Requires [Ripgrep][rg]
[bat]: https://github.com/sharkdp/bat
[rg]: https://github.com/BurntSushi/ripgrep
fzf is pretty fast for filtering a list that you will rarely have to think
about its performance. But it is not the right tool for searching for text
inside many large files, and in that case you should definitely use something
like [Ripgrep][rg].
In the next example, Ripgrep is the primary filter that searches for the given
text in files, and fzf is used as the secondary fuzzy filter that adds
interactivity to the workflow. And we use [bat][bat] to show the matching line in
the preview window.
This is a bash script and it will not run as expected on other non-compliant
shells. To avoid the compatibility issue, let's save this snippet as a script
file called `rfv`.
```bash
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
And run it with an initial query string.
```sh
# Make the script executable
chmod +x rfv
# Run it with the initial query "algo"
./rfv algo
```
> Ripgrep will perform the initial search and list all the lines that contain
`algo`. Then we further narrow down the list on fzf.
![image](https://user-images.githubusercontent.com/700826/113683873-a42a6200-96ff-11eb-9666-26ce4091b0e4.png)
I know it's a lot to digest, let's try to break down the code.
- Ripgrep prints the matching lines in the following format
```
man/man1/fzf.1:54:.BI "--algo=" TYPE
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
```
The first token delimited by `:` is the file path, and the second token is
the line number of the matching line. They respectively correspond to `{1}`
and `{2}` in the preview command.
- `--preview 'bat --color=always {1} --highlight-line {2}'`
- As we run `rg` with `--color=always` option, we should tell fzf to parse
ANSI color codes in the input by setting `--ansi`.
- We customize how fzf colors various text elements using `--color` option.
`-1` tells fzf to keep the original color from the input. See `man fzf` for
available color options.
- The value of `--preview-window` option consists of 5 components delimited
by `,`
1. `up` — Position of the preview window
1. `60%` — Size of the preview window
1. `border-bottom` — Preview window border only on the bottom side
1. `+{2}+3/3` — Scroll offset of the preview contents
1. `~3` — Fixed header
- Let's break down the latter two. We want to display the bat output in the
preview window with a certain scroll offset so that the matching line is
positioned near the center of the preview window.
- `+{2}` — The base offset is extracted from the second token
- `+3` — We add 3 lines to the base offset to compensate for the header
part of `bat` output
- ```
───────┬──────────────────────────────────────────────────────────
│ File: CHANGELOG.md
───────┼──────────────────────────────────────────────────────────
1 │ CHANGELOG
2 │ =========
3 │
4 │ 0.26.0
5 │ ------
```
- `/3` adjusts the offset so that the matching line is shown at a third
position in the window
- `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset
- Instead of using shell script to process the final output of fzf, we use
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
into a new process that opens the file with `vim` (`vim {1}`) and move the
cursor to the line (`+{2}`).
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
### Using fzf as interactive Ripgrep launcher
We have learned that we can bind `reload` action to a key (e.g.
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
`reload` action to `change` event** so that whenever the user *changes* the
query string on fzf, `reload` action is triggered.
Here is a variation of the above `rfv` script. fzf will restart Ripgrep every
time the user updates the query string on fzf. Searching and filtering is
completely done by Ripgrep, and fzf merely provides the interactive interface.
So we lose the "fuzziness", but the performance will be better on larger
projects, and it will free up memory as you narrow down the results.
```bash
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
an empty input (`: | fzf`), then we make it start the initial Ripgrep
process immediately via `start:reload` binding. This way, fzf owns the
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
the process will keep running in the background.
- Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt.
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
reduce the number of intermediate Ripgrep processes while we're typing in
a query.
### Switching to fzf-only search mode
In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
```sh
#!/usr/bin/env bash
# Two-phase filtering with Ripgrep and fzf
#
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# * Press alt-enter to switch to fzf-only filtering
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
* Phase 1. Filtering with Ripgrep
![image](https://user-images.githubusercontent.com/700826/119213880-735e8a80-bafd-11eb-8493-123e4be24fbc.png)
* Phase 2. Filtering with fzf
![image](https://user-images.githubusercontent.com/700826/119213887-7e191f80-bafd-11eb-98c9-71a1af9d7aab.png)
- We added `--prompt` option to show that fzf is initially running in "Ripgrep
launcher mode".
- We added `alt-enter` binding that
1. unbinds `change` event, so Ripgrep is no longer restarted on key press
2. changes the prompt to `2. fzf>`
3. enables search functionality of fzf
4. clears the current query string that was used to start Ripgrep process
5. and unbinds `alt-enter` itself as this is a one-off event
- We reverted `--color` option for customizing how the matching chunks are
displayed in the second phase
### Switching between Ripgrep mode and fzf mode
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
that were previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
CTRL-F.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
- To restore the query string when switching between modes, we store the
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
- Also note that we unbind `ctrl-r` binding on `start` event which is
triggered once when fzf starts.
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
### Switching between Ripgrep mode and fzf mode using a single key binding
In contrast to the previous version, we use just one hotkey to toggle between
ripgrep and fzf mode. This is achieved by using the `$FZF_PROMPT` as a state
within the `transform` action, a feature introduced in [fzf 0.45.0][0.45.0]. A
more detailed explanation of this feature can be found in a previous section -
[Toggling with a single keybinding](#toggling-with-a-single-key-binding).
[0.45.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0450
When using the `transform` action, the placeholder (`\{q}`) should be escaped to
prevent immediate evaluation.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header 'CTRL-T: Switch between ripgrep/fzf' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
Log tailing
-----------
fzf can run long-running preview commands and render partial results before
completion. And when you specify `follow` flag in `--preview-window` option,
fzf will "`tail -f`" the result, automatically scrolling to the bottom.
```bash
# With "follow", preview window will automatically scroll to the bottom.
# "\033[2J" is an ANSI escape sequence for clearing the screen.
# When fzf reads this code it clears the previous preview contents.
fzf --preview-window follow --preview 'for i in $(seq 100000); do
echo "$i"
sleep 0.01
(( i % 300 == 0 )) && printf "\033[2J"
done'
```
![image](https://user-images.githubusercontent.com/700826/113473303-dd669600-94a3-11eb-88a9-1f61b996bb0e.png)
Admittedly, that was a silly example. Here's a practical one for browsing
Kubernetes pods.
```bash
pods() {
: | command='kubectl get pods --all-namespaces' fzf \
--info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}
```
![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png)
- The preview window will *"log tail"* the pod
- Holding on to a large amount of log will consume a lot of memory. So we
limited the initial log amount with `--tail=10000`.
- `execute` bindings allow you to run any command without leaving fzf
- Press enter key on a pod to `kubectl exec` into it
- Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to rotate through a different sets of preview
window options
1. `80%,border-bottom`
1. `hidden`
1. Empty string after `|` translates to the default options from `--preview-window`
Key bindings for git objects
----------------------------
Oftentimes, you want to put the identifiers of various Git object to the
command-line. For example, it is common to write commands like these:
```sh
git checkout [SOME_COMMIT_HASH or BRANCH or TAG]
git diff [SOME_COMMIT_HASH or BRANCH or TAG] [SOME_COMMIT_HASH or BRANCH or TAG]
```
[fzf-git.sh](https://github.com/junegunn/fzf-git.sh) project defines a set of
fzf-based key bindings for Git objects. I strongly recommend that you check
them out because they are seriously useful.
### Files listed in `git status`
<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
![image](https://user-images.githubusercontent.com/700826/113473779-a9d93b00-94a6-11eb-87b5-f62a8d0a0efc.png)
### Branches
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
![image](https://user-images.githubusercontent.com/700826/113473758-87dfb880-94a6-11eb-82f4-9218103f10bd.png)
### Commit hashes
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
![image](https://user-images.githubusercontent.com/700826/113473765-91692080-94a6-11eb-8d38-ed4d41f27ac1.png)
Color themes
------------
You can customize how fzf colors the text elements with `--color` option. Here
are a few color themes. Note that you need a terminal emulator that can
display 24-bit colors.
```sh
# junegunn/seoul256.vim (dark)
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
```
![seoul256](https://user-images.githubusercontent.com/700826/113475011-2c192d80-94ae-11eb-9d17-1e5867bae01f.png)
```sh
# junegunn/seoul256.vim (light)
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
```
![seoul256-light](https://user-images.githubusercontent.com/700826/113475022-389d8600-94ae-11eb-905f-0939dd535837.png)
```sh
# morhetz/gruvbox
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
```
![gruvbox](https://user-images.githubusercontent.com/700826/113475042-494dfc00-94ae-11eb-9322-cd03a027305a.png)
```sh
# arcticicestudio/nord-vim
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
```
![nord](https://user-images.githubusercontent.com/700826/113475063-67b3f780-94ae-11eb-9b24-5f0d22b63399.png)
```sh
# tomasr/molokai
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
```
![molokai](https://user-images.githubusercontent.com/700826/113475085-8619f300-94ae-11eb-85e4-2766fc3246bf.png)
### fzf Theme Playground
[fzf Theme Playground](https://vitormv.github.io/fzf-themes/) created by
[Vitor Mello](https://github.com/vitormv) is a webpage where you can
interactively create fzf themes.
### Generating fzf color theme from Vim color schemes
The Vim plugin of fzf can generate `--color` option from the current color
scheme according to `g:fzf_colors` variable. You can find the detailed
explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors).
Here is an example. Add this to your Vim configuration file.
```vim
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'preview-bg': ['bg', 'NormalFloat'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
```
Then you can see how the `--color` option is generated by printing the result
of `fzf#wrap()`.
```vim
:echo fzf#wrap()
```
Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of
the current file.
```vim
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))
```

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
FROM archlinux/base:latest FROM ubuntu:24.04
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc 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 '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile RUN echo '. ~/.bashrc' >> ~/.bash_profile
@ -8,4 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
RUN rm -f /etc/bash.bashrc RUN rm -f /etc/bash.bashrc
COPY . /fzf COPY . /fzf
RUN cd /fzf && make install && ./install --all RUN cd /fzf && make install && ./install --all
ENV LANG C.UTF-8
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ] CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,10 +1,10 @@
SHELL := bash SHELL := bash
GO ?= go GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)
@ -20,19 +20,23 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION ifdef FZF_REVISION
REVISION := $(FZF_REVISION) REVISION := $(FZF_REVISION)
else else
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null) REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
endif endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION) $(error Not on git repository; cannot determine $$FZF_REVISION)
endif 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 BINARY64 := fzf-$(GOOS)_amd64
BINARYS390 := fzf-$(GOOS)_s390x
BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8 BINARYARM8 := fzf-$(GOOS)_arm8
BINARYPPC64LE := fzf-$(GOOS)_ppc64le BINARYPPC64LE := fzf-$(GOOS)_ppc64le
BINARYRISCV64 := fzf-$(GOOS)_riscv64
BINARYLOONG64 := fzf-$(GOOS)_loong64
# https://en.wikipedia.org/wiki/Uname # https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m) UNAME_M := $(shell uname -m)
@ -40,6 +44,12 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),armv5l) else ifeq ($(UNAME_M),armv5l)
BINARY := $(BINARYARM5) BINARY := $(BINARYARM5)
else ifeq ($(UNAME_M),armv6l) else ifeq ($(UNAME_M),armv6l)
@ -47,13 +57,19 @@ else ifeq ($(UNAME_M),armv6l)
else ifeq ($(UNAME_M),armv7l) else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7) BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),armv8l) else ifeq ($(UNAME_M),armv8l)
BINARY := $(BINARYARM8) # armv8l is always 32-bit and should implement the armv7 ISA, so
# just use the same filename as for armv7.
BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),arm64) else ifeq ($(UNAME_M),arm64)
BINARY := $(BINARYARM8) BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),aarch64) else ifeq ($(UNAME_M),aarch64)
BINARY := $(BINARYARM8) BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),ppc64le) else ifeq ($(UNAME_M),ppc64le)
BINARY := $(BINARYPPC64LE) BINARY := $(BINARYPPC64LE)
else ifeq ($(UNAME_M),riscv64)
BINARY := $(BINARYRISCV64)
else ifeq ($(UNAME_M),loongarch64)
BINARY := $(BINARYLOONG64)
else else
$(error Build on $(UNAME_M) is not supported, yet.) $(error Build on $(UNAME_M) is not supported, yet.)
endif endif
@ -61,18 +77,29 @@ endif
all: target/$(BINARY) all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util github.com/junegunn/fzf/src/util
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
install: bin/fzf install: bin/fzf
generate:
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
build: build:
goreleaser --rm-dist --snapshot goreleaser build --clean --snapshot --skip=post-hooks
release: release:
# Make sure that the tests pass and the build works
TAGS=tcell make test
make test build clean
ifndef GITHUB_TOKEN ifndef GITHUB_TOKEN
$(error GITHUB_TOKEN is not defined) $(error GITHUB_TOKEN is not defined)
endif endif
@ -90,6 +117,7 @@ endif
grep -qF $(VERSION) install.ps1 grep -qF $(VERSION) install.ps1
# Make release note out of CHANGELOG.md # Make release note out of CHANGELOG.md
mkdir -p tmp
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \ sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
@ -98,7 +126,7 @@ endif
git push origin temp --follow-tags --force git push origin temp --follow-tags --force
# Make a GitHub release # Make a GitHub release
goreleaser --rm-dist --release-notes tmp/release-note goreleaser --clean --release-notes tmp/release-note
# Push to master # Push to master
git checkout master git checkout master
@ -110,9 +138,14 @@ endif
clean: clean:
$(RM) -r dist target $(RM) -r dist target
target/$(BINARY32): $(SOURCES)
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES) target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYS390): $(SOURCES)
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm # https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
@ -129,19 +162,26 @@ target/$(BINARYARM8): $(SOURCES)
target/$(BINARYPPC64LE): $(SOURCES) target/$(BINARYPPC64LE): $(SOURCES)
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYRISCV64): $(SOURCES)
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYLOONG64): $(SOURCES)
GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin bin/fzf: target/$(BINARY) | bin
-rm -f bin/fzf
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf
docker: docker:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch tmux docker run -it fzf-ubuntu tmux
docker-test: docker-test:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch docker run -it fzf-ubuntu
update: update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all build release test install clean docker docker-test update .PHONY: all generate build release test bench install clean docker docker-test update

View File

@ -12,7 +12,10 @@ differ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using git " If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
@ -23,7 +26,10 @@ written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If installed using Homebrew on Apple Silicon
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
``` ```
@ -115,7 +121,7 @@ let g:fzf_action = {
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }')) call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
copen copen
cc cc
endfunction endfunction
@ -127,9 +133,15 @@ let g:fzf_action = {
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
" - Popup window " - Popup window (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" - Popup window (center of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
@ -226,19 +238,20 @@ call fzf#run({'sink': 'e'})
``` ```
We haven't specified the `source`, so this is equivalent to starting fzf on We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or command line without standard input pipe; fzf will traverse the file system
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current under the current directory to get the list of files. (If
directory. When you select one, it will open it with the sink, `:e` command. `$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
If you want to open it in a new tab, you can pass `:tabedit` command instead instead.) When you select one, it will open it with the sink, `:e` command. If
as the sink. you want to open it in a new tab, you can pass `:tabedit` command instead as
the sink.
```vim ```vim
call fzf#run({'sink': 'tabedit'}) call fzf#run({'sink': 'tabedit'})
``` ```
Instead of using the default find command, you can use any shell command as You can use any shell command as the source to generate the list. The
the source. The following example will list the files managed by git. It's following example will list the files managed by git. It's equivalent to
equivalent to running `git ls-files | fzf` on shell. running `git ls-files | fzf` on shell.
```vim ```vim
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})
@ -277,7 +290,7 @@ The following table summarizes the available options.
| `source` | list | Vim list as input to fzf | | `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | | `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item | | `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | | `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
@ -302,7 +315,8 @@ following options are allowed:
- Optional: - Optional:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `border` [string default `rounded`]: Border style - `relative` [boolean default v:false]
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
`fzf#wrap` `fzf#wrap`
@ -362,7 +376,7 @@ Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS /tmp` is possible. to it, so that something like `:LS /tmp` is possible.
```vim ```vim
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
``` ```
@ -372,7 +386,7 @@ a unique name to our command and pass it as the first argument to `fzf#wrap`.
```vim ```vim
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined. " The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
``` ```
@ -380,7 +394,7 @@ command! -bang -complete=dir -nargs=* LS
- `g:fzf_layout` - `g:fzf_layout`
- `g:fzf_action` - `g:fzf_action`
- **Works only when no custom `sink` (or `sink*`) is provided** - **Works only when no custom `sink` (or `sinklist`) is provided**
- Having custom sink usually means that each entry is not an ordinary - Having custom sink usually means that each entry is not an ordinary
file path (e.g. name of color scheme), so we can't blindly apply the file path (e.g. name of color scheme), so we can't blindly apply the
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense) same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
@ -392,15 +406,41 @@ Tips
### fzf inside terminal buffer ### fzf inside terminal buffer
The latest versions of Vim and Neovim include builtin terminal emulator On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
(`:terminal`) and fzf will start in a terminal buffer in the following cases: If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
in Neovim.
- On Neovim ```vim
- On GVim " Terminal colors for seoul256 color scheme
- On Terminal Vim with a non-default layout if has('nvim')
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` let g:terminal_color_0 = '#4e4e4e'
let g:terminal_color_1 = '#d68787'
let g:terminal_color_2 = '#5f865f'
let g:terminal_color_3 = '#d8af5f'
let g:terminal_color_4 = '#85add4'
let g:terminal_color_5 = '#d7afaf'
let g:terminal_color_6 = '#87afaf'
let g:terminal_color_7 = '#d0d0d0'
let g:terminal_color_8 = '#626262'
let g:terminal_color_9 = '#d75f87'
let g:terminal_color_10 = '#87af87'
let g:terminal_color_11 = '#ffd787'
let g:terminal_color_12 = '#add4fb'
let g:terminal_color_13 = '#ffafaf'
let g:terminal_color_14 = '#87d7d7'
let g:terminal_color_15 = '#e4e4e4'
else
let g:terminal_ansi_colors = [
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
\ '#626262', '#d75f87', '#87af87', '#ffd787',
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
\ ]
endif
```
#### Starting fzf in a popup window ### Starting fzf in a popup window
```vim ```vim
" Required: " Required:
@ -410,6 +450,7 @@ The latest versions of Vim and Neovim include builtin terminal emulator
" Optional: " Optional:
" - xoffset [float default 0.5 range [0 ~ 1]] " - xoffset [float default 0.5 range [0 ~ 1]]
" - yoffset [float default 0.5 range [0 ~ 1]] " - yoffset [float default 0.5 range [0 ~ 1]]
" - relative [boolean default v:false]
" - border [string default 'rounded']: Border style " - border [string default 'rounded']: Border style
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
@ -427,21 +468,21 @@ else
endif endif
``` ```
#### Hide statusline ### Hide statusline
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. the window.
For example, if you use a non-popup layout (e.g. `{'down': '40%'}`) on Neovim, For example, if you open fzf on the bottom on the screen (e.g. `{'down':
you might want to temporarily disable the statusline for a cleaner look. '40%'}`), you might want to temporarily disable the statusline for a cleaner
look.
```vim ```vim
if has('nvim') && !exists('g:fzf_layout') let g:fzf_layout = { 'down': '30%' }
autocmd! FileType fzf autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler \| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
``` ```
[License](LICENSE) [License](LICENSE)
@ -449,4 +490,4 @@ endif
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi

462
README.md

File diff suppressed because one or more lines are too long

74
bin/fzf-preview.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# The purpose of this script is to demonstrate how to preview a file or an
# image in the preview window of fzf.
#
# Dependencies:
# - https://github.com/sharkdp/bat
# - https://github.com/hpjansson/chafa
# - https://iterm2.com/utilities/imgcat
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
exit 1
fi
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
file "$1"
exit
fi
# Sometimes bat is installed as batcat.
if command -v batcat > /dev/null; then
batname="batcat"
elif command -v bat > /dev/null; then
batname="bat"
else
cat "$1"
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
exit
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then
chafa -f sixel -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo
# 3. If chafa is not found but imgcat is available, use it on iTerm2
elif command -v imgcat > /dev/null; then
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
# user is running iTerm2. But for the sake of simplicity, we just assume
# that's the case here.
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
# 4. Cannot find any suitable method to preview the image
else
file "$file"
fi

View File

@ -7,10 +7,9 @@ fail() {
exit 2 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' [[ -x "$fzf" ]] || fail 'fzf executable not found'
tmux_args=()
args=() args=()
opt="" opt=""
skip="" skip=""
@ -58,7 +57,7 @@ while [[ $# -gt 0 ]]; do
;; ;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*) -p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then if [[ "$arg" =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-K -E" ]] || opt="-K -E" [[ "$opt" =~ "-E" ]] || opt="-E"
elif [[ "$arg" =~ ^.[lr] ]]; then elif [[ "$arg" =~ ^.[lr] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
@ -96,9 +95,9 @@ while [[ $# -gt 0 ]]; do
elif [[ "$size" =~ %$ ]]; then elif [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:((${#size}-1))}
if [[ -n "$swap" ]]; then if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -l $(( 100 - size ))%"
else else
opt="$opt -p $size" opt="$opt -l $size%"
fi fi
else else
if [[ -n "$swap" ]]; then if [[ -n "$swap" ]]; then
@ -119,8 +118,6 @@ while [[ $# -gt 0 ]]; do
# "--" can be used to separate fzf-tmux options from fzf options to # "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts # avoid conflicts
skip=1 skip=1
tmux_args=("${args[@]}")
args=()
continue continue
;; ;;
*) *)
@ -139,7 +136,7 @@ fi
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
# Handle zoomed tmux pane without popup options by moving it to a temp window # Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
zoomed_without_popup=1 zoomed_without_popup=1
original_window=$(tmux display-message -p "#{window_id}") original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
@ -154,12 +151,18 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
else
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
fi
cleanup() { cleanup() {
\rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options # Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
eval "tmux ${tmux_win_opts[*]}" eval "tmux ${tmux_win_opts[*]}"
fi fi
@ -172,7 +175,7 @@ cleanup() {
resize-pane -Z resize-pane -Z
fi fi
if [ $# -gt 0 ]; then if [[ $# -gt 0 ]]; then
trap - EXIT trap - EXIT
exit 130 exit 130
fi fi
@ -181,9 +184,23 @@ trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
[[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS" if [[ "$opt" =~ "-E" ]]; then
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
elif [[ $tmux_version = 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
exit 2
fi
fi
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"
# Build arguments to fzf # Build arguments to fzf
@ -195,7 +212,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX") export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-K -E" ]]; then if [[ "$opt" =~ "-E" ]]; then
cat $fifo2 & cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
@ -204,7 +221,8 @@ if [[ "$opt" =~ "-K -E" ]]; then
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1
exit $? exit $?
fi fi
@ -216,9 +234,9 @@ else
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux set-window-option synchronize-panes off \;\ tmux \
set-window-option remain-on-exit off \;\ split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \ $tmux_off_opts \
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; } > /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
cat $fifo2 cat $fifo2
exit "$(cat $fifo3)" exit "$(cat $fifo3)"

View File

@ -1,4 +1,4 @@
fzf.txt fzf Last change: January 3 2021 fzf.txt fzf Last change: February 15 2024
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@ -14,8 +14,8 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to
Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap| Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap|
Tips |fzf-tips| Tips |fzf-tips|
fzf inside terminal buffer |fzf-inside-terminal-buffer| fzf inside terminal buffer |fzf-inside-terminal-buffer|
Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window| Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window|
Hide statusline |fzf-hide-statusline| Hide statusline |fzf-hide-statusline|
License |fzf-license| License |fzf-license|
FZF VIM INTEGRATION *fzf-vim-integration* FZF VIM INTEGRATION *fzf-vim-integration*
@ -32,7 +32,10 @@ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using git " If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
< <
If you use {vim-plug}{1}, the same can be written as: If you use {vim-plug}{1}, the same can be written as:
@ -40,7 +43,10 @@ If you use {vim-plug}{1}, the same can be written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If installed using Homebrew on Apple Silicon
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
< <
But if you want the latest Vim plugin file from GitHub rather than the one But if you want the latest Vim plugin file from GitHub rather than the one
@ -68,16 +74,16 @@ SUMMARY *fzf-summary*
The Vim plugin of fzf provides two core functions, and `:FZF` command which is The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them. the basic file selector command built on top of them.
1. `fzf#run([specdict])` 1. `fzf#run([spec dict])`
- Starts fzf inside Vim with the given spec - Starts fzf inside Vim with the given spec
- `:callfzf#run({'source':'ls'})` - `:call fzf#run({'source': 'ls'})`
2. `fzf#wrap([specdict])->(dict)` 2. `fzf#wrap([spec dict]) -> (dict)`
- Takes a spec for `fzf#run` and returns an extended version of it with - Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`) additional options for addressing global preferences (`g:fzf_xxx`)
- `:echofzf#wrap({'source':'ls'})` - `:echo fzf#wrap({'source': 'ls'})`
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run` - We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
- `:callfzf#run(fzf#wrap({'source':'ls'}))` - `:call fzf#run(fzf#wrap({'source': 'ls'}))`
3. `:FZF[fzf_optionsstring][pathstring]` 3. `:FZF [fzf_options string] [path string]`
- Basic fuzzy file selector - Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript to - A reference implementation for those who don't want to write VimScript to
implement custom commands implement custom commands
@ -143,7 +149,7 @@ Examples~
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }')) call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
copen copen
cc cc
endfunction endfunction
@ -155,9 +161,15 @@ Examples~
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
" - Popup window " - Popup window (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" - Popup window (center of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
@ -216,12 +228,12 @@ list:
`spinner` | Streaming input indicator `spinner` | Streaming input indicator
`query` | Query string `query` | Query string
`disabled` | Query string when search is disabled `disabled` | Query string when search is disabled
`prompt` | Prompt before query ( `>` ) `prompt` | Prompt before query ( `> ` )
`pointer` | Pointer to the current line ( `>` ) `pointer` | Pointer to the current line ( `>` )
----------------------------+------------------------------------------------------ ----------------------------+------------------------------------------------------
- `component` specifies the component (`fg` / `bg`) from which to extract the - `component` specifies the component (`fg` / `bg`) from which to extract the
color when considering each of the following highlight groups color when considering each of the following highlight groups
- `group1[,group2,...]` is a list of highlight groups that are searched (in - `group1 [, group2, ...]` is a list of highlight groups that are searched (in
order) for a matching color definition order) for a matching color definition
For example, consider the following specification: For example, consider the following specification:
@ -252,17 +264,18 @@ entry.
call fzf#run({'sink': 'e'}) call fzf#run({'sink': 'e'})
< <
We haven't specified the `source`, so this is equivalent to starting fzf on We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or command line without standard input pipe; fzf will traverse the file system
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current under the current directory to get the list of files. (If
directory. When you select one, it will open it with the sink, `:e` command. `$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
If you want to open it in a new tab, you can pass `:tabedit` command instead instead.) When you select one, it will open it with the sink, `:e` command. If
as the sink. you want to open it in a new tab, you can pass `:tabedit` command instead as
the sink.
> >
call fzf#run({'sink': 'tabedit'}) call fzf#run({'sink': 'tabedit'})
< <
Instead of using the default find command, you can use any shell command as You can use any shell command as the source to generate the list. The
the source. The following example will list the files managed by git. It's following example will list the files managed by git. It's equivalent to
equivalent to running `gitls-files|fzf` on shell. running `git ls-files | fzf` on shell.
> >
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})
< <
@ -290,17 +303,17 @@ The following table summarizes the available options.
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
Option name | Type | Description ~ Option name | Type | Description ~
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` ) `source` | string | External command to generate input to fzf (e.g. `find .` )
`source` | list | Vim list as input to fzf `source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item `sink` | funcref | Reference to function to process each selected item
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once `sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `verticalaboveleft30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width':0.9,'height':0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string `options` entry can be either a string or a list. For simple cases, string
@ -318,7 +331,8 @@ following options are allowed:
- Optional: - Optional:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `border` [string default `rounded`]: Border style - `relative` [boolean default v:false]
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
@ -336,7 +350,7 @@ So how can we make our custom `fzf#run` calls also respect those variables?
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
`fzf#run`. `fzf#run`.
- `fzf#wrap([namestring],[specdict],[fullscreenbool])->(dict)` - `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
- All arguments are optional. Usually we only need to pass a spec - All arguments are optional. Usually we only need to pass a spec
dictionary. dictionary.
- `name` is for managing history files. It is ignored if `g:fzf_history_dir` - `name` is for managing history files. It is ignored if `g:fzf_history_dir`
@ -370,9 +384,9 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0)) command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
< <
Our `:LS` command will be much more useful if we can pass a directory argument Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS/tmp` is possible. to it, so that something like `:LS /tmp` is possible.
> >
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
< <
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
@ -380,7 +394,7 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
> >
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined. " The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
< <
@ -389,10 +403,10 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
- `g:fzf_layout` - `g:fzf_layout`
- `g:fzf_action` - `g:fzf_action`
- Works only when no custom `sink` (or `sink*`) is provided - Works only when no custom `sink` (or `sinklist`) is provided
- Having custom sink usually means that each entry is not an ordinary - Having custom sink usually means that each entry is not an ordinary
file path (e.g. name of color scheme), so we can't blindly apply the file path (e.g. name of color scheme), so we can't blindly apply the
same strategy (i.e. `tabeditsome-color-scheme` doesn't make sense) same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
- `g:fzf_colors` - `g:fzf_colors`
- `g:fzf_history_dir` - `g:fzf_history_dir`
@ -404,16 +418,42 @@ TIPS *fzf-tips*
< fzf inside terminal buffer >________________________________________________~ < fzf inside terminal buffer >________________________________________________~
*fzf-inside-terminal-buffer* *fzf-inside-terminal-buffer*
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
- On GVim If you find the default ANSI colors to be different, consider configuring the
- On Terminal Vim with a non-default layout colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}` in Neovim.
>
" Terminal colors for seoul256 color scheme
if has('nvim')
let g:terminal_color_0 = '#4e4e4e'
let g:terminal_color_1 = '#d68787'
let g:terminal_color_2 = '#5f865f'
let g:terminal_color_3 = '#d8af5f'
let g:terminal_color_4 = '#85add4'
let g:terminal_color_5 = '#d7afaf'
let g:terminal_color_6 = '#87afaf'
let g:terminal_color_7 = '#d0d0d0'
let g:terminal_color_8 = '#626262'
let g:terminal_color_9 = '#d75f87'
let g:terminal_color_10 = '#87af87'
let g:terminal_color_11 = '#ffd787'
let g:terminal_color_12 = '#add4fb'
let g:terminal_color_13 = '#ffafaf'
let g:terminal_color_14 = '#87d7d7'
let g:terminal_color_15 = '#e4e4e4'
else
let g:terminal_ansi_colors = [
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
\ '#626262', '#d75f87', '#87af87', '#ffd787',
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
\ ]
endif
<
Starting fzf in a popup window~ < Starting fzf in a popup window >____________________________________________~
*fzf-starting-fzf-in-a-popup-window* *fzf-starting-fzf-in-a-popup-window*
> >
" Required: " Required:
@ -423,6 +463,7 @@ Starting fzf in a popup window~
" Optional: " Optional:
" - xoffset [float default 0.5 range [0 ~ 1]] " - xoffset [float default 0.5 range [0 ~ 1]]
" - yoffset [float default 0.5 range [0 ~ 1]] " - yoffset [float default 0.5 range [0 ~ 1]]
" - relative [boolean default v:false]
" - border [string default 'rounded']: Border style " - border [string default 'rounded']: Border style
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
@ -438,21 +479,21 @@ or above) by putting fzf-tmux options in `tmux` key.
endif endif
< <
Hide statusline~ < Hide statusline >___________________________________________________________~
*fzf-hide-statusline* *fzf-hide-statusline*
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. the window.
For example, if you use a non-popup layout (e.g. `{'down':'40%'}`) on Neovim, For example, if you open fzf on the bottom on the screen (e.g. `{'down':
you might want to temporarily disable the statusline for a cleaner look. '40%'}`), you might want to temporarily disable the statusline for a cleaner
look.
> >
if has('nvim') && !exists('g:fzf_layout') let g:fzf_layout = { 'down': '30%' }
autocmd! FileType fzf autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler \| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
< <
LICENSE *fzf-license* LICENSE *fzf-license*
@ -460,7 +501,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

25
go.mod
View File

@ -1,15 +1,20 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell v1.4.0 github.com/charlievieth/fastwalk v1.0.3
github.com/mattn/go-isatty v0.0.12 github.com/gdamore/tcell/v2 v2.7.4
github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.10 github.com/mattn/go-shellwords v1.0.12
github.com/saracen/walker v0.1.1 github.com/rivo/uniseg v0.4.7
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 golang.org/x/sys v0.20.0
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect golang.org/x/term v0.20.0
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1
golang.org/x/text v0.3.3 // indirect
) )
go 1.13 require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
golang.org/x/text v0.14.0 // indirect
)
go 1.20

78
go.sum
View File

@ -1,35 +1,57 @@
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/saracen/walker v0.1.1 h1:Ou2QIKTWqo0QxhtuHVmtObbmhjMCEUyJ82xp0uV+MGI= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saracen/walker v0.1.1/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 h1:/DtoiOYKoQCcIFXQjz07RnWNPRCbqmSXSpgEzhC9ZHM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

89
install
View File

@ -2,7 +2,7 @@
set -u set -u
version=0.25.0 version=0.52.1
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() { try_curl() {
command -v curl > /dev/null && command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf - curl -fL $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@ -125,7 +125,7 @@ try_curl() {
try_wget() { try_wget() {
command -v wget > /dev/null && command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf - wget -O - $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@ -168,20 +168,24 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;; Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;; Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;; Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;; Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;; Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;; Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;; Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;; FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
Windows*\ *64) download fzf-$version-windows_amd64.zip ;; OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
*) binary_available=0 binary_error=1 ;; CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;;
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
*) binary_available=0 binary_error=1 ;;
esac esac
cd "$fzf_base" cd "$fzf_base"
@ -192,12 +196,12 @@ if [ -n "$binary_error" ]; then
echo " - $binary_error !!!" echo " - $binary_error !!!"
fi fi
if command -v go > /dev/null; then if command -v go > /dev/null; then
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... " echo -n "Building binary (go install github.com/junegunn/fzf) ... "
if [ -z "${GOPATH-}" ]; then if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH" mkdir -p "$GOPATH"
fi fi
if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then
echo "OK" echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
@ -241,7 +245,7 @@ for shell in $shells; do
src=${prefix_expand}.${shell} src=${prefix_expand}.${shell}
echo -n "Generate $src ... " echo -n "Generate $src ... "
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
if [ $auto_completion -eq 0 ]; then if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
@ -255,9 +259,19 @@ for shell in $shells; do
# Setup fzf # Setup fzf
# --------- # ---------
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin" PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else
cat >> "$src" << EOF
# Auto-completion # Auto-completion
# --------------- # ---------------
$fzf_completion $fzf_completion
@ -266,6 +280,7 @@ $fzf_completion
# ------------ # ------------
$fzf_key_bindings $fzf_key_bindings
EOF EOF
fi
echo "OK" echo "OK"
done done
@ -277,23 +292,6 @@ if [[ "$shells" =~ fish ]]; then
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
EOF EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p "${fish_dir}/functions"
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
fi
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
if [ $key_bindings -ne 0 ]; then
echo -n "Symlink $fish_binding ... "
ln -sf "$fzf_base/shell/key-bindings.fish" \
"$fish_binding" && echo "OK" || echo "Failed"
else
echo -n "Removing $fish_binding ... "
rm -f "$fish_binding"
echo "OK"
fi
fi fi
append_line() { append_line() {
@ -356,12 +354,23 @@ done
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then if [ ! -e "$bind_file" ]; then
mkdir -p "${fish_dir}/functions"
create_file "$bind_file" \ create_file "$bind_file" \
'function fish_user_key_bindings' \ 'function fish_user_key_bindings' \
' fzf_key_bindings' \ ' fzf --fish | source' \
'end' 'end'
else else
append_line $update_config "fzf_key_bindings" "$bind_file" echo "Check $bind_file:"
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $lno ]]; then
echo " ** Found 'fzf_key_bindings' in line #$lno"
echo " ** You have to replace the line to 'fzf --fish | source'"
echo
else
echo " - Clear"
echo
append_line $update_config "fzf --fish | source" "$bind_file"
fi
fi fi
fi fi

View File

@ -1,4 +1,4 @@
$version="0.25.0" $version="0.52.1"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
@ -43,7 +43,11 @@ function download {
$url="https://github.com/junegunn/fzf/releases/download/$version/$file" $url="https://github.com/junegunn/fzf/releases/download/$version/$file"
$temp=$env:TMP + "\fzf.zip" $temp=$env:TMP + "\fzf.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp")) if ($PSVersionTable.PSVersion.Major -ge 3) {
Invoke-WebRequest -Uri $url -OutFile $temp
} else {
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
}
if ($?) { if ($?) {
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp) (Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
} else { } else {
@ -53,7 +57,7 @@ function download {
$binary_error="Failed to download $file" $binary_error="Failed to download $file"
return return
} }
check_binary >$null echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
} }
download "fzf-$version-windows_amd64.zip" download "fzf-$version-windows_amd64.zip"

76
main.go
View File

@ -1,14 +1,82 @@
package main package main
import ( import (
"github.com/junegunn/fzf/src" _ "embed"
"fmt"
"os"
"strings"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.24" var version = "0.52"
var revision string = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash
var bashKeyBindings []byte
//go:embed shell/completion.bash
var bashCompletion []byte
//go:embed shell/key-bindings.zsh
var zshKeyBindings []byte
//go:embed shell/completion.zsh
var zshCompletion []byte
//go:embed shell/key-bindings.fish
var fishKeyBindings []byte
func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content)))
fmt.Println("### end: " + label + " ###")
}
func exit(code int, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() { func main() {
protector.Protect() protector.Protect()
fzf.Run(fzf.ParseOptions(), version, revision)
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash {
printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion)
return
}
if options.Zsh {
printScript("key-bindings.zsh", zshKeyBindings)
printScript("completion.zsh", zshCompletion)
return
}
if options.Fish {
printScript("key-bindings.fish", fishKeyBindings)
fmt.Println("fzf_key_bindings")
return
}
if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
code, err := fzf.Run(options)
exit(code, err)
} }

View File

@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jan 2021" "fzf 0.25.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 .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
" Copyright (c) 2017 Junegunn Choi " Copyright (c) 2013-2024 Junegunn Choi
" "
" MIT License " MIT License
" "
@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage) return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction endfunction
function! s:wrap_cmds(cmds) function! s:wrap_cmds(cmds)
return map([ return map(['@echo off']
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
\ + (has('gui_running') ? ['set TERM= > nul'] : []) \ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ + ['endlocal'],
\ '<SID>enc_to_cp(v:val."\r")') \ '<SID>enc_to_cp(v:val."\r")')
endfunction endfunction
else else
@ -84,11 +81,21 @@ else
endif endif
function! s:shellesc_cmd(arg) function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let e = '"'
let escaped = substitute(escaped, '%', '%%', 'g') let slashes = 0
let escaped = substitute(escaped, '"', '\\^&', 'g') for c in split(a:arg, '\zs')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') if c ==# '\'
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' 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 endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)
@ -96,7 +103,12 @@ function! fzf#shellescape(arg, ...)
if shell =~# 'cmd.exe$' if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg) return s:shellesc_cmd(a:arg)
endif endif
return s:fzf_call('shellescape', a:arg) try
let [shell, &shell] = [&shell, shell]
return s:fzf_call('shellescape', a:arg)
finally
let [shell, &shell] = [&shell, shell]
endtry
endfunction endfunction
function! s:fzf_getcwd() function! s:fzf_getcwd()
@ -138,7 +150,7 @@ function! fzf#install()
if !filereadable(script) if !filereadable(script)
throw script.' not found' throw script.' not found'
endif endif
let script = 'powershell -ExecutionPolicy Bypass -file ' . script let script = 'powershell -ExecutionPolicy Bypass -file ' . shellescape(script)
else else
let script = s:base_dir.'/install' let script = s:base_dir.'/install'
if !executable(script) if !executable(script)
@ -154,46 +166,79 @@ function! fzf#install()
endif endif
endfunction endfunction
function! s:version_requirement(val, min) let s:versions = {}
let val = split(a:val, '\.') function s:get_version(bin)
let min = split(a:min, '\.') if has_key(s:versions, a:bin)
for idx in range(0, len(min) - 1) return s:versions[a:bin]
let v = get(val, idx, 0) end
if v < min[idx] | return 0 let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
elseif v > min[idx] | return 1 let output = systemlist(command)
if v:shell_error || empty(output)
return ''
endif
let ver = matchstr(output[-1], '[0-9.]\+')
let s:versions[a:bin] = ver
return ver
endfunction
function! s:compare_versions(a, b)
let a = split(a:a, '\.')
let b = split(a:b, '\.')
for idx in range(0, max([len(a), len(b)]) - 1)
let v1 = str2nr(get(a, idx, 0))
let v2 = str2nr(get(b, idx, 0))
if v1 < v2 | return -1
elseif v1 > v2 | return 1
endif endif
endfor endfor
return 1 return 0
endfunction
function! s:compare_binary_versions(a, b)
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
endfunction endfunction
let s:checked = {} let s:checked = {}
function! fzf#exec(...) function! fzf#exec(...)
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) let binaries = []
let s:exec = s:fzf_go if executable('fzf')
elseif executable('fzf') call add(binaries, 'fzf')
let s:exec = 'fzf'
elseif input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
call fzf#install()
return fzf#exec()
else
redraw
throw 'fzf executable not found'
endif endif
if executable(s:fzf_go)
call add(binaries, s:fzf_go)
endif
if empty(binaries)
if input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
call fzf#install()
return fzf#exec()
else
redraw
throw 'fzf executable not found'
endif
elseif len(binaries) > 1
call sort(binaries, 's:compare_binary_versions')
endif
let s:exec = binaries[-1]
endif endif
if a:0 && !has_key(s:checked, a:1) if a:0 && !has_key(s:checked, a:1)
let command = s:exec . ' --version' let fzf_version = s:get_version(s:exec)
let output = systemlist(command) if empty(fzf_version)
if v:shell_error || empty(output) let message = printf('Failed to run "%s --version"', s:exec)
throw printf('Failed to run "%s": %s', command, output) unlet s:exec
endif throw message
let fzf_version = matchstr(output[-1], '[0-9.]\+') end
if s:version_requirement(fzf_version, a:1)
if s:compare_versions(fzf_version, a:1) >= 0
let s:checked[a:1] = 1 let s:checked[a:1] = 1
return s:exec return s:exec
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y' elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
let s:versions = {}
unlet s:exec
redraw redraw
call fzf#install() call fzf#install()
return fzf#exec(a:1, 1) return fzf#exec(a:1, 1)
@ -304,7 +349,8 @@ function! s:common_sink(action, lines) abort
endfunction endfunction
function! s:get_color(attr, ...) function! s:get_color(attr, ...)
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors " Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
let fam = gui ? 'gui' : 'cterm' let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
@ -386,13 +432,13 @@ function! fzf#wrap(...)
endif endif
" Action: g:fzf_action " Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*']) if !s:has_any(opts, ['sink', 'sinklist', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action) let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',') let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort function! opts.sinklist(lines) abort
return s:common_sink(self._action, a:lines) return s:common_sink(self._action, a:lines)
endfunction endfunction
let opts['sink*'] = remove(opts, 'sink') let opts['sink*'] = opts.sinklist " For backward compatibility
endif endif
return opts return opts
@ -411,6 +457,36 @@ function! s:use_sh()
return [shell, shellslash, shellcmdflag, shellxquote] return [shell, shellslash, shellcmdflag, shellxquote]
endfunction endfunction
function! s:writefile(...)
if call('writefile', a:000) == -1
throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.'
endif
endfunction
function! s:extract_option(opts, name)
let opt = ''
let expect = 0
" There are a few cases where this function doesn't work as expected.
" Let's just assume such cases are extremely unlikely in real world.
" e.g. --query --border
for word in split(a:opts)
if expect && word !~ '^"\=-'
let opt = opt . ' ' . word
let expect = 0
elseif word == '--no-'.a:name
let opt = ''
elseif word =~ '^--'.a:name.'='
let opt = word
elseif word =~ '^--'.a:name.'$'
let opt = word
let expect = 1
elseif expect
let expect = 0
endif
endfor
return opt
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@ -419,15 +495,15 @@ try
let temps = { 'result': s:fzf_tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = s:evaluate_opts(get(dict, 'options', '')) let optstr = s:evaluate_opts(get(dict, 'options', ''))
try try
let fzf_exec = fzf#shellescape(fzf#exec()) let fzf_exec = shellescape(fzf#exec())
catch catch
throw v:exception throw v:exception
endtry endtry
if !has_key(dict, 'dir') if !s:present(dict, 'dir')
let dict.dir = s:fzf_getcwd() let dict.dir = s:fzf_getcwd()
endif endif
if has('win32unix') && has_key(dict, 'dir') if has('win32unix') && s:present(dict, 'dir')
let dict.dir = fnamemodify(dict.dir, ':p') let dict.dir = fnamemodify(dict.dir, ':p')
endif endif
@ -435,11 +511,11 @@ try
let source = dict.source let source = dict.source
let type = type(source) let type = type(source)
if type == 1 if type == 1
let prefix = '( '.source.' )|' let prefix = '('.source.')|'
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call s:writefile(source, temps.input)
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|' let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else else
throw 'Invalid source type' throw 'Invalid source type'
endif endif
@ -454,19 +530,20 @@ try
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_height if use_term
let optstr .= ' --no-height'
elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --height='.height
elseif use_term
let optstr .= ' --no-height'
endif endif
let optstr .= s:border_opt(get(dict, 'window', 0)) " 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 command = prefix.(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 if use_term
@ -615,7 +692,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif endif
if s:is_win if s:is_win
let batchfile = s:fzf_tempname().'.bat' let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile) call s:writefile(s:wrap_cmds(command), batchfile)
let command = batchfile let command = batchfile
let a:temps.batchfile = batchfile let a:temps.batchfile = batchfile
if has('nvim') if has('nvim')
@ -633,8 +710,8 @@ function! s:execute(dict, command, use_height, temps) abort
endif endif
elseif has('win32unix') && $TERM !=# 'cygwin' elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname() let shellscript = s:fzf_tempname()
call writefile([command], shellscript) 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 let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height
@ -682,7 +759,7 @@ function! s:calc_size(max, val, dict)
return size return size
endif endif
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
if stridx(opts, '--header') > stridx(opts, '--no-header') if stridx(opts, '--header') > stridx(opts, '--no-header')
let margin += len(split(opts, "\n")) let margin += len(split(opts, "\n"))
endif endif
@ -699,9 +776,9 @@ function! s:border_opt(window)
endif endif
" Border style " Border style
let style = tolower(get(a:window, 'border', 'rounded')) let style = tolower(get(a:window, 'border', ''))
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1) if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
let style = 'sharp' let style = a:window.rounded ? 'rounded' : 'sharp'
endif endif
if style == 'none' || style == 'no' if style == 'none' || style == 'no'
return '' return ''
@ -709,7 +786,7 @@ function! s:border_opt(window)
" For --border styles, we need fzf 0.24.0 or above " For --border styles, we need fzf 0.24.0 or above
call fzf#exec('0.24.0') call fzf#exec('0.24.0')
let opt = ' --border=' . style let opt = ' --border ' . style
if has_key(a:window, 'highlight') if has_key(a:window, 'highlight')
let color = s:get_color('fg', a:window.highlight) let color = s:get_color('fg', a:window.highlight)
if len(color) if len(color)
@ -764,6 +841,24 @@ function! s:split(dict)
endtry endtry
endfunction endfunction
nnoremap <silent> <Plug>(fzf-insert) i
nnoremap <silent> <Plug>(fzf-normal) <Nop>
if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
endif
let s:warned = 0
function! s:handle_ambidouble(dict)
if &ambiwidth == 'double'
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
2sleep
let s:warned = 1
endif
endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
@ -776,7 +871,7 @@ function! s:execute_term(dict, command, temps) abort
function! fzf.switch_back(inplace) function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf if a:inplace && bufnr('') == self.buf
if bufexists(self.pbuf) if bufexists(self.pbuf)
execute 'keepalt b' self.pbuf execute 'keepalt keepjumps b' self.pbuf
endif endif
" No other listed buffer " No other listed buffer
if bufnr('') == self.buf if bufnr('') == self.buf
@ -816,31 +911,37 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(self.dict) call s:pushd(self.dict)
call s:callback(self.dict, lines) call s:callback(self.dict, lines)
call self.switch_back(s:getpos() == self.ppos) call self.switch_back(s:getpos() == self.ppos)
if &buftype == 'terminal'
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
endif
endfunction endfunction
try try
call s:pushd(a:dict) call s:pushd(a:dict)
if s:is_win if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat' let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile) call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile let command = fzf.temps.batchfile
else else
let command = a:command let command = a:command
endif endif
let command .= s:term_marker let command .= s:term_marker
if has('nvim') if has('nvim')
call s:handle_ambidouble(fzf)
call termopen(command, fzf) call termopen(command, fzf)
else else
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'} let term_opts = {'exit_cb': function(fzf.on_exit)}
if v:version >= 802
let term_opts.term_kill = 'term'
endif
if is_popup if is_popup
let term_opts.hidden = 1 let term_opts.hidden = 1
else else
let term_opts.curwin = 1 let term_opts.curwin = 1
endif endif
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts) call s:handle_ambidouble(term_opts)
if exists('&termwinkey') keepjumps let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
call setbufvar(fzf.buf, '&termwinkey', '<c-z>')
endif
if is_popup && exists('#TerminalWinOpen') if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen doautocmd <nomodeline> TerminalWinOpen
endif endif
@ -849,6 +950,9 @@ function! s:execute_term(dict, command, temps) abort
endif endif
endif endif
tnoremap <buffer> <c-z> <nop> tnoremap <buffer> <c-z> <nop>
if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '<c-w>')
tnoremap <buffer> <c-w> <c-w>.
endif
finally finally
call s:dopopd() call s:dopopd()
endtry endtry
@ -886,6 +990,8 @@ function! s:callback(dict, lines) abort
endif endif
if has_key(a:dict, 'sink*') if has_key(a:dict, 'sink*')
call a:dict['sink*'](a:lines) call a:dict['sink*'](a:lines)
elseif has_key(a:dict, 'sinklist')
call a:dict['sinklist'](a:lines)
endif endif
catch catch
if stridx(v:exception, ':E325:') < 0 if stridx(v:exception, ':E325:') < 0
@ -901,20 +1007,16 @@ function! s:callback(dict, lines) abort
endfunction endfunction
if has('nvim') if has('nvim')
function s:create_popup(hl, opts) abort function s:create_popup(opts) abort
let buf = nvim_create_buf(v:false, v:true) let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
let win = nvim_open_win(buf, v:true, opts) let win = nvim_open_win(buf, v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl) silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
call setwinvar(win, '&colorcolumn', '') call setwinvar(win, '&colorcolumn', '')
if !empty(border)
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
endif
return buf return buf
endfunction endfunction
else else
function! s:create_popup(hl, opts) abort function! s:create_popup(opts) abort
let s:popup_create = {buf -> popup_create(buf, #{ let s:popup_create = {buf -> popup_create(buf, #{
\ line: a:opts.row, \ line: a:opts.row,
\ col: a:opts.col, \ col: a:opts.col,
@ -929,11 +1031,19 @@ else
endif endif
function! s:popup(opts) abort function! s:popup(opts) abort
let xoffset = get(a:opts, 'xoffset', 0.5)
let yoffset = get(a:opts, 'yoffset', 0.5)
let relative = get(a:opts, 'relative', 0)
" Use current window size for positioning relatively positioned popups
let columns = relative ? winwidth(0) : &columns
let lines = relative ? winheight(0) : (&lines - has('nvim'))
" Size and position " Size and position
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(&columns * a:opts.width)]), &columns]) let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(&lines * a:opts.height)]), &lines - has('nvim')]) let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
let row = float2nr(get(a:opts, 'yoffset', 0.5) * (&lines - height)) let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
let col = float2nr(get(a:opts, 'xoffset', 0.5) * (&columns - width)) let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
" Managing the differences " Managing the differences
let row = min([max([0, row]), &lines - has('nvim') - height]) let row = min([max([0, row]), &lines - has('nvim') - height])
@ -941,7 +1051,7 @@ function! s:popup(opts) abort
let row += !has('nvim') let row += !has('nvim')
let col += !has('nvim') let col += !has('nvim')
call s:create_popup('Normal', { call s:create_popup({
\ 'row': row, 'col': col, 'width': width, 'height': height \ 'row': row, 'col': col, 'width': width, 'height': height
\ }) \ })
endfunction endfunction

View File

@ -4,40 +4,48 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.bash # /_/ /___/_/ completion.bash
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty) # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _fzf_compgen_dir > /dev/null; then # To use custom commands instead of find, override _fzf_compgen_{path,dir}
_fzf_compgen_dir() { #
command find -L "$1" \ # _fzf_compgen_path() {
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ # echo "$1"
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' # command find -L "$1" \
} # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
fi # -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
# }
#
# _fzf_compgen_dir() {
# command find -L "$1" \
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
# }
########################################################### ###########################################################
# To redraw line after fzf closes (printf '\e[5n') # To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 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() { __fzf_comprun() {
if [ "$(type -t _fzf_comprun 2>&1)" = function ]; then if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then
shift shift
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@" fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
else else
@ -55,72 +63,172 @@ __fzf_orig_completion() {
cmd="${BASH_REMATCH[3]}" cmd="${BASH_REMATCH[3]}"
[[ "$f" = _fzf_* ]] && continue [[ "$f" = _fzf_* ]] && continue
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}" printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
if [[ "$l" = *" -o nospace "* ]] && [[ ! "$__fzf_nospace_commands" = *" $cmd "* ]]; then if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then
__fzf_nospace_commands="$__fzf_nospace_commands $cmd " __fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
fi fi
fi fi
done done
} }
# @param $1 cmd - Command name for which the original completion is searched
# @var[out] REPLY - Original function name is returned
__fzf_orig_completion_get_orig_func() {
local cmd orig_var orig
cmd=$1
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}"
REPLY="${orig##*#}"
[[ $REPLY ]] && type "$REPLY" &> /dev/null
}
# @param $1 cmd - Command name for which the original completion is searched
# @param $2 func - Fzf's completion function to replace the original function
# @var[out] REPLY - Completion setting is returned as a string to "eval"
__fzf_orig_completion_instantiate() {
local cmd func orig_var orig
cmd=$1
func=$2
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}"
orig="${orig%#*}"
[[ $orig == *' %s '* ]] || return 1
printf -v REPLY "$orig" "$func"
}
_fzf_opts_completion() { _fzf_opts_completion() {
local cur prev opts local cur prev opts
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-x --extended -h --help
-e --exact -e --exact
--algo +x --no-extended
-i +i -q --query
-f --filter
--literal
--scheme
--expect
--disabled
--tiebreak
--bind
--color
-d --delimiter
-n --nth -n --nth
--with-nth --with-nth
-d --delimiter
+s --no-sort +s --no-sort
--track
--tac --tac
--tiebreak -i --ignore-case
+i --no-ignore-case
-m --multi -m --multi
--ansi
--no-mouse --no-mouse
--bind +c --no-color
--cycle --no-bold
--no-hscroll --layout
--jump-labels
--height
--literal
--reverse --reverse
--margin --cycle
--inline-info --keep-right
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--info
--separator
--no-separator
--no-scrollbar
--jump-labels
-1 --select-1
-0 --exit-0
--read0
--print0
--print-query
--prompt --prompt
--pointer --pointer
--marker --marker
--header --sync
--header-lines
--ansi
--tabstop
--color
--no-bold
--history --history
--history-size --history-size
--header
--header-lines
--header-first
--ellipsis
--preview --preview
--preview-window --preview-window
-q --query --height
-1 --select-1 --min-height
-0 --exit-0 --border
-f --filter --border-label
--print-query --border-label-pos
--expect --preview-label
--sync" --preview-label-pos
--no-unicode
--margin
--padding
--tabstop
--listen
--no-clear
--version
--"
case "${prev}" in case "${prev}" in
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
;;
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
return 0 return 0
;; ;;
--color) --color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
return 0 return 0
;; ;;
--history) --layout)
COMPREPLY=() COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
return 0
;;
--info)
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
return 0
;;
--preview-window)
COMPREPLY=( $(compgen -W "
default
hidden
nohidden
wrap
nowrap
cycle
nocycle
up top
down bottom
left
right
rounded border border-rounded
sharp border-sharp
border-bold
border-block
border-thinblock
border-double
noborder border-none
border-horizontal
border-vertical
border-up border-top
border-down border-bottom
border-left
border-right
follow
nofollow" -- "$cur") )
return 0
;;
--border)
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
return 0
;;
--border-label-pos|--preview-label-pos)
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
return 0 return 0
;; ;;
esac esac
@ -134,54 +242,78 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd orig_complete local cmd ret REPLY orig_cmd orig_complete
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd" if __fzf_orig_completion_get_orig_func "$cmd"; then
orig="${!orig_var##*#}" "$REPLY" "$@"
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then elif [[ -n "${_fzf_completion_loader-}" ]]; then
$orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@" $_fzf_completion_loader "$@"
ret=$? ret=$?
# _completion_loader may not have updated completion for the command # _completion_loader may not have updated completion for the command
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null) __fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
# Update orig_complete by _fzf_orig_completion entry
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
orig_complete=$REPLY
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }" eval "${orig_complete/ -F / -o nospace -F }"
else else
eval "$orig_complete" eval "$orig_complete"
fi fi
fi fi
[[ $ret -eq 0 ]] && return 124
return $ret return $ret
fi fi
} }
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" cmd="${COMP_WORDS[0]}"
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
fi
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" eval "base=$base" 2> /dev/null || return
dir=
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
while true; do while true; do
if [ -z "$dir" ] || [ -d "$dir" ]; then if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [[ -z "$dir" ]] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do matches=$(
printf "%q$3 " "$item" export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
done) unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
if [[ $1 =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
)
matches=${matches% } matches=${matches% }
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " [[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [ -n "$matches" ]; then if [[ -n "$matches" ]]; then
COMPREPLY=( "$matches" ) COMPREPLY=( "$matches" )
else else
COMPREPLY=( "$cur" ) COMPREPLY=( "$cur" )
@ -189,7 +321,7 @@ __fzf_generic_path_completion() {
printf '\e[5n' printf '\e[5n'
return 0 return 0
fi fi
dir=$(dirname "$dir") dir=$(command dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/ [[ "$dir" =~ /$ ]] || dir="$dir"/
done done
else else
@ -223,18 +355,21 @@ _fzf_complete() {
fi fi
local cur selected trigger cmd post local cur selected trigger cmd post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | command awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post='command cat'
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cmd="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}} 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 | 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" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [ -n "$selected" ]; then if [[ -n "$selected" ]]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
else else
COMPREPLY=("$cur") COMPREPLY=("$cur")
@ -260,79 +395,119 @@ _fzf_dir_completion() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
local trigger=${FZF_COMPLETION_TRIGGER-'**'}
local cur="${COMP_WORDS[COMP_CWORD]}"
if [[ -z "$cur" ]]; then
COMP_WORDS[$COMP_CWORD]=$trigger
elif [[ "$cur" != *"$trigger" ]]; then
return 1
fi
_fzf_proc_completion "$@" _fzf_proc_completion "$@"
} }
_fzf_proc_completion() { _fzf_proc_completion() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
_fzf_proc_completion_post() { _fzf_proc_completion_post() {
awk '{print $2}' command awk '{print $2}'
} }
# To use custom hostname lists, override __fzf_list_hosts.
# The function is expected to print hostnames, one per line as well as in the
# desired sorting and with any duplicates removed, to standard output.
#
# e.g.
# # Use bash-completionss _known_hosts_real() for getting the list of hosts
# __fzf_list_hosts() {
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
# local COMPREPLY=()
# _known_hosts_real ''
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
# }
if ! declare -F __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() {
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
}
fi
_fzf_host_completion() { _fzf_host_completion() {
_fzf_complete +m -- "$@" < <( _fzf_complete +m -- "$@" < <(__fzf_list_hosts)
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ }
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | # Values for $1 $2 $3 are described here
awk '{if (length($2) > 0) {print $2}}' | sort -u # https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
) # > the first argument ($1) is the name of the command whose arguments are being completed,
# > the second argument ($2) is the word being completed,
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
_fzf_complete_ssh() {
case $3 in
-i|-F|-E)
_fzf_path_completion "$@"
;;
*)
local user=
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_var_completion() { _fzf_var_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //' declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
) )
} }
_fzf_alias_completion() { _fzf_alias_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //' alias | command sed -En 's|^alias ([^=]+).*|\1|p'
) )
} }
# fzf options # fzf options
complete -o default -F _fzf_opts_completion fzf complete -o default -F _fzf_opts_completion fzf
# fzf-tmux is a thin fzf wrapper that has only a few more options than fzf
# itself. As a quick improvement we take fzf's completion. Adding the few extra
# 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}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
a_cmds="
awk cat diff diff3 # NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
emacs emacsclient ex file ftp g++ gcc gvim head hg java # 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 javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp 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 # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
if type _completion_loader > /dev/null 2>&1; then if type _comp_load > /dev/null 2>&1; then
_fzf_completion_loader=1 # _comp_load was added in bash-completion 2.12 to replace _completion_loader.
# We use it without -D option so that it does not use _comp_complete_minimal as the fallback.
_fzf_completion_loader=_comp_load
elif type __load_completion > /dev/null 2>&1; then
# In bash-completion 2.11, _completion_loader internally calls __load_completion
# and if it returns a non-zero status, it sets the default 'minimal' completion.
_fzf_completion_loader=__load_completion
elif type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=_completion_loader
fi fi
__fzf_defc() { __fzf_defc() {
local cmd func opts orig_var orig def local cmd func opts REPLY
cmd="$1" cmd="$1"
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" if __fzf_orig_completion_instantiate "$cmd" "$func"; then
orig="${!orig_var}" eval "$REPLY"
if [ -n "$orig" ]; then
printf -v def "$orig" "$func"
eval "$def"
else else
complete -F "$func" $opts "$cmd" complete -F "$func" $opts "$cmd"
fi fi
@ -345,13 +520,24 @@ done
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
done done
# Kill completion (supports empty completion trigger) # Variables
complete -F _fzf_complete_kill -o default -o bashdefault kill for cmd in $v_cmds; do
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
done
unset cmd d_cmds a_cmds # 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 v_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
local kind fn cmd local kind fn cmd
@ -373,9 +559,4 @@ _fzf_setup_completion() {
done done
} }
# Environment variables / Aliases / Hosts
_fzf_setup_completion 'var' export unset
_fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' ssh telnet
fi fi

View File

@ -4,10 +4,13 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.zsh # /_/ /___/_/ completion.zsh
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%') # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $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 # Both branches of the following `if` do the same thing -- define
# __fzf_completion_options such that `eval $__fzf_completion_options` sets # __fzf_completion_options such that `eval $__fzf_completion_options` sets
@ -67,41 +70,46 @@ fi
# control. There are several others that could wreck havoc if they are set # control. There are several others that could wreck havoc if they are set
# to values we don't expect. With the following `emulate` command we # to values we don't expect. With the following `emulate` command we
# sidestep this issue entirely. # sidestep this issue entirely.
'emulate' 'zsh' '-o' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
# This brace is the start of try-always block. The `always` part is like # 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. # `finally` in lesser languages. We use it to *always* restore user options.
{ {
# The 'emulate' command should not be placed inside the interactive if check;
# Bail out if not interactive shell. # placing it there fails to disable alias expansion. See #3731.
[[ -o interactive ]] || return 0 if [[ -o interactive ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then #
_fzf_compgen_path() { # _fzf_compgen_path() {
echo "$1" # echo "$1"
command find -L "$1" \ # command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' # -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} # }
fi #
# _fzf_compgen_dir() {
if ! declare -f _fzf_compgen_dir > /dev/null; then # command find -L "$1" \
_fzf_compgen_dir() { # -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
command find -L "$1" \ # -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ # }
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
########################################################### ###########################################################
__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() { __fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then elif [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; }; then
shift shift
if [ -n "$FZF_TMUX_OPTS" ]; then if [ -n "${FZF_TMUX_OPTS-}" ]; then
fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@" fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@"
else else
fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@" fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@"
@ -137,7 +145,10 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
setopt localoptions nonomatch setopt localoptions nonomatch
eval "base=$base" if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
return
fi
eval "base=$base" 2> /dev/null || return
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [[ -z "$dir" || -d ${dir} ]]; then if [[ -z "$dir" || -d ${dir} ]]; then
@ -145,9 +156,25 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do matches=$(
echo -n "${(q)item}$suffix " export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
done) unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
if [[ $compgen =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
done
)
matches=${matches% } matches=${matches% }
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
@ -183,7 +210,7 @@ _fzf_complete() {
args=("$@") args=("$@")
sep= sep=
for i in {0..${#args[@]}}; do for i in {0..${#args[@]}}; do
if [[ "${args[$i]}" = -- ]]; then if [[ "${args[$i]-}" = -- ]]; then
sep=$i sep=$i
break break
fi fi
@ -207,29 +234,47 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo" _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 if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle reset-prompt
command rm -f "$fifo" command rm -f "$fifo"
} }
# To use custom hostname lists, override __fzf_list_hosts.
# The function is expected to print hostnames, one per line as well as in the
# desired sorting and with any duplicates removed, to standard output.
if ! declare -f __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() {
setopt localoptions nonomatch
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
}
fi
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete +m -- "$@" < <( _fzf_complete +m -- "$@" < <(__fzf_list_hosts)
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
# The current word without the trigger is in the $prefix variable passed from the caller.
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete +m -- "$@" < <( local tokens=(${(z)1})
setopt localoptions nonomatch case ${tokens[-1]} in
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ -i|-F|-E)
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ _fzf_path_completion "$prefix" "$1"
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | ;;
awk '{if (length($2) > 0) {print $2}}' | sort -u *)
) local user=
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_complete_export() { _fzf_complete_export() {
@ -251,8 +296,9 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
@ -279,29 +325,27 @@ fzf-completion() {
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token # When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]}${tokens[-1]}" tokens[-2]="${tokens[-2]-}${tokens[-1]}"
tokens=(${tokens[0,-2]}) tokens=(${tokens[0,-2]})
fi fi
lbuf=$LBUFFER lbuf=$LBUFFER
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
tail=$trigger
tokens+=$trigger
lbuf="$lbuf$trigger"
fi
# Trigger sequence given # Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then 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}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
return
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd} > /dev/null"; then if eval "type _fzf_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf} prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
zle reset-prompt
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" "$lbuf" _fzf_dir_completion "$prefix" "$lbuf"
else else
@ -321,6 +365,7 @@ fzf-completion() {
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion
fi
} always { } always {
# Restore the original options. # Restore the original options.

View File

@ -11,53 +11,97 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
# Key bindings
# ------------
__fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then 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__() {
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
}
__fzfcmd() { __fzfcmd() {
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } && [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
} }
fzf-file-widget() { fzf-file-widget() {
local selected="$(__fzf_select__)" local selected="$(__fzf_select__ "$@")"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} )) READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
} }
__fzf_cd__() { __fzf_cd__() {
local cmd dir local dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ dir=$(
-o -type d -print 2> /dev/null | cut -b3-"}" FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" 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)"
} }
__fzf_history__() { if command -v perl > /dev/null; then
local output __fzf_history__() {
output=$( local output script
builtin fc -lnr -2147483648 | script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' | output=$(
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE" set +o pipefail
) || return builtin fc -lnr -2147483648 |
READLINE_LINE=${output#*$'\t'} last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
if [ -z "$READLINE_POINT" ]; then FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
echo "$READLINE_LINE" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
else ) || return
READLINE_POINT=0x7fffffff READLINE_LINE=${output#*$'\t'}
fi if [[ -z "$READLINE_POINT" ]]; then
} echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
else # awk - fallback for POSIX systems
__fzf_history__() {
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
[[ $(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 }
/^\t/ { P(b); b = substr($0, 2); next }
{ b = b RS $0 }
END { if (NR) P(b) }'
output=$(
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=$(__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
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
fi
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind -m emacs-standard '"\er": redraw-current-line' bind -m emacs-standard '"\er": redraw-current-line'
@ -66,21 +110,25 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode' bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode' bind -m emacs-standard '"\C-z": vi-editing-mode'
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then if (( BASH_VERSINFO[0] < 4 )); then
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"' bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"' bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"' bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard -x '"\C-t": fzf-file-widget' if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m vi-command -x '"\C-t": fzf-file-widget' bind -m emacs-standard -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget' bind -m vi-command -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard -x '"\C-r": __fzf_history__' bind -m emacs-standard -x '"\C-r": __fzf_history__'
@ -89,8 +137,10 @@ else
fi fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
fi fi

View File

@ -11,28 +11,35 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
status is-interactive; or exit 0
# Key bindings # Key bindings
# ------------ # ------------
function fzf_key_bindings 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 # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -lx dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --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")
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
commandline -f repaint commandline -f repaint
@ -42,6 +49,7 @@ function fzf_key_bindings
commandline -t "" commandline -t ""
end end
for i in $result for i in $result
commandline -it -- $prefix
commandline -it -- (string escape $i) commandline -it -- (string escape $i)
commandline -it -- ' ' commandline -it -- ' '
end end
@ -51,7 +59,8 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --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_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
@ -72,22 +81,23 @@ function fzf_key_bindings
function fzf-cd-widget -d "Change directory" function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -lx dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --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")
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd $result cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""
commandline -it -- $prefix
end end
end end
@ -106,19 +116,33 @@ function fzf_key_bindings
end end
end end
bind \ct fzf-file-widget
bind \cr fzf-history-widget bind \cr fzf-history-widget
bind \ec fzf-cd-widget if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
if bind -M insert > /dev/null 2>&1 end
bind -M insert \ct fzf-file-widget if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind -M insert \cr fzf-history-widget bind \ec fzf-cd-widget
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 and rest of token' if bind -M insert > /dev/null 2>&1
bind -M insert \cr fzf-history-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'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# eval is used to do shell expansion on paths # eval is used to do shell expansion on paths
set -l commandline (eval "printf '%s' "(commandline -t)) eval set commandline $commandline
if [ -z $commandline ] if [ -z $commandline ]
# Default to current directory with no --query # Default to current directory with no --query
@ -138,6 +162,7 @@ function fzf_key_bindings
echo $dir echo $dir
echo $fzf_query echo $fzf_query
echo $prefix
end end
function __fzf_get_dir -d 'Find the longest existing filepath from input string' function __fzf_get_dir -d 'Find the longest existing filepath from input string'

View File

@ -11,6 +11,7 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
# Key bindings # Key bindings
# ------------ # ------------
@ -32,21 +33,26 @@ else
} }
fi fi
'emulate' 'zsh' '-o' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{ {
if [[ -o interactive ]]; then
[[ -o interactive ]] || return 0 __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 # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fzf_select() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@ -55,72 +61,73 @@ __fsel() {
} }
__fzfcmd() { __fzfcmd() {
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } && [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
} }
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$? local ret=$?
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-file-widget if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bindkey '^T' fzf-file-widget zle -N fzf-file-widget
bindkey -M emacs '^T' fzf-file-widget
# Ensure precmds are run after cd bindkey -M vicmd '^T' fzf-file-widget
fzf-redraw-prompt() { bindkey -M viins '^T' fzf-file-widget
local precmd fi
for precmd in $precmd_functions; do
$precmd
done
zle reset-prompt
}
zle -N fzf-redraw-prompt
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" local dir="$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
fi fi
if [ -z "$BUFFER" ]; then zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="cd ${(q)dir}" BUFFER="builtin cd -- ${(q)dir:a}"
zle accept-line zle accept-line
else
print -sr "cd ${(q)dir}"
cd "$dir"
fi
local ret=$? local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion unset dir # ensure this doesn't end up appearing in prompt expansion
zle fzf-redraw-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-cd-widget if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bindkey '\ec' fzf-cd-widget 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 # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' | 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..,.. --tiebreak=index --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=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$(awk '{print $1}' <<< "$selected")
if [ -n "$num" ]; then if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
zle vi-fetch-history -n $num zle vi-fetch-history -n ${num%\*}
else # selected is a custom query, not from history
LBUFFER="$selected"
fi fi
fi fi
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' 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 { } always {
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

133
src/actiontype_string.go Normal file
View File

@ -0,0 +1,133 @@
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
package fzf
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[actIgnore-0]
_ = x[actStart-1]
_ = x[actClick-2]
_ = x[actInvalid-3]
_ = x[actChar-4]
_ = x[actMouse-5]
_ = x[actBeginningOfLine-6]
_ = x[actAbort-7]
_ = x[actAccept-8]
_ = x[actAcceptNonEmpty-9]
_ = x[actAcceptOrPrintQuery-10]
_ = x[actBackwardChar-11]
_ = x[actBackwardDeleteChar-12]
_ = x[actBackwardDeleteCharEof-13]
_ = x[actBackwardWord-14]
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = 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 = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
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) {
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
}

View File

@ -80,6 +80,7 @@ Scoring criteria
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@ -89,6 +90,10 @@ import (
var DEBUG bool var DEBUG bool
var delimiterChars = "/,:;|"
const whiteChars = " \t\n\v\f\r\x85\xA0"
func indexAt(index int, max int, forward bool) int { func indexAt(index int, max int, forward bool) int {
if forward { if forward {
return index return index
@ -107,7 +112,7 @@ type Result struct {
const ( const (
scoreMatch = 16 scoreMatch = 16
scoreGapStart = -3 scoreGapStart = -3
scoreGapExtention = -1 scoreGapExtension = -1
// We prefer matches at the beginning of a word, but the bonus should not be // We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over // too great to prevent the longer acronym matches from always winning over
@ -125,31 +130,93 @@ const (
// Edge-triggered bonus for matches in camelCase words. // Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps // Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly. // (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtention bonusCamel123 = bonusBoundary + scoreGapExtension
// Minimum bonus point given to characters in consecutive chunks. // Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we // Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm. // used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtention) bonusConsecutive = -(scoreGapStart + scoreGapExtension)
// The first character in the typed pattern usually has more significance // The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where // than the rest so it's important that it appears at special positions where
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo". // bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is // The amount of the extra bonus should be limited so that the gap penalty is
// still respected. // still respected.
bonusFirstCharMultiplier = 2 bonusFirstCharMultiplier = 2
) )
var (
// Extra bonus for word boundary after whitespace character or beginning of the string
bonusBoundaryWhite int16 = bonusBoundary + 2
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
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 type charClass int
const ( const (
charNonWord charClass = iota charWhite charClass = iota
charNonWord
charDelimiter
charLower charLower
charUpper charUpper
charLetter charLetter
charNumber charNumber
) )
func Init(scheme string) bool {
switch scheme {
case "default":
bonusBoundaryWhite = bonusBoundary + 2
bonusBoundaryDelimiter = bonusBoundary + 1
case "path":
bonusBoundaryWhite = bonusBoundary
bonusBoundaryDelimiter = bonusBoundary + 1
if os.PathSeparator == '/' {
delimiterChars = "/"
} else {
delimiterChars = string([]rune{os.PathSeparator, '/'})
}
initialCharClass = charDelimiter
case "history":
bonusBoundaryWhite = bonusBoundary
bonusBoundaryDelimiter = bonusBoundary
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
}
func posArray(withPos bool, len int) *[]int { func posArray(withPos bool, len int) *[]int {
if withPos { if withPos {
pos := make([]int, 0, len) pos := make([]int, 0, len)
@ -174,17 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
return offset, make([]int32, size) return offset, make([]int32, size)
} }
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass { func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) { if unicode.IsLower(char) {
return charLower return charLower
@ -194,36 +250,56 @@ func charClassOfNonAscii(char rune) charClass {
return charNumber return charNumber
} else if unicode.IsLetter(char) { } else if unicode.IsLetter(char) {
return charLetter return charLetter
} else if unicode.IsSpace(char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
} }
return charNonWord return charNonWord
} }
func charClassOf(char rune) charClass { func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
return charClassOfAscii(char) return asciiCharClasses[char]
} }
return charClassOfNonAscii(char) return charClassOfNonAscii(char)
} }
func bonusFor(prevClass charClass, class charClass) int16 { func bonusFor(prevClass charClass, class charClass) int16 {
if prevClass == charNonWord && class != charNonWord { if class > charNonWord {
// Word boundary switch prevClass {
return bonusBoundary case charWhite:
} else if prevClass == charLower && class == charUpper || // Word boundary after whitespace
return bonusBoundaryWhite
case charDelimiter:
// Word boundary after a delimiter character
return bonusBoundaryDelimiter
case charNonWord:
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber { prevClass != charNumber && class == charNumber {
// camelCase letter123 // camelCase letter123
return bonusCamel123 return bonusCamel123
} else if class == charNonWord { }
switch class {
case charNonWord, charDelimiter:
return bonusNonWord return bonusNonWord
case charWhite:
return bonusBoundaryWhite
} }
return 0 return 0
} }
func bonusAt(input *util.Chars, idx int) int16 { func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 { if idx == 0 {
return bonusBoundary return bonusBoundaryWhite
} }
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
} }
func normalizeRune(r rune) rune { func normalizeRune(r rune) rune {
@ -276,30 +352,45 @@ func isAscii(runes []rune) bool {
return true return true
} }
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
// Can't determine // Can't determine
if !input.IsBytes() { if !input.IsBytes() {
return 0 return 0, input.Length()
} }
// Not possible // Not possible
if !isAscii(pattern) { if !isAscii(pattern) {
return -1 return -1, -1
} }
firstIdx, idx := 0, 0 firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ { for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 { if idx < 0 {
return -1 return -1, -1
} }
if pidx == 0 && idx > 0 { if pidx == 0 && idx > 0 {
// Step back to find the right bonus point // Step back to find the right bonus point
firstIdx = idx - 1 firstIdx = idx - 1
} }
lastIdx = idx
idx++ idx++
} }
return firstIdx
// Find the last appearance of the last character of the pattern to limit the search scope
bu := b
if !caseSensitive && b >= 'a' && b <= 'z' {
bu = b - 32
}
scope := input.Bytes()[lastIdx:]
for offset := len(scope) - 1; offset > 0; offset-- {
if scope[offset] == b || scope[offset] == bu {
return firstIdx, lastIdx + offset + 1
}
}
return firstIdx, lastIdx + 1
} }
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
@ -348,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return Result{0, 0, 0}, posArray(withPos, M) return Result{0, 0, 0}, posArray(withPos, M)
} }
N := input.Length() N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Since O(nm) algorithm can be prohibitively expensive for large input, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
@ -356,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
// Phase 1. Optimized search for ASCII string // Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(input, pattern, caseSensitive) minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 { if minIdx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
N = maxIdx - minIdx
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0 offset16 := 0
@ -372,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
offset32, F := alloc32(offset32, slab, M) offset32, F := alloc32(offset32, slab, M)
// Rune array // Rune array
_, T := alloc32(offset32, slab, N) _, T := alloc32(offset32, slab, N)
input.CopyRunes(T) input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point // Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0 maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0 pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:] for off, char := range T {
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
var class charClass var class charClass
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
class = charClassOfAscii(char) class = asciiCharClasses[char]
if !caseSensitive && class == charUpper { if !caseSensitive && class == charUpper {
char += 32 char += 32
T[off] = char
} }
} else { } else {
class = charClassOfNonAscii(char) class = charClassOfNonAscii(char)
@ -395,53 +490,53 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if normalize { if normalize {
char = normalizeRune(char) char = normalizeRune(char)
} }
T[off] = char
} }
Tsub[off] = char bonus := bonusMatrix[prevClass][class]
bonus := bonusFor(prevClass, class) B[off] = bonus
Bsub[off] = bonus
prevClass = class prevClass = class
if char == pchar { if char == pchar {
if pidx < M { if pidx < M {
F[pidx] = int32(idx + off) F[pidx] = int32(off)
pidx++ pidx++
pchar = pattern[util.Min(pidx, M-1)] pchar = pattern[util.Min(pidx, M-1)]
} }
lastIdx = idx + off lastIdx = off
} }
if char == pchar0 { if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score H0[off] = score
C0sub[off] = 1 C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off maxScore, maxScorePos = score, off
if forward && bonus == bonusBoundary { if forward && bonus >= bonusBoundary {
break break
} }
} }
inGap = false inGap = false
} else { } else {
if inGap { if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0) H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else { } else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) H0[off] = util.Max16(prevH0+scoreGapStart, 0)
} }
C0sub[off] = 0 C0[off] = 0
inGap = true inGap = true
} }
prevH0 = H0sub[off] prevH0 = H0[off]
} }
if pidx != M { if pidx != M {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if M == 1 { if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
if !withPos { if !withPos {
return result, nil return result, nil
} }
pos := []int{maxScorePos} pos := []int{minIdx + maxScorePos}
return result, &pos return result, &pos
} }
@ -477,7 +572,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
var s1, s2, consecutive int16 var s1, s2, consecutive int16
if inGap { if inGap {
s2 = Hleft[off] + scoreGapExtention s2 = Hleft[off] + scoreGapExtension
} else { } else {
s2 = Hleft[off] + scoreGapStart s2 = Hleft[off] + scoreGapStart
} }
@ -486,11 +581,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
s1 = Hdiag[off] + scoreMatch s1 = Hdiag[off] + scoreMatch
b := Bsub[off] b := Bsub[off]
consecutive = Cdiag[off] + 1 consecutive = Cdiag[off] + 1
// Break consecutive chunk if consecutive > 1 {
if b == bonusBoundary { fb := B[col-int(consecutive)+1]
consecutive = 1 // Break consecutive chunk
} else if consecutive > 1 { if b >= bonusBoundary && b > fb {
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1])) consecutive = 1
} else {
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
}
} }
if s1+b < s2 { if s1+b < s2 {
s1 += Bsub[off] s1 += Bsub[off]
@ -535,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
if s > s1 && (s > s2 || s == s2 && preferMatch) { if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j) *pos = append(*pos, j+minIdx)
if i == 0 { if i == 0 {
break break
} }
@ -548,14 +646,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Start offset we return here is only relevant when begin tiebreak is used. // Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't // However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance. // want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
} }
// Implement the same sorting criteria as V2 // Implement the same sorting criteria as V2
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern)) pos := posArray(withPos, len(pattern))
prevClass := charNonWord prevClass := initialCharClass
if sidx > 0 { if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1)) prevClass = charClassOf(text.Get(sidx - 1))
} }
@ -578,12 +676,12 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
*pos = append(*pos, idx) *pos = append(*pos, idx)
} }
score += scoreMatch score += scoreMatch
bonus := bonusFor(prevClass, class) bonus := bonusMatrix[prevClass][class]
if consecutive == 0 { if consecutive == 0 {
firstBonus = bonus firstBonus = bonus
} else { } else {
// Break consecutive chunk // Break consecutive chunk
if bonus == bonusBoundary { if bonus >= bonusBoundary && bonus > firstBonus {
firstBonus = bonus firstBonus = bonus
} }
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive) bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
@ -598,7 +696,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
pidx++ pidx++
} else { } else {
if inGap { if inGap {
score += scoreGapExtention score += scoreGapExtension
} else { } else {
score += scoreGapStart score += scoreGapStart
} }
@ -616,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
@ -710,7 +809,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
@ -741,7 +841,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
if bonus > bestBonus { if bonus > bestBonus {
bestPos, bestBonus = index, bonus bestPos, bestBonus = index, bonus
} }
if bonus == bonusBoundary { if bonus >= bonusBoundary {
break break
} }
index -= pidx - 1 index -= pidx - 1
@ -877,8 +977,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
match = runesStr == string(pattern) match = runesStr == string(pattern)
} }
if match { if match {
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern + return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil (bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil
} }
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }

View File

@ -9,6 +9,10 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func init() {
Init("default")
}
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
} }
@ -43,46 +47,46 @@ func TestFuzzyMatch(t *testing.T) {
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} { for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
for _, forward := range []bool{true, false} { for _, forward := range []bool{true, false} {
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9, assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9, assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) int(bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13, assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2) scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10, assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13, assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter))
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10, assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention) scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10, assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention) scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9, assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7, assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention) bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8, assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+
scoreGapStart*2+scoreGapExtention*3) scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4, assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6, assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+ scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary) bonusNonWord+bonusBoundary)
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9, assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9, assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*2+
scoreGapStart*2+scoreGapExtention*4) scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7, assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+ scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtention*2) scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4, assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+ scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
util.Max(bonusCamel123, bonusBoundary)) util.Max(bonusCamel123, int(bonusBoundaryWhite)))
// Consecutive bonus updated // Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6, assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
@ -98,10 +102,10 @@ func TestFuzzyMatch(t *testing.T) {
func TestFuzzyMatchBackward(t *testing.T) { func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4, assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+ scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtention) scoreGapStart+scoreGapExtension)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9, assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary) scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite))
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
@ -114,9 +118,9 @@ func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2) scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3)) scoreMatch*4+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4)) scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+int(bonusBoundaryDelimiter))
} }
} }
@ -128,7 +132,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1) score := scoreMatch*3 + int(bonusBoundaryWhite)*bonusFirstCharMultiplier + int(bonusBoundaryWhite)*2
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
@ -156,9 +160,10 @@ func TestSuffixMatch(t *testing.T) {
// Strip trailing white space from the string // Strip trailing white space from the string
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9, assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
scoreMatch*3+bonusConsecutive*2) scoreMatch*3+bonusConsecutive*2)
// Only when the pattern doesn't end with a space // Only when the pattern doesn't end with a space
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10, assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
scoreMatch*4+bonusConsecutive*2+bonusNonWord) scoreMatch*4+bonusConsecutive*2+int(bonusBoundaryWhite))
} }
} }
@ -182,9 +187,9 @@ func TestNormalize(t *testing.T) {
input, pattern, sidx, eidx, score) input, pattern, sidx, eidx, score)
} }
} }
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2) test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
} }
func TestLongString(t *testing.T) { func TestLongString(t *testing.T) {

View File

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

View File

@ -1,8 +1,6 @@
package fzf package fzf
import ( import (
"bytes"
"regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -57,6 +55,9 @@ func (s *ansiState) ToString() string {
if s.attr&tui.Reverse > 0 { if s.attr&tui.Reverse > 0 {
ret += "7;" ret += "7;"
} }
if s.attr&tui.StrikeThrough > 0 {
ret += "9;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40) ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m" return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
@ -82,73 +83,161 @@ func toAnsiString(color tui.Color, offset int) string {
return ret + ";" return ret + ";"
} }
var ansiRegex *regexp.Regexp func isPrint(c uint8) bool {
return '\x20' <= c && c <= '\x7e'
func init() {
/*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
} }
func findAnsiStart(str string) int { func matchOperatingSystemCommand(s string) int {
idx := 0 // `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
for ; idx < len(str); idx++ { // ^ match starting here
b := str[idx] //
if b == 0x1b || b == 0x0e || b == 0x0f { i := 5 // prefix matched in nextAnsiEscapeSequence()
return idx for ; i < len(s) && isPrint(s[i]); i++ {
}
if i < len(s) {
if s[i] == '\x07' {
return i + 1
} }
if b == 0x08 && idx > 0 { if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
return idx - 1 return i + 2
} }
} }
return idx return -1
}
func matchControlSequence(s string) int {
// `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
// ^ match starting here
//
i := 2 // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s); i++ {
c := s[i]
switch c {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ';', ':', '?':
// ok
default:
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
return i + 1
}
return -1
}
}
return -1
}
func isCtrlSeqStart(c uint8) bool {
switch c {
case '\\', '[', '(', ')':
return true
}
return false
}
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used):
//
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences
i := 0
for ; i < len(s); i++ {
switch s[i] {
case '\x0e', '\x0f', '\x1b', '\x08':
// We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of
// speed and simplicity.
goto Loop
}
}
return -1, -1
Loop:
for ; i < len(s); i++ {
switch s[i] {
case '\x08':
// backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' {
if s[i-1] < utf8.RuneSelf {
return i - 1, i + 1
}
_, n := utf8.DecodeLastRuneInString(s[:i])
return i - n, i + 1
}
case '\x1b':
// match: `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
if j := matchControlSequence(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b.`
if i+1 < len(s) && s[i+1] != '\n' {
if s[i+1] < utf8.RuneSelf {
return i, i + 2
}
_, n := utf8.DecodeRuneInString(s[i+1:])
return i, i + n + 1
}
case '\x0e', '\x0f':
// match: `[\x0e\x0f]`
return i, i + 1
}
}
return -1, -1
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset // We append to a stack allocated variable that we'll
var output bytes.Buffer // later copy and return, to save on allocations.
offsets := make([]ansiOffset, 0, 32)
if state != nil { if state != nil {
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
} }
prevIdx := 0 var (
runeCount := 0 pstate *ansiState // lazily allocated
output strings.Builder
prevIdx int
runeCount int
)
for idx := 0; idx < len(str); { for idx := 0; idx < len(str); {
idx += findAnsiStart(str[idx:]) // Make sure that we found an ANSI code
if idx == len(str) { start, end := nextAnsiEscapeSequence(str[idx:])
if start == -1 {
break break
} }
start += idx
// Make sure that we found an ANSI code idx += end
offset := ansiRegex.FindStringIndex(str[idx:])
if len(offset) < 2 {
idx++
continue
}
offset[0] += idx
offset[1] += idx
idx = offset[1]
// Check if we should continue // Check if we should continue
prev := str[prevIdx:offset[0]] prev := str[prevIdx:start]
if proc != nil && !proc(prev, state) { if proc != nil && !proc(prev, state) {
return "", nil, nil return "", nil, nil
} }
prevIdx = idx
prevIdx = offset[1] if len(prev) != 0 {
runeCount += utf8.RuneCountInString(prev) runeCount += utf8.RuneCountInString(prev)
output.WriteString(prev) // Grow the buffer size to the maximum possible length (string length
// containing ansi codes) to avoid repetitive allocation
if output.Cap() == 0 {
output.Grow(len(str))
}
output.WriteString(prev)
}
newState := interpretCode(str[offset[0]:offset[1]], state) newState := interpretCode(str[start:idx], state)
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
@ -157,8 +246,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if newState.colored() { if newState.colored() {
// Append new offset // Append new offset
state = newState if pstate == nil {
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state}) pstate = &ansiState{}
}
*pstate = newState
state = pstate
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
newState,
})
} else { } else {
// Discard state // Discard state
state = nil state = nil
@ -168,7 +264,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
var rest string var rest string
var trimmed string var trimmed string
if prevIdx == 0 { if prevIdx == 0 {
// No ANSI code found // No ANSI code found
rest = str rest = str
@ -178,51 +273,89 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(rest) output.WriteString(rest)
trimmed = output.String() trimmed = output.String()
} }
if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if proc != nil { if proc != nil {
proc(rest, state) proc(rest, state)
} }
if len(offsets) == 0 { if len(offsets) > 0 {
return trimmed, nil, state if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
// Return a copy of the offsets slice
a := make([]ansiOffset, len(offsets))
copy(a, offsets)
return trimmed, &a, state
} }
return trimmed, &offsets, state return trimmed, nil, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// State var remaining string
var state *ansiState var i int
if prevState == nil { if delimiter == 0 {
state = &ansiState{-1, -1, 0, -1} // Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
}
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg} i = strings.IndexByte(s, delimiter)
}
if i >= 0 {
delimiter = s[i]
remaining = s[i+1:]
s = s[:i]
}
if len(s) > 0 {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
}
code = code*10 + int(ch)
}
return code, delimiter, remaining
}
return -1, delimiter, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
var state ansiState
if prevState == nil {
state = ansiState{-1, -1, 0, -1}
} else {
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
} }
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if strings.HasSuffix(ansiCode, "0K") { if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
state.lbg = prevState.bg state.lbg = prevState.bg
} }
return state return state
} }
ptr := &state.fg if len(ansiCode) <= 3 {
state256 := 0
init := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.attr = 0 state.attr = 0
state256 = 0 return state
} }
ansiCode = ansiCode[2 : len(ansiCode)-1] ansiCode = ansiCode[2 : len(ansiCode)-1]
if len(ansiCode) == 0 {
init() state256 := 0
} ptr := &state.fg
for _, code := range strings.Split(ansiCode, ";") {
if num, err := strconv.Atoi(code); err == nil { var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
count++
switch state256 { switch state256 {
case 0: case 0:
switch num { switch num {
@ -248,12 +381,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
state.attr = state.attr | tui.Blink state.attr = state.attr | tui.Blink
case 7: case 7:
state.attr = state.attr | tui.Reverse state.attr = state.attr | tui.Reverse
case 9:
state.attr = state.attr | tui.StrikeThrough
case 22:
state.attr = state.attr &^ tui.Bold
state.attr = state.attr &^ tui.Dim
case 23: // tput rmso case 23: // tput rmso
state.attr = state.attr &^ tui.Italic state.attr = state.attr &^ tui.Italic
case 24: // tput rmul case 24: // tput rmul
state.attr = state.attr &^ tui.Underline state.attr = state.attr &^ tui.Underline
case 25:
state.attr = state.attr &^ tui.Blink
case 27:
state.attr = state.attr &^ tui.Reverse
case 29:
state.attr = state.attr &^ tui.StrikeThrough
case 0: case 0:
init() state.fg = -1
state.bg = -1
state.attr = 0
state256 = 0
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
state.fg = tui.Color(num - 30) state.fg = tui.Color(num - 30)
@ -289,6 +436,14 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
} }
} }
} }
// Empty sequence: reset
if count == 0 {
state.fg = -1
state.bg = -1
state.attr = 0
}
if state256 > 0 { if state256 > 0 {
*ptr = -1 *ptr = -1
} }

View File

@ -1,13 +1,192 @@
package fzf package fzf
import ( import (
"fmt" "math/rand"
"regexp"
"strings" "strings"
"testing" "testing"
"unicode/utf8"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
) )
// The following regular expression will include not all but most of the
// frequently used ANSI sequences. This regex is used as a reference for
// testing nextAnsiEscapeSequence().
//
// References:
// - https://github.com/gnachman/iTerm2
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
func testParserReference(t testing.TB, str string) {
t.Helper()
toSlice := func(start, end int) []int {
if start == -1 {
return nil
}
return []int{start, end}
}
s := str
for i := 0; ; i++ {
got := toSlice(nextAnsiEscapeSequence(s))
exp := ansiRegexReference.FindStringIndex(s)
equal := len(got) == len(exp)
if equal {
for i := 0; i < len(got); i++ {
if got[i] != exp[i] {
equal = false
break
}
}
}
if !equal {
var exps, gots []rune
if len(got) == 2 {
gots = []rune(s[got[0]:got[1]])
}
if len(exp) == 2 {
exps = []rune(s[exp[0]:exp[1]])
}
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
return
}
if len(exp) == 0 {
return
}
s = s[exp[1]:]
}
}
func TestNextAnsiEscapeSequence(t *testing.T) {
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
"\x1b椙",
"椙\x08",
"\n\x08",
"X\x08",
"",
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
ansiBenchmarkString,
}
for _, s := range testStrs {
testParserReference(t, s)
}
}
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
ansiBenchmarkString,
}
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
modifyString := func(s string, rr *rand.Rand) string {
n := rr.Intn(len(s))
b := []rune(s)
for ; n >= 0 && len(b) != 0; n-- {
i := rr.Intn(len(b))
switch x := rr.Intn(4); x {
case 0:
b = append(b[:i], b[i+1:]...)
case 1:
j := rr.Intn(len(replacementBytes) - 1)
b[i] = replacementBytes[j]
case 2:
x := rune(rr.Intn(utf8.MaxRune))
for !utf8.ValidRune(x) {
x = rune(rr.Intn(utf8.MaxRune))
}
b[i] = x
case 3:
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
default:
t.Fatalf("unsupported value: %d", x)
}
}
return string(b)
}
rr := rand.New(rand.NewSource(1))
for _, s := range testStrs {
for i := 1_000; i >= 0; i-- {
testParserReference(t, modifyString(s, rr))
}
}
}
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
randomString := func(rr *rand.Rand) string {
numChars := rand.Intn(50)
codePoints := make([]rune, numChars)
for i := 0; i < len(codePoints); i++ {
var r rune
for n := 0; n < 1000; n++ {
r = rune(rr.Intn(utf8.MaxRune))
// Allow 10% of runes to be invalid
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
break
}
}
codePoints[i] = r
}
return string(codePoints)
}
rr := rand.New(rand.NewSource(1))
for i := 0; i < 100_000; i++ {
testParserReference(t, randomString(rr))
}
}
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr tui.Attr var attr tui.Attr
@ -29,7 +208,7 @@ func TestExtractColor(t *testing.T) {
if output != "hello world" { if output != "hello world" {
t.Errorf("Invalid output: %s %v", output, []rune(output)) t.Errorf("Invalid output: %s %v", output, []rune(output))
} }
fmt.Println(src, ansiOffsets, clean) t.Log(src, ansiOffsets, clean)
assertion(ansiOffsets, state) assertion(ansiOffsets, state)
} }
@ -163,12 +342,15 @@ func TestAnsiCodeStringConversion(t *testing.T) {
state := interpretCode(code, prevState) state := interpretCode(code, prevState)
if expected != state.ToString() { if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s", t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1), strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1)) strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
} }
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "") assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m") assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m") assert("\x1b[41m", nil, "\x1b[39;41m")
@ -179,9 +361,71 @@ func TestAnsiCodeStringConversion(t *testing.T) {
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m") assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m") assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m") assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m") assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m") assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;7m", assert("\x1b[48;5;100;38;2;10;20;30;7m",
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1}, &ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m") "\x1b[2;3;7;38;2;10;20;30;48;5;100m")
} }
func TestParseAnsiCode(t *testing.T) {
tests := []struct {
In, Exp string
N int
}{
{"123", "", 123},
{"1a", "", -1},
{"1a;12", "12", -1},
{"12;a", "a", 12},
{"-2", "", -1},
}
for _, x := range tests {
n, _, s := parseAnsiCode(x.In, 0)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
}
}
}
// kernel/bpf/preload/iterators/README
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
"\x1b[0m\x1b[38:5:149mMakefile\x1b[m\x1b[K\x1b[0m"
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
_, o := nextAnsiEscapeSequence(s)
if o == -1 {
break
}
s = s[o:]
}
}
}
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
// previously used regex based implementation.
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
a := ansiRegexReference.FindStringIndex(s)
if len(a) == 0 {
break
}
s = s[a[1]:]
}
}
}
func BenchmarkExtractColor(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
extractColor(ansiBenchmarkString, nil, nil)
}
}

View File

@ -12,8 +12,14 @@ type ChunkCache struct {
} }
// NewChunkCache returns a new ChunkCache // NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache { func NewChunkCache() *ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} 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 // Add adds the list to the cache

View File

@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
cache := NewChunkCache() cache := NewChunkCache()
chunk1p := &Chunk{} chunk1p := &Chunk{}
chunk2p := &Chunk{count: chunkSize} chunk2p := &Chunk{count: chunkSize}
items1 := []Result{Result{}} items1 := []Result{{}}
items2 := []Result{Result{}, Result{}} items2 := []Result{{}, {}}
cache.Add(chunk1p, "foo", items1) cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2) cache.Add(chunk2p, "bar", items2)

View File

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

View File

@ -1,33 +1,8 @@
/* // Package fzf implements fzf, a command-line fuzzy finder.
Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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.
*/
package fzf package fzf
import ( import (
"fmt" "sync"
"os"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@ -43,19 +18,23 @@ Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options) (int, error) {
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
}
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
os.Exit(exitOk)
}
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
@ -69,16 +48,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored { if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState lineAnsiState = newState
return util.ToChars([]byte(trimmed)), offsets return util.ToChars(stringBytes(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars([]byte(trimmed)), nil return util.ToChars(stringBytes(trimmed)), nil
} }
} }
} }
@ -90,7 +69,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(data)) header = append(header, byteString(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
@ -101,7 +80,7 @@ func Run(opts *Options, version string, revision string) {
}) })
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(string(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
if prevLineAnsiState != nil { if prevLineAnsiState != nil {
@ -125,7 +104,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor([]byte(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces() item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
@ -134,33 +113,42 @@ func Run(opts *Options, version string, revision string) {
}) })
} }
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader var reader *Reader
if !streamingFilter { if !streamingFilter {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource() go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
// Matcher // Matcher
forward := true forward := true
for _, cri := range opts.Criteria[1:] { withPos := false
if cri == byEnd { for idx := len(opts.Criteria) - 1; idx > 0; idx-- {
switch opts.Criteria[idx] {
case byChunk:
withPos = true
case byEnd:
forward = false forward = false
break case byBegin:
} forward = true
if cri == byBegin {
break
} }
} }
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@ -174,18 +162,21 @@ func Run(opts *Options, version string, revision string) {
found := false found := false
if streamingFilter { if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size) slab := util.MakeSlab(slab16Size, slab32Size)
mutex := sync.Mutex{}
reader := NewReader( reader := NewReader(
func(runes []byte) bool { func(runes []byte) bool {
item := Item{} item := Item{}
if chunkList.trans(&item, runes) { if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString()) opts.Printer(item.text.ToString())
found = true found = true
} }
mutex.Unlock()
} }
return false return false
}, eventBox, opts.ReadZero, false) }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource() reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
@ -200,9 +191,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
os.Exit(exitOk) return ExitOk, nil
} }
os.Exit(exitNoMatch) return ExitNoMatch, nil
} }
// Synchronous search // Synchronous search
@ -213,31 +204,61 @@ func Run(opts *Options, version string, revision string) {
// Go interactive // Go interactive
go matcher.Loop() go matcher.Loop()
defer matcher.Stop()
// Terminal I/O // 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
if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad()
}
deferred := opts.Select1 || opts.Exit0 deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()
if !deferred { if !deferred && !heightUnknown {
terminal.startChan <- true // Start right away
terminal.startChan <- fitpad{-1, -1}
} }
// Event coordination // Event coordination
reading := true reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *commandSpec
restart := func(command string) { var nextEnviron []string
reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
total := 0
query := []rune{} query := []rune{}
determine := func(final bool) {
if heightUnknown {
if total >= maxFit || final {
deferred = false
heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
}
} else if deferred {
deferred = false
terminal.startChan <- fitpad{-1, -1}
}
}
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command commandSpec, environ []string) {
reading = true
chunkList.Clear()
itemIndex = 0
inputRevision++
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ)
}
exitCode := ExitOk
stop := false
for { for {
delay := true delay := true
ticks++ ticks++
@ -254,41 +275,83 @@ func Run(opts *Options, version string, revision string) {
} }
for evt, value := range *events { for evt, value := range *events {
switch evt { switch evt {
case EvtQuit:
if reading {
reader.terminate()
}
quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand) restart(*nextCommand, nextEnviron)
nextCommand = nil nextCommand = nil
nextEnviron = nil
break break
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
snapshot, count := chunkList.Snapshot() if useSnapshot && evt == EvtReadFin {
terminal.UpdateCount(count, !reading, value.(*string)) useSnapshot = false
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
} }
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache()) if !useSnapshot {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
}
if heightUnknown && !deferred {
determine(!reading)
}
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *commandSpec
var environ []string
var changed bool
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
environ = val.environ
changed = val.changed
if command != nil {
useSnapshot = val.sync
}
} }
if command != nil { if command != nil {
if reading { if reading {
reader.terminate() reader.terminate()
nextCommand = command nextCommand = command
nextEnviron = environ
} else { } else {
restart(*command) restart(*command, environ)
} }
}
if !changed {
break break
} }
snapshot, _ := chunkList.Snapshot() if !useSnapshot {
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache()) 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 || newCount > 0 {
if snapshotRevision != inputRevision {
query = []rune{}
}
snapshot = newSnapshot
snapshotRevision = inputRevision
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@ -308,8 +371,7 @@ func Run(opts *Options, version string, revision string) {
if deferred { if deferred {
count := val.Length() count := val.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
deferred = false determine(val.final)
terminal.startChan <- true
} else if val.final { } else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery { if opts.PrintQuery {
@ -321,21 +383,24 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count == 0 {
os.Exit(exitOk) exitCode = ExitNoMatch
} }
os.Exit(exitNoMatch) stop = true
return
} }
deferred = false determine(val.final)
terminal.startChan <- true
} }
} }
terminal.UpdateList(val, clearSelection()) terminal.UpdateList(val, true)
} }
} }
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks)*coordinatorDelayStep,
@ -343,4 +408,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur) time.Sleep(dur)
} }
} }
return exitCode, err
} }

35
src/functions.go Normal file
View File

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

View File

@ -2,7 +2,6 @@ package fzf
import ( import (
"errors" "errors"
"io/ioutil"
"os" "os"
"strings" "strings"
) )
@ -26,12 +25,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
} }
// Read history file // Read history file
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
// If it doesn't exist, check if we can create a file with the name // If it doesn't exist, check if we can create a file with the name
if os.IsNotExist(err) { if os.IsNotExist(err) {
data = []byte{} data = []byte{}
if err := ioutil.WriteFile(path, data, 0600); err != nil { if err := os.WriteFile(path, data, 0600); err != nil {
return nil, fmtError(err) return nil, fmtError(err)
} }
} else { } else {
@ -62,11 +61,11 @@ func (h *History) append(line string) error {
lines = lines[len(lines)-h.maxSize:] lines = lines[len(lines)-h.maxSize:]
} }
h.lines = append(lines, "") h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) return os.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
} }
func (h *History) override(str string) { func (h *History) override(str string) {
// You can update the history but they're not written to the file // You can update the history, but they're not written to the file
if h.cursor == len(h.lines)-1 { if h.cursor == len(h.lines)-1 {
h.lines[h.cursor] = str h.lines[h.cursor] = str
} else if h.cursor < len(h.lines)-1 { } else if h.cursor < len(h.lines)-1 {

View File

@ -1,9 +1,7 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os" "os"
"os/user"
"runtime" "runtime"
"testing" "testing"
) )
@ -12,16 +10,12 @@ func TestHistory(t *testing.T) {
maxHistory := 50 maxHistory := 50
// Invalid arguments // Invalid arguments
user, _ := user.Current()
var paths []string var paths []string
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// GOPATH should exist, so we shouldn't be able to override it // GOPATH should exist, so we shouldn't be able to override it
paths = []string{os.Getenv("GOPATH")} paths = []string{os.Getenv("GOPATH")}
} else { } else {
paths = []string{"/etc", "/proc"} paths = []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
} }
for _, path := range paths { for _, path := range paths {
@ -30,7 +24,7 @@ func TestHistory(t *testing.T) {
} }
} }
f, _ := ioutil.TempFile("", "fzf-history") f, _ := os.CreateTemp("", "fzf-history")
f.Close() f.Close()
{ // Append lines { // Append lines

View File

@ -12,15 +12,16 @@ import (
// MatchRequest represents a search request // MatchRequest represents a search request
type MatchRequest struct { type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool revision int
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
type Matcher struct { type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool tac bool
@ -29,6 +30,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int
} }
const ( const (
@ -37,10 +39,11 @@ const (
) )
// NewMatcher returns a new Matcher // 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) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache,
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
@ -48,7 +51,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: partitions, partitions: partitions,
slab: make([]*util.Slab, partitions), slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger),
revision: revision}
} }
// Loop puts Matcher in action // Loop puts Matcher in action
@ -58,8 +62,13 @@ func (m *Matcher) Loop() {
for { for {
var request MatchRequest var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) { 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) { switch val := val.(type) {
case MatchRequest: case MatchRequest:
request = val request = val
@ -69,11 +78,15 @@ func (m *Matcher) Loop() {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if request.sort != m.sort || request.clearCache { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() m.cache.Clear()
} }
// Restart search // Restart search
@ -140,11 +153,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
numChunks := len(request.chunks) numChunks := len(request.chunks)
if numChunks == 0 { if numChunks == 0 {
return EmptyMerger, false return EmptyMerger(request.revision), false
} }
pattern := request.pattern pattern := request.pattern
if pattern.IsEmpty() { if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
@ -218,11 +231,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac), false return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@ -231,5 +244,9 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
}
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
} }

View File

@ -3,30 +3,36 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false) func EmptyMerger(revision int) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision)
}
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
type Merger struct { type Merger struct {
pattern *Pattern pattern *Pattern
lists [][]Result lists [][]Result
merged []Result merged []Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
tac bool tac bool
final bool final bool
count int count int
pass bool
revision int
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0} count: 0,
pass: true,
revision: revision}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@ -35,17 +41,18 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
merged: []Result{}, merged: []Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
tac: tac, tac: tac,
final: false, final: false,
count: 0} count: 0,
revision: revision}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@ -53,11 +60,42 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
return &mg return &mg
} }
// Revision returns revision number
func (mg *Merger) Revision() int {
return mg.revision
}
// Length returns the number of items // Length returns the number of items
func (mg *Merger) Length() int { func (mg *Merger) Length() int {
return mg.count return mg.count
} }
func (mg *Merger) First() Result {
if mg.tac && !mg.sorted {
return mg.Get(mg.count - 1)
}
return mg.Get(0)
}
// FindIndex returns the index of the item with the given item index
func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1
if mg.pass {
index = int(itemIndex)
if mg.tac {
index = mg.count - index - 1
}
} else {
for i := 0; i < mg.count; i++ {
if mg.Get(i).item.Index() == itemIndex {
index = i
break
}
}
}
return index
}
// Get returns the pointer to the Result object indexed by the given integer // Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) Result { func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil { if mg.chunks != nil {
@ -106,7 +144,6 @@ func (mg *Merger) mergedGet(idx int) Result {
minIdx = listIdx minIdx = listIdx
} }
} }
mg.cursors[listIdx] = cursor
} }
if minIdx >= 0 { if minIdx >= 0 {

View File

@ -23,10 +23,10 @@ func randResult() Result {
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
assert(t, EmptyMerger.Length() == 0, "Not empty") assert(t, EmptyMerger(0).Length() == 0, "Not empty")
assert(t, EmptyMerger.count == 0, "Invalid count") assert(t, EmptyMerger(0).count == 0, "Invalid count")
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]Result, []Result) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false) mg := NewMerger(nil, lists, false, false, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(nil, lists, true, false) mg := NewMerger(nil, lists, true, false, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false) mg2 := NewMerger(nil, lists, true, false, 0)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

File diff suppressed because it is too large Load Diff

13
src/options_no_pprof.go Normal file
View File

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

73
src/options_pprof.go Normal file
View File

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

89
src/options_pprof_test.go Normal file
View File

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

View File

@ -2,7 +2,7 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
@ -65,9 +65,22 @@ func TestDelimiterRegexRegex(t *testing.T) {
} }
} }
func TestDelimiterRegexRegexCaret(t *testing.T) {
delim := delimiterRegexp(`(^\s*|\s+)`)
tokens := Tokenize("foo bar baz", delim)
if delim.str != nil ||
len(tokens) != 4 ||
tokens[0].text.ToString() != "" ||
tokens[1].text.ToString() != "foo " ||
tokens[2].text.ToString() != "bar " ||
tokens[3].text.ToString() != "baz" {
t.Errorf("%s %d", tokens, len(tokens))
}
}
func TestSplitNth(t *testing.T) { func TestSplitNth(t *testing.T) {
{ {
ranges := splitNth("..") ranges, _ := splitNth("..")
if len(ranges) != 1 || if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis || ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis { ranges[0].end != rangeEllipsis {
@ -75,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 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@ -102,7 +115,7 @@ func TestIrrelevantNth(t *testing.T) {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
} }
} }
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
@ -124,7 +137,7 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(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) { checkEvent := func(e tui.Event, s string) {
if pairs[e] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s) t.Errorf("%s != %s", pairs[e], s)
@ -150,35 +163,35 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE") checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms // 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 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
check(tui.CtrlM, "Return") check(tui.CtrlM, "Return")
checkEvent(tui.Key(' '), "space") checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab") check(tui.Tab, "tab")
check(tui.BTab, "btab") check(tui.ShiftTab, "btab")
check(tui.ESC, "esc") check(tui.Esc, "esc")
check(tui.Up, "up") check(tui.Up, "up")
check(tui.Down, "down") check(tui.Down, "down")
check(tui.Left, "left") check(tui.Left, "left")
check(tui.Right, "right") 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 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(tui.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(tui.PgUp, "page-up") check(tui.PageUp, "page-up")
check(tui.PgDn, "Page-Down") check(tui.PageDown, "Page-Down")
check(tui.Home, "Home") check(tui.Home, "Home")
check(tui.End, "End") check(tui.End, "End")
check(tui.AltBS, "Alt-BSpace") check(tui.AltBackspace, "Alt-BSpace")
check(tui.SLeft, "shift-left") check(tui.ShiftLeft, "shift-left")
check(tui.SRight, "shift-right") check(tui.ShiftRight, "shift-right")
check(tui.BTab, "shift-tab") check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(tui.BSpace, "bspace") check(tui.Backspace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
@ -193,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) {
} }
} }
pairs := parseKeyChords(",", "") pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "") pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "") pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "") pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "") pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c") check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "") pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "") pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,") check(pairs, tui.AltKey(','), "ALT-,")
} }
@ -251,7 +264,7 @@ func TestBind(t *testing.T) {
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "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 (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
@ -283,7 +296,7 @@ func TestBind(t *testing.T) {
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := tui.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark, _ := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
@ -291,7 +304,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
light := parseTheme(theme, "dark,light") light, _ := parseTheme(theme, "dark,light")
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
@ -302,7 +315,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") 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 { if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@ -315,7 +328,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) 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 { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@ -337,14 +350,14 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
f, _ := ioutil.TempFile("", "fzf-history") f, _ := os.CreateTemp("", "fzf-history")
f.Close() f.Close()
hist := "--history=" + f.Name() hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPreviousHistory) check([]string{hist}, tui.CtrlP, actPrevHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
@ -384,23 +397,23 @@ func TestPreviewOpts(t *testing.T) {
opts.Preview.size.size == 50) { opts.Preview.size.size == 50) {
t.Error() t.Error()
} }
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap:+{1}-/2") opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
if !(opts.Preview.command == "cat {}" && if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true && opts.Preview.hidden == true &&
opts.Preview.wrap == true && opts.Preview.wrap == true &&
opts.Preview.position == posLeft && opts.Preview.position == posLeft &&
opts.Preview.scroll == "{1}-/2" && opts.Preview.scroll == "+{1}-/2" &&
opts.Preview.size.percent == false && opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) { opts.Preview.size.size == 15) {
t.Error(opts.Preview) t.Error(opts.Preview)
} }
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle") opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
if !(opts.Preview.command == "" && if !(opts.Preview.command == "" &&
opts.Preview.hidden == true && opts.Preview.hidden == true &&
opts.Preview.wrap == true && opts.Preview.wrap == true &&
opts.Preview.cycle == true && opts.Preview.cycle == true &&
opts.Preview.position == posDown && opts.Preview.position == posDown &&
opts.Preview.scroll == "{1}-/2" && opts.Preview.scroll == "+{1}+3-1-2/2" &&
opts.Preview.size.percent == false && opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) { opts.Preview.size.size == 15) {
t.Error(opts.Preview.size.size) t.Error(opts.Preview.size.size)
@ -440,8 +453,6 @@ func TestValidateSign(t *testing.T) {
{"😀", true}, {"😀", true},
{"", false}, {"", false},
{">>>", false}, {">>>", false},
{"\n", false},
{"\t", false},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@ -455,3 +466,35 @@ func TestValidateSign(t *testing.T) {
} }
} }
} }
func TestParseSingleActionList(t *testing.T) {
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))
}
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
t.Errorf("Invalid action parsed: %v", actions[0])
}
if actions[1].t != actUp || actions[2].t != actUp {
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
}
if actions[3].t != actReload || actions[3].a != "down+down" {
t.Errorf("Invalid action parsed: %v", actions[3])
}
}
func TestParseSingleActionListError(t *testing.T) {
_, err := parseSingleActionList("change-query(foobar)baz")
if err == nil {
t.Errorf("Failed to detect error")
}
}
func TestMaskActionContents(t *testing.T) {
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
expected := ":execute +change-query +up,x:reload "
masked := maskActionContents(original)
if masked != expected {
t.Errorf("Not masked: %s", masked)
}
}

View File

@ -51,6 +51,7 @@ type Pattern struct {
caseSensitive bool caseSensitive bool
normalize bool normalize bool
forward bool forward bool
withPos bool
text []rune text []rune
termSets []termSet termSets []termSet
sortable bool sortable bool
@ -59,33 +60,18 @@ type Pattern struct {
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache
} }
var ( var _splitRegex *regexp.Regexp
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
func init() { func init() {
_splitRegex = regexp.MustCompile(" +") _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 // 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,
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
if extended { if extended {
@ -97,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes) 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 { if found {
return cached return cached
} }
@ -145,12 +133,14 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
normalize: normalize, normalize: normalize,
forward: forward, forward: forward,
withPos: withPos,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
sortable: sortable, sortable: sortable,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@ -160,19 +150,19 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr patternCache[asString] = ptr
return ptr return ptr
} }
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { 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) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
switchSet := false switchSet := false
afterBar := false afterBar := false
for _, token := range tokens { 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) lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect || caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText caseMode == CaseSmart && text != lowerText
@ -207,11 +197,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// Flip exactness // Flip exactness
if fuzzy && !inv { if fuzzy && !inv {
typ = termExact typ = termExact
text = text[1:]
} else { } else {
typ = termFuzzy typ = termFuzzy
text = text[1:]
} }
text = text[1:]
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
if typ == termSuffix { if typ == termSuffix {
typ = termEqual typ = termEqual
@ -281,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil { if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
// Prefix/suffix cache // Prefix/suffix cache
space := _cache.Search(chunk, cacheKey) space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab) matches := p.matchChunk(chunk, space, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) p.cache.Add(chunk, cacheKey, matches)
} }
return matches return matches
} }
@ -302,13 +291,13 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }
} else { } else {
for _, result := range space { for _, result := range space {
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }
@ -337,7 +326,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
var input []Token var input []Token
if len(p.nth) == 0 { if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}} input = []Token{{text: &item.text, prefixLength: 0}}
} else { } else {
input = p.transformInput(item) input = p.transformInput(item)
} }
@ -350,7 +339,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
var input []Token var input []Token
if len(p.nth) == 0 { if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}} input = []Token{{text: &item.text, prefixLength: 0}}
} else { } else {
input = p.transformInput(item) input = p.transformInput(item)
} }

View File

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

View File

@ -1,8 +1,6 @@
// +build !openbsd //go:build !openbsd
package protector package protector
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {}
return
}

View File

@ -1,4 +1,4 @@
// +build openbsd //go:build openbsd
package protector package protector
@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {
unix.PledgePromises("stdio rpath tty proc exec") unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
} }

View File

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

View File

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

View File

@ -49,6 +49,22 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
case byScore: case byScore:
// Higher is better // Higher is better
val = math.MaxUint16 - util.AsUint16(score) val = math.MaxUint16 - util.AsUint16(score)
case byChunk:
if validOffsetFound {
b := minBegin
e := maxEnd
for ; b >= 1; b-- {
if unicode.IsSpace(item.text.Get(b - 1)) {
break
}
}
for ; e < numChars; e++ {
if unicode.IsSpace(item.text.Get(e)) {
break
}
}
val = util.AsUint16(e - b)
}
case byLength: case byLength:
val = item.TrimLength() val = item.TrimLength()
case byBegin, byEnd: case byBegin, byEnd:
@ -64,7 +80,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
} }
} }
} }
@ -122,7 +138,9 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
for i := off[0]; i < off[1]; i++ { for i := off[0]; i < off[1]; i++ {
// Negative of 1-based index of itemColors // Negative of 1-based index of itemColors
// - The extra -1 means highlighted // - The extra -1 means highlighted
cols[i] = cols[i]*-1 - 1 if cols[i] >= 0 {
cols[i] = cols[i]*-1 - 1
}
} }
} }
@ -164,7 +182,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// combination of either [hl and bg] or [hl+ and bg+]. // combination of either [hl and bg] or [hl+ and bg+].
// //
// If the original text already has background color, and the // If the original text already has background color, and the
// forground color of colMatch is -1, we shouldn't only apply the // foreground color of colMatch is -1, we shouldn't only apply the
// background color of colMatch. // background color of colMatch.
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline // e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline // echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline

View File

@ -1,4 +1,4 @@
// +build !386,!amd64 //go:build !386 && !amd64
package fzf package fzf

View File

@ -1,5 +1,3 @@
// +build !tcell
package fzf package fzf
import ( import (
@ -18,8 +16,8 @@ func withIndex(i *Item, index int) *Item {
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
offsets := []Offset{ offsets := []Offset{
Offset{3, 5}, Offset{2, 7}, {3, 5}, {2, 7},
Offset{1, 3}, Offset{2, 9}} {1, 3}, {2, 9}}
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 || if offsets[0][0] != 1 || offsets[0][1] != 3 ||
@ -56,9 +54,9 @@ func TestResultRank(t *testing.T) {
// FIXME global // FIXME global
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} str := []rune("foo")
item1 := buildResult( item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
if item1.points[3] != math.MaxUint16-2 || // Bonus if item1.points[3] != math.MaxUint16-2 || // Bonus
item1.points[2] != 3 || // Length item1.points[2] != 3 || // Length
item1.points[1] != 0 || // Unused item1.points[1] != 0 || // Unused
@ -67,7 +65,7 @@ func TestResultRank(t *testing.T) {
t.Error(item1) t.Error(item1)
} }
// Only differ in index // Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)
items := []Result{item1, item2} items := []Result{item1, item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
@ -84,13 +82,13 @@ func TestResultRank(t *testing.T) {
// Sort by relevance // Sort by relevance
item3 := buildResult( item3 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3) withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
item4 := buildResult( item4 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4) withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
item5 := buildResult( item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5) withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
item6 := buildResult( item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6) withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
items = []Result{item1, item2, item3, item4, item5, item6} items = []Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 && if !(items[0] == item6 && items[1] == item5 &&
@ -100,12 +98,29 @@ func TestResultRank(t *testing.T) {
} }
} }
func TestChunkTiebreak(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byChunk}
score := 100
test := func(input string, offset Offset, chunk string) {
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
t.Error(item.points)
}
}
test("hello foobar goodbye", Offset{8, 9}, "foobar")
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
test("hello foobar goodbye", Offset{0, 1}, "hello")
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
}
func TestColorOffset(t *testing.T) { func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ---- // ------------ 20 ---- -- ----
// ++++++++ ++++++++++ // ++++++++ ++++++++++
// --++++++++-- --++++++++++--- // --++++++++-- --++++++++++---
offsets := []Offset{{5, 15}, {25, 35}} offsets := []Offset{{5, 15}, {10, 12}, {25, 35}}
item := Result{ item := Result{
item: &Item{ item: &Item{
colors: &[]ansiOffset{ colors: &[]ansiOffset{

View File

@ -1,4 +1,4 @@
// +build 386 amd64 //go:build 386 || amd64
package fzf package fzf

252
src/server.go Normal file
View File

@ -0,0 +1,252 @@
package fzf
import (
"bufio"
"bytes"
"crypto/subtle"
"errors"
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
)
var getRegex *regexp.Regexp
func init() {
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
}
type getParams struct {
limit int
offset int
}
const (
crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
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
)
type httpServer struct {
apiKey []byte
actionChannel chan []*action
responseChannel chan string
}
type listenAddress struct {
host string
port int
}
func (addr listenAddress) IsLocal() bool {
return addr.host == "localhost" || addr.host == "127.0.0.1"
}
var defaultListenAddr = listenAddress{"localhost", 0}
func parseListenAddress(address string) (listenAddress, error) {
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
if len(parts) != 2 {
return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
}
portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
}
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
return listenAddress{parts[0], port}, nil
}
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 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 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 nil, port, fmt.Errorf("cannot extract port: %s", addr)
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return nil, port, err
}
}
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
responseChannel: responseChannel,
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
continue
}
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
}()
return listener, port, nil
}
// Here we are writing a simplistic HTTP server without using net/http
// package to reduce the size of the binary.
//
// * No --listen: 2.8MB
// * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
contentLength := 0
apiKey := ""
body := ""
answer := func(code string, message string) string {
message += "\n"
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
unauthorized := func(message string) string {
return answer(httpUnauthorized, message)
}
bad := func(message string) string {
return answer(httpBadRequest, message)
}
good := func(message string) string {
return answer(httpOk+jsonContentType, message)
}
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
scanner := bufio.NewScanner(conn)
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
found := bytes.Index(data, []byte(crlf))
if found >= 0 {
token := data[:found+len(crlf)]
return len(token), token, nil
}
if atEOF || len(body)+len(data) >= contentLength {
return 0, data, bufio.ErrFinalToken
}
return 0, nil, nil
})
section := 0
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0:
getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
select {
case response := <-server.responseChannel:
return good(response)
case <-time.After(channelTimeout):
go func() {
// Drain the channel
<-server.responseChannel
}()
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1:
if text == crlf {
if contentLength == 0 {
return bad("content-length header missing")
}
section++
continue
}
pair := strings.SplitN(text, ":", 2)
if len(pair) == 2 {
switch strings.ToLower(pair[0]) {
case "content-length":
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
if err != nil || length <= 0 || length > maxContentLength {
return bad("invalid content length")
}
contentLength = length
case "x-api-key":
apiKey = strings.TrimSpace(pair[1])
}
}
case 2:
body += text
}
}
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
return unauthorized("invalid api key")
}
if len(body) < contentLength {
return bad("incomplete request")
}
body = body[:contentLength]
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")
}
select {
case server.actionChannel <- actions:
case <-time.After(channelTimeout):
return httpUnavailable + crlf
}
return httpOk + crlf
}
func parseGetParams(query string) getParams {
params := getParams{limit: 100, offset: 0}
for _, pair := range strings.Split(query, "&") {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
switch parts[0] {
case "limit", "offset":
if val, err := strconv.Atoi(parts[1]); err == nil {
if parts[0] == "limit" {
params.limit = val
} else {
params.offset = val
}
}
}
}
}
return params
}

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,31 @@
package fzf package fzf
import ( import (
"bytes"
"io"
"os"
"regexp" "regexp"
"strings"
"testing" "testing"
"text/template"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func newItem(str string) *Item { func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
bytes := []byte(str) replaced, _ := replacePlaceholder(replacePlaceholderParams{
trimmed, _, _ := extractColor(str, nil, nil) template: template,
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} stripAnsi: stripAnsi,
delimiter: delimiter,
printsep: printsep,
forcePlus: forcePlus,
query: query,
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
executor: util.NewExecutor(""),
})
return replaced
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
@ -30,110 +45,615 @@ func TestReplacePlaceholder(t *testing.T) {
t.Errorf("expected: %s, actual: %s", expected, result) t.Errorf("expected: %s, actual: %s", expected, result)
} }
} }
// helper function that converts template format into string and carries out the check()
checkFormat := func(format string) {
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
unixStyle := quotes{`'`, `'\''`, "\n"}
windowsStyle := quotes{`^"`, `'`, "\n"}
var effectiveStyle quotes
if util.IsWindows() {
effectiveStyle = windowsStyle
} else {
effectiveStyle = unixStyle
}
expected := templateToString(format, effectiveStyle)
check(expected)
}
printsep := "\n" printsep := "\n"
/*
Test multiple placeholders and the function parameters.
*/
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar baz'") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar baz'") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus // forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter // Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'") checkFormat("echo {{.O}} foo{{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo 'bar baz'") checkFormat("echo {{.O}}bar baz{{.O}}")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter // Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`) regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '") checkFormat("echo {{.O}} {{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ''\\'''") checkFormat("echo {{.O}}{{.I}}{{.O}}")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '") checkFormat("echo {{.O}} {{.O}}")
// No match // No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'") checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter // String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter // Regex delimiter
regex = regexp.MustCompile("[oa]+") regex = regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/*
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
see: TestParsePlaceholder
*/
items3 := []*Item{
// single line
newItem("1a 1b 1c 1d 1e 1f"),
// multi line
newItem("1a 1b 1c 1d 1e 1f"),
newItem("2a 2b 2c 2d 2e 2f"),
newItem("3a 3b 3c 3d 3e 3f"),
newItem("4a 4b 4c 4d 4e 4f"),
newItem("5a 5b 5c 5d 5e 5f"),
newItem("6a 6b 6c 6d 6e 6f"),
newItem("7a 7b 7c 7d 7e 7f"),
}
stripAnsi := false
forcePlus := false
query := "sample query"
templateToOutput := make(map[string]string)
templateToFile := make(map[string]string) // same as above, but the file contents will be matched
// I. item type placeholder
templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
templateToOutput[`{n}`] = `0`
templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
templateToFile[`{nf}`] = `0{{.S}}`
templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
// II. token type placeholders
templateToOutput[`{..}`] = templateToOutput[`{}`]
templateToOutput[`{1..}`] = templateToOutput[`{}`]
templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
// shorthand for x..x range
templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
templateToOutput[`{-6}`] = templateToOutput[`{1}`]
// multiple ranges
templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
// flags
templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
templateToFile[`{f1}`] = `1a{{.S}}`
templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
// IV. escaping placeholder
templateToOutput[`\{}`] = `{}`
templateToOutput[`\{q}`] = `{q}`
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput {
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
checkFormat(wantOutput)
}
for giveTemplate, wantOutput := range templateToFile {
path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
data, err := readFile(path)
if err != nil {
t.Errorf("Cannot read the content of the temp file %s.", path)
}
result = string(data)
checkFormat(wantOutput)
}
} }
func TestQuoteEntryCmd(t *testing.T) { func TestQuoteEntry(t *testing.T) {
type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes
exec := util.NewExecutor("")
if util.IsWindows() {
effectiveStyle = windowsStyle
} else {
effectiveStyle = unixStyle
}
tests := map[string]string{ tests := map[string]string{
`"`: `^"\^"^"`, `'`: `{{.O}}{{.SQ}}{{.O}}`,
`\`: `^"\\^"`, `"`: `{{.O}}{{.DQ}}{{.O}}`,
`\"`: `^"\\\^"^"`, `\`: `{{.O}}{{.BS}}{{.O}}`,
`"\\\"`: `^"\^"\\\\\\\^"^"`, `\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`, `"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`, `$`: `{{.O}}${{.O}}`,
`$HOME`: `{{.O}}$HOME{{.O}}`,
`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
`&`: `{{.O}}{{.E}}&{{.O}}`,
`|`: `{{.O}}{{.E}}|{{.O}}`,
`<`: `{{.O}}{{.E}}<{{.O}}`,
`>`: `{{.O}}{{.E}}>{{.O}}`,
`(`: `{{.O}}{{.E}}({{.O}}`,
`)`: `{{.O}}{{.E}}){{.O}}`,
`@`: `{{.O}}{{.E}}@{{.O}}`,
`^`: `{{.O}}{{.E}}^{{.O}}`,
`%`: `{{.O}}{{.E}}%{{.O}}`,
`!`: `{{.O}}{{.E}}!{{.O}}`,
`%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
`C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
`"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
} }
for input, expected := range tests { for input, expected := range tests {
escaped := quoteEntryCmd(input) escaped := exec.QuoteEntry(input)
expected = templateToString(expected, effectiveStyle)
if escaped != expected { if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
} }
} }
} }
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
func TestUnixCommands(t *testing.T) {
if util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
// 1) working examples
// paths that does not have to evaluated will work fine, when quoted
{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
// quoting the tilde is required (to be treated as string)
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
// 2) problematic examples
// (not necessarily unexpected)
// paths that need to expand some part of it won't work (special characters and variables)
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
}
testCommands(t, tests)
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) {
// XXX Deprecated
t.SkipNow()
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
// 1) working examples
// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
// 2) problematic examples
// (not necessarily unexpected)
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
// cat: "C:\\test.txt: Invalid argument
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
// cat: "C:\\test.txt": Invalid argument
{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
// the temp file contains: `cat "C:\test.txt"`
// TODO this should actually work
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
}
testCommands(t, tests)
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
func TestPowershellCommands(t *testing.T) {
if !util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
/*
You can read each line in the following table as a pipeline that
consist of series of parsers that act upon your input (col. 1) and
each cell represents the output value.
For example:
- exec.Command("program.exe", `\''`)
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
Character CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
" empty string² missing argument error ... ... |
\" literal " unbalanced quote error ... ... |
'\"' literal '"' literal " empty string empty string (match all) | yes
'\\\"' literal '\"' literal \" literal " literal " |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
\ transparent transparent transparent regex error |
'\' transparent literal \ literal \ regex error | yes
\\ transparent transparent transparent literal \ |
'\\' transparent literal \\ literal \\ literal \ |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
' transparent unbalanced quote error ... ... |
\' transparent literal \ and unb. quote error ... ... |
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
''' transparent unbalanced quote error ... ... |
'''' transparent literal ' literal ' literal ' | yes
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
¹: native commands like grep, git grep, ripgrep
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
*/
// 1) working examples
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
// example of escaping single quotes
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
// chaining powershells
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
// 2) problematic examples
// (not necessarily unexpected)
// looking for a path string will only work with escaped backslashes
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
// looking for a literal double quote will only work with triple escaped double quotes
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
// to explorer, which will prompt user to pick editing program for the fzf-preview file
// the temp file contains: `cat "C:\test.txt"`
// TODO this should actually work
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
}
// to force powershell-style escaping we temporarily set environment variable that fzf honors
shellBackup := os.Getenv("SHELL")
os.Setenv("SHELL", "powershell")
testCommands(t, tests)
os.Setenv("SHELL", shellBackup)
}
/*
Test typical valid placeholders and parsing of them.
Also since the parser assumes the input is matched with `placeholder` regex,
the regex is tested here as well.
*/
func TestParsePlaceholder(t *testing.T) {
// give, want pairs
templates := map[string]string{
// I. item type placeholder
`{}`: `{}`,
`{+}`: `{+}`,
`{n}`: `{n}`,
`{+n}`: `{+n}`,
`{f}`: `{f}`,
`{+nf}`: `{+nf}`,
// II. token type placeholders
`{..}`: `{..}`,
`{1..}`: `{1..}`,
`{..2}`: `{..2}`,
`{1..2}`: `{1..2}`,
`{-2..-1}`: `{-2..-1}`,
// shorthand for x..x range
`{1}`: `{1}`,
`{1..1}`: `{1..1}`,
`{-6}`: `{-6}`,
// multiple ranges
`{1,2}`: `{1,2}`,
`{1,2,4}`: `{1,2,4}`,
`{1,2..4}`: `{1,2..4}`,
`{1..2,-4..-3}`: `{1..2,-4..-3}`,
// flags
`{+1}`: `{+1}`,
`{+-1}`: `{+-1}`,
`{s1}`: `{s1}`,
`{f1}`: `{f1}`,
`{+s1..2}`: `{+s1..2}`,
`{+sf1..2}`: `{+sf1..2}`,
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
`{q}`: `{qq}`,
// IV. escaping placeholder
`\{}`: `{}`,
`\{++}`: `{++}`,
`{++}`: `{+}`,
}
for giveTemplate, wantTemplate := range templates {
if !placeholder.MatchString(giveTemplate) {
t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
continue
}
_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
if gotTemplate != wantTemplate {
t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
}
}
}
/* utilities section */
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
// Functions tested in this file require array of items (allItems). The array needs
// to consist of at least two nils. This is helper function.
func newItems(str ...string) []*Item {
result := make([]*Item, util.Max(len(str), 2))
for i, s := range str {
result[i] = newItem(s)
}
return result
}
// (for logging purposes)
func (item *Item) String() string {
return item.AsString(true)
}
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string {
bb := &bytes.Buffer{}
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
if err != nil {
panic(err)
}
return bb.String()
}
// ad hoc types for test cases
type give struct {
template string
query string
allItems []*Item
}
type want struct {
/*
Unix:
The `want.output` string is supposed to be formatted for evaluation by
`sh -c command` system call.
Windows:
The `want.output` string is supposed to be formatted for evaluation by
`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
behaviour, which is more favourable for nesting (possibly escaped)
special characters. This is the relevant section of `help cmd`:
...old behavior is to see if the first character is
a quote character and if so, strip the leading character and
remove the last quote character on the command line, preserving
any text after the last quote character.
*/
output string // literal output
match string // output is matched against this regex (when output is empty string)
}
type testCase struct {
give
want
}
func testCommands(t *testing.T, tests []testCase) {
// common test parameters
delim := "\t"
delimiter := Delimiter{str: &delim}
printsep := ""
stripAnsi := false
forcePlus := false
// evaluate the test cases
for idx, test := range tests {
gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query,
test.give.allItems)
switch {
case test.want.output != "":
if gotOutput != test.want.output {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.output)
}
case test.want.match != "":
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
wantRegex := regexp.MustCompile(wantMatch)
if !wantRegex.MatchString(gotOutput) {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.match)
}
default:
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
}
}
}
// naive encoder of placeholder flags
func (flags placeholderFlags) encodePlaceholder() string {
encoded := ""
if flags.plus {
encoded += "+"
}
if flags.preserveSpace {
encoded += "s"
}
if flags.number {
encoded += "n"
}
if flags.file {
encoded += "f"
}
if flags.forceUpdate { // FIXME
encoded += "q"
}
return encoded
}
// can be replaced with os.ReadFile() in go 1.16+
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data := make([]byte, 0, 128)
for {
if len(data) >= cap(data) {
d := append(data[:cap(data)], 0)
data = d[:len(data)]
}
n, err := file.Read(data[len(data):cap(data)])
data = data[:len(data)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return data, err
}
}
}

View File

@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package fzf package fzf
@ -6,6 +6,8 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
@ -13,7 +15,12 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
} }
func notifyStop(p *os.Process) { func notifyStop(p *os.Process) {
p.Signal(syscall.SIGSTOP) pid := p.Pid
pgid, err := unix.Getpgid(pid)
if err == nil {
pid = pgid * -1
}
unix.Kill(pid, syscall.SIGSTOP)
} }
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {

View File

@ -1,4 +1,4 @@
// +build windows //go:build windows
package fzf package fzf

View File

@ -35,7 +35,7 @@ type Delimiter struct {
str *string str *string
} }
// String returns the string representation of a Delimeter. // String returns the string representation of a Delimiter.
func (d Delimiter) String() string { func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
} }
@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
prefixLength := begin prefixLength := begin
for idx := range tokens { for idx := range tokens {
chars := util.ToChars([]byte(tokens[idx])) chars := util.ToChars(stringBytes(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)} ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length() prefixLength += chars.Length()
} }
@ -156,14 +156,14 @@ func Tokenize(text string, delimiter Delimiter) []Token {
// FIXME performance // FIXME performance
var tokens []string var tokens []string
if delimiter.regex != nil { if delimiter.regex != nil {
for len(text) > 0 { locs := delimiter.regex.FindAllStringIndex(text, -1)
loc := delimiter.regex.FindStringIndex(text) begin := 0
if len(loc) < 2 { for _, loc := range locs {
loc = []int{0, len(text)} tokens = append(tokens, text[begin:loc[1]])
} begin = loc[1]
last := util.Max(loc[1], 1) }
tokens = append(tokens, text[:last]) if begin < len(text) {
text = text[last:] tokens = append(tokens, text[begin:])
} }
} }
return withPrefixLengths(tokens, 0) return withPrefixLengths(tokens, 0)
@ -187,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars([]byte(joinTokens(tokens))) chars := util.ToChars(stringBytes(joinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {

View File

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

View File

@ -1,6 +1,4 @@
// +build !ncurses //go:build !tcell && !windows
// +build !tcell
// +build !windows
package tui package tui
@ -10,32 +8,41 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
} }
const ( const (
AttrUndefined = Attr(0) AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 7) AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 8) AttrClear = Attr(1 << 9)
Bold = Attr(1) Bold = Attr(1)
Dim = Attr(1 << 1) Dim = Attr(1 << 1)
Italic = Attr(1 << 2) Italic = Attr(1 << 2)
Underline = Attr(1 << 3) Underline = Attr(1 << 3)
Blink = Attr(1 << 4) Blink = Attr(1 << 4)
Blink2 = Attr(1 << 5) Blink2 = Attr(1 << 5)
Reverse = Attr(1 << 6) Reverse = Attr(1 << 6)
StrikeThrough = Attr(1 << 7)
) )
func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) PassThrough(string) {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }

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

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

View File

@ -2,6 +2,7 @@ package tui
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@ -11,8 +12,9 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
const ( const (
@ -22,59 +24,73 @@ const (
defaultEscDelay = 100 defaultEscDelay = 100
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10 offsetPollTries = 10
maxInputBuffer = 10 * 1024 maxInputBuffer = 1024 * 1024
) )
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) PassThrough(str string) {
r.stderrInternal(str, true) r.queued.WriteString("\x1b7" + str + "\x1b8")
} }
// FIXME: Need better handling of non-displayable characters func (r *LightRenderer) stderr(str string) {
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { r.stderrInternal(str, true, "")
}
const CR string = "\x1b[2m␍"
const LF string = "\x1b[2m␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
runes := []rune{} runes := []rune{}
for len(bytes) > 0 { for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes) r, sz := utf8.DecodeRune(bytes)
nlcr := r == '\n' || r == '\r' nlcr := r == '\n' || r == '\r'
if r >= 32 || r == '\x1b' || nlcr { if r >= 32 || r == '\x1b' || nlcr {
if r == utf8.RuneError || nlcr && !allowNLCR { if nlcr && !allowNLCR {
runes = append(runes, ' ') if r == '\r' {
} else { runes = append(runes, []rune(CR+resetCode)...)
} else {
runes = append(runes, []rune(LF+resetCode)...)
}
} else if r != utf8.RuneError {
runes = append(runes, r) runes = append(runes, r)
} }
} }
bytes = bytes[sz:] bytes = bytes[sz:]
} }
r.queued += string(runes) r.queued.WriteString(string(runes))
} }
func (r *LightRenderer) csi(code string) { func (r *LightRenderer) csi(code string) string {
r.stderr("\x1b[" + code) fullcode := "\x1b[" + code
r.stderr(fullcode)
return fullcode
} }
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if len(r.queued) > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, r.queued) fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued = "" r.queued.Reset()
} }
} }
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
clearOnExit bool clearOnExit bool
prevDownTime time.Time prevDownTime time.Time
clickY []int clicks [][2]int
ttyin *os.File ttyin *os.File
ttyout *os.File
buffer []byte buffer []byte
origState *terminal.State origState *term.State
width int width int
height int height int
yoffset int yoffset int
@ -82,7 +98,7 @@ type LightRenderer struct {
escDelay int escDelay int
fullscreen bool fullscreen bool
upOneLine bool upOneLine bool
queued string queued strings.Builder
y int y int
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
@ -111,19 +127,29 @@ type LightWindow struct {
bg Color 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{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: openTtyIn(), ttyin: in,
ttyout: out,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r, nil
} }
func repeat(r rune, times int) string { func repeat(r rune, times int) string {
@ -141,11 +167,11 @@ func atoi(s string, defaultValue int) int {
return value return value
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil { if err := r.initPlatform(); err != nil {
errorExit(err.Error()) return err
} }
r.updateTerminalSize() r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@ -173,9 +199,7 @@ func (r *LightRenderer) Init() {
} }
} }
if r.mouse { r.enableMouse()
r.csi("?1000h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
@ -185,6 +209,11 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
return nil
}
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
r.maxHeightFunc = maxHeightFunc
} }
func (r *LightRenderer) makeSpace() { func (r *LightRenderer) makeSpace() {
@ -219,19 +248,20 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() ([]byte, error) {
return r.getBytesInternal(r.buffer, false) 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) c, ok := r.getch(nonblock)
if !nonblock && !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) return nil, errors.New("failed to read " + consoleDevice)
} }
retries := 0 retries := 0
if c == ESC.Int() || nonblock { if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@ -246,7 +276,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue continue
} }
break break
} else if c == ESC.Int() && pc != c { } else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} else { } else {
retries = 0 retries = 0
@ -258,19 +288,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately. // so terminate fzf immediately.
if len(buffer) > maxInputBuffer { if len(buffer) > maxInputBuffer {
r.Close() 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 { func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 { 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 { if len(r.buffer) == 0 {
panic("Empty buffer") return Event{Fatal, 0, nil}
} }
sz := 1 sz := 1
@ -286,7 +320,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte(): case CtrlQ.Byte():
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28: case 28:
@ -297,11 +331,13 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil} return Event{CtrlCaret, 0, nil}
case 31: case 31:
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
case ESC.Byte(): case Esc.Byte():
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
r.buffer = r.getBytes() if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz) ev = r.escSequence(&sz)
} }
return ev return ev
@ -313,7 +349,7 @@ func (r *LightRenderer) GetChar() Event {
} }
char, rsz := utf8.DecodeRune(r.buffer) char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError { if char == utf8.RuneError {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
sz = rsz sz = rsz
return Event{Rune, char, nil} return Event{Rune, char, nil}
@ -321,7 +357,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event { func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 { if len(r.buffer) < 2 {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
loc := offsetRegexpBegin.FindIndex(r.buffer) loc := offsetRegexpBegin.FindIndex(r.buffer)
@ -335,15 +371,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
} }
alt := false alt := false
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() { if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
r.buffer = r.buffer[1:] r.buffer = r.buffer[1:]
alt = true alt = true
} }
switch r.buffer[1] { switch r.buffer[1] {
case ESC.Byte(): case Esc.Byte():
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
case 127: case 127:
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
case '[', 'O': case '[', 'O':
if len(r.buffer) < 3 { if len(r.buffer) < 3 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@ -372,12 +408,12 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Up, 0, nil} return Event{Up, 0, nil}
case 'Z': case 'Z':
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case 'H': case 'H':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 'F': case 'F':
return Event{End, 0, nil} return Event{End, 0, nil}
case 'M': case '<':
return r.mouseSequence(sz) return r.mouseSequence(sz)
case 'P': case 'P':
return Event{F1, 0, nil} return Event{F1, 0, nil}
@ -387,7 +423,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{F3, 0, nil} return Event{F3, 0, nil}
case 'S': case 'S':
return Event{F4, 0, nil} return Event{F4, 0, nil}
case '1', '2', '3', '4', '5', '6': case '1', '2', '3', '4', '5', '6', '7', '8':
if len(r.buffer) < 4 { if len(r.buffer) < 4 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@ -419,13 +455,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
return Event{Del, 0, nil} if r.buffer[3] == '~' {
return Event{Delete, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{ShiftDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case '6': case '6':
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
return Event{End, 0, nil}
case '1': case '1':
switch r.buffer[3] { switch r.buffer[3] {
case '~': case '~':
@ -459,16 +511,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case '1', '2', '3', '5': case '1', '2', '3', '4', '5':
// Kitty iTerm2 WezTerm
// SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3' alt := r.buffer[4] == '3'
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5] char := r.buffer[5]
if altShift { altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 { if len(r.buffer) < 7 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 7 *sz = 7
char = r.buffer[6] char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if len(r.buffer) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
char = r.buffer[5]
} }
switch char { switch char {
case 'A': case 'A':
@ -476,33 +541,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
} }
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
case 'B': case 'B':
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
} }
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
case 'C': case 'C':
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
} }
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
case 'D': case 'D':
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
} }
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]
@ -518,47 +583,79 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
func (r *LightRenderer) mouseSequence(sz *int) Event { func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || !r.mouse { // "\e[<0;0;0M"
if len(r.buffer) < 9 || !r.mouse {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6
switch r.buffer[3] { rest := r.buffer[*sz:]
case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl end := bytes.IndexAny(rest, "mM")
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl if end == -1 {
mod := r.buffer[3] >= 36 return Event{Invalid, 0, nil}
left := r.buffer[3] == 32 }
down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33) elems := strings.SplitN(string(rest[:end]), ";", 3)
y := int(r.buffer[5]-33) - r.yoffset if len(elems) != 3 {
double := false return Event{Invalid, 0, nil}
if down { }
now := time.Now()
if !left { // Right double click is not allowed t := atoi(elems[0], -1)
r.clickY = []int{} x := atoi(elems[1], -1) - 1
} else if now.Sub(r.prevDownTime) < doubleClickDuration { y := atoi(elems[2], -1) - 1 - r.yoffset
r.clickY = append(r.clickY, y) if t < 0 || x < 0 {
} else { return Event{Invalid, 0, nil}
r.clickY = []int{y} }
} *sz += end + 1
r.prevDownTime = now
down := rest[end] == 'M'
scroll := 0
if t >= 64 {
t -= 64
if t&0b1 == 1 {
scroll = -1
} else { } else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && scroll = 1
time.Since(r.prevDownTime) < doubleClickDuration { }
double = true }
// middle := t & 0b1
left := t&0b11 == 0
// shift := t & 0b100
// ctrl := t & 0b1000
mod := t&0b1100 > 0
drag := t&0b100000 > 0
if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
}
double := false
if down && !drag {
now := time.Now()
if !left { // Right double click is not allowed
r.clicks = [][2]int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clicks = append(r.clicks, [2]int{x, y})
} else {
r.clicks = [][2]int{{x, y}}
}
r.prevDownTime = now
} else {
n := len(r.clicks)
if len(r.clicks) > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1] &&
time.Since(r.prevDownTime) < doubleClickDuration {
double = true
if double {
r.clicks = [][2]int{}
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
func (r *LightRenderer) smcup() { func (r *LightRenderer) smcup() {
@ -570,6 +667,7 @@ func (r *LightRenderer) rmcup() {
} }
func (r *LightRenderer) Pause(clear bool) { func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.restoreTerminal() r.restoreTerminal()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
@ -582,6 +680,22 @@ func (r *LightRenderer) Pause(clear bool) {
} }
} }
func (r *LightRenderer) enableMouse() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
}
func (r *LightRenderer) disableMouse() {
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
if clear { if clear {
@ -590,12 +704,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else { } else {
r.rmcup() r.rmcup()
} }
r.enableMouse()
r.flush() r.flush()
} else if sigcont && !r.fullscreen && r.mouse { } else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z): // NOTE: SIGCONT (Coming back from CTRL-Z):
// It's highly likely that the offset we obtained at the beginning is // It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input. // no longer correct, so we simply disable mouse input.
r.csi("?1000l") r.disableMouse()
r.mouse = false r.mouse = false
} }
} }
@ -610,6 +725,14 @@ func (r *LightRenderer) Clear() {
r.flush() r.flush()
} }
func (r *LightRenderer) NeedScrollbarRedraw() bool {
return false
}
func (r *LightRenderer) ShouldEmitResizeEvent() bool {
return false
}
func (r *LightRenderer) RefreshWindows(windows []Window) { func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush() r.flush()
} }
@ -633,12 +756,15 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen { } else if !r.fullscreen {
r.csi("u") r.csi("u")
} }
if r.mouse { r.disableMouse()
r.csi("?1000l")
}
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
return r.yoffset
} }
func (r *LightRenderer) MaxX() int { func (r *LightRenderer) MaxX() int {
@ -646,6 +772,9 @@ func (r *LightRenderer) MaxX() int {
} }
func (r *LightRenderer) MaxY() int { func (r *LightRenderer) MaxY() int {
if r.height == 0 {
r.updateTerminalSize()
}
return r.height return r.height
} }
@ -669,80 +798,117 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color w.bg = r.theme.Bg.Color
} }
w.drawBorder() w.drawBorder(false)
return w return w
} }
func (w *LightWindow) drawBorder() { func (w *LightWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape { switch w.border.shape {
case BorderRounded, BorderSharp: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
w.drawBorderAround() w.drawBorderAround(onlyHorizontal)
case BorderHorizontal: case BorderHorizontal:
w.drawBorderHorizontal(true, true) w.drawBorderHorizontal(true, true)
case BorderVertical: case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true) w.drawBorderVertical(true, true)
case BorderTop: case BorderTop:
w.drawBorderHorizontal(true, false) w.drawBorderHorizontal(true, false)
case BorderBottom: case BorderBottom:
w.drawBorderHorizontal(false, true) w.drawBorderHorizontal(false, true)
case BorderLeft: case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false) w.drawBorderVertical(true, false)
case BorderRight: case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true) w.drawBorderVertical(false, true)
} }
} }
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} }
if bottom { if bottom {
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} }
} }
func (w *LightWindow) drawBorderVertical(left, right bool) { func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2 vw := runeWidth(w.border.left)
if !left || !right { color := ColBorder
width++ if w.preview {
color = ColPreviewBorder
} }
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left { if left {
w.CPrint(ColBorder, string(w.border.vertical)) w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
} }
w.CPrint(ColBorder, repeat(' ', width))
if right { if right {
w.CPrint(ColBorder, string(w.border.vertical)) w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
} }
} }
} }
func (w *LightWindow) drawBorderAround() { func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0) w.Move(0, 0)
color := ColBorder color := ColBorder
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) hw := runeWidth(w.border.top)
for y := 1; y < w.height-1; y++ { tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
w.Move(y, 0) bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
w.CPrint(color, string(w.border.vertical)) rem := (w.width - tcw) % hw
w.CPrint(color, repeat(' ', w.width-2)) w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
w.CPrint(color, string(w.border.vertical)) if !onlyHorizontal {
vw := runeWidth(w.border.left)
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
} }
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) rem = (w.width - bcw) % hw
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
} }
func (w *LightWindow) csi(code string) { func (w *LightWindow) csi(code string) string {
w.renderer.csi(code) return w.renderer.csi(code)
} }
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
w.renderer.stderrInternal(str, allowNLCR) w.renderer.stderrInternal(str, allowNLCR, resetCode)
} }
func (w *LightWindow) Top() int { func (w *LightWindow) Top() int {
@ -818,6 +984,9 @@ func attrCodes(attr Attr) []string {
if (attr & Reverse) > 0 { if (attr & Reverse) > 0 {
codes = append(codes, "7") codes = append(codes, "7")
} }
if (attr & StrikeThrough) > 0 {
codes = append(codes, "9")
}
return codes return codes
} }
@ -845,10 +1014,10 @@ func colorCodes(fg Color, bg Color) []string {
return codes return codes
} }
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
w.csi(";" + strings.Join(codes, ";") + "m") code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0 return len(codes) > 0, code
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@ -856,20 +1025,21 @@ func (w *LightWindow) Print(text string) {
} }
func cleanse(str string) 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) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
w.csi("m") w.csi("m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
if w.csiColor(fg, bg, attr) { hasColors, code := w.csiColor(fg, bg, attr)
if hasColors {
defer w.csi("m") defer w.csi("m")
} }
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
} }
type wrappedLine struct { type wrappedLine struct {
@ -881,38 +1051,40 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
lines := []wrappedLine{} lines := []wrappedLine{}
width := 0 width := 0
line := "" line := ""
for _, r := range input { gr := uniseg.NewGraphemes(input)
w := util.RuneWidth(r, prefixLength+width, 8) for gr.Next() {
width += w rs := gr.Runes()
str := string(r) str := string(rs)
if r == '\t' { var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w) str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else {
w = uniseg.StringWidth(str)
} }
width += w
if prefixLength+width <= max { if prefixLength+width <= max {
line += str line += str
} else { } else {
lines = append(lines, wrappedLine{string(line), width - w}) lines = append(lines, wrappedLine{string(line), width - w})
line = str line = str
prefixLength = 0 prefixLength = 0
width = util.RuneWidth(r, prefixLength, 8) width = w
} }
} }
lines = append(lines, wrappedLine{string(line), width}) lines = append(lines, wrappedLine{string(line), width})
return lines return lines
} }
func (w *LightWindow) fill(str string, onMove func()) FillReturn { func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n") allLines := strings.Split(str, "\n")
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines { for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 { w.stderrInternal(wl.text, false, resetCode)
if w.posy < w.height-1 {
w.Move(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth w.posx += wl.displayWidth
// Wrap line // Wrap line
@ -922,23 +1094,35 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
onMove() w.renderer.stderr(resetCode)
} }
} }
} }
if w.posx+1 >= w.Width() {
if w.posy+1 >= w.height {
return FillSuspend
}
w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode)
return FillNextLine
}
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() { func (w *LightWindow) setBg() string {
if w.bg != colDefault { if w.bg != colDefault {
w.csiColor(colDefault, w.bg, AttrRegular) _, code := w.csiColor(colDefault, w.bg, AttrRegular)
return code
} }
// Should clear dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
return "\x1b[m"
} }
func (w *LightWindow) Fill(text string) FillReturn { func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx) w.Move(w.posy, w.posx)
w.setBg() code := w.setBg()
return w.fill(text, w.setBg) return w.fill(text, code)
} }
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
@ -949,22 +1133,29 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }
if w.csiColor(fg, bg, attr) { if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
defer w.csi("m") defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) }) return w.fill(text, resetCode)
} }
return w.fill(text, w.setBg) return w.fill(text, w.setBg())
} }
func (w *LightWindow) FinishFill() { func (w *LightWindow) FinishFill() {
w.MoveAndClear(w.posy, w.posx) if w.posy < w.height {
w.MoveAndClear(w.posy, w.posx)
}
for y := w.posy + 1; y < w.height; y++ { for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0) w.MoveAndClear(y, 0)
} }
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
w.drawBorder() w.DrawBorder()
// We don't erase the window here to avoid flickering during scroll w.Move(0, 0)
w.FinishFill()
w.Move(0, 0) w.Move(0, 0)
} }
func (w *LightWindow) EraseMaybe() bool {
return false
}

View File

@ -1,16 +1,25 @@
// +build !windows //go:build !windows
package tui package tui
import ( import (
"fmt" "errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"syscall" "syscall"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/sys/unix"
"golang.org/x/term"
)
var (
tty string
ttyin *os.File
ttyout *os.File
mutex sync.Mutex
) )
func IsLightRendererSupported() bool { func IsLightRendererSupported() bool {
@ -34,12 +43,12 @@ func (r *LightRenderer) fd() int {
func (r *LightRenderer) initPlatform() error { func (r *LightRenderer) initPlatform() error {
fd := r.fd() fd := r.fd()
origState, err := terminal.GetState(fd) origState, err := term.GetState(fd)
if err != nil { if err != nil {
return err return err
} }
r.origState = origState r.origState = origState
terminal.MakeRaw(fd) term.MakeRaw(fd)
return nil return nil
} }
@ -47,31 +56,60 @@ func (r *LightRenderer) closePlatform() {
// NOOP // NOOP
} }
func openTtyIn() *os.File { func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil { if err != nil {
tty := ttyname() if len(tty) == 0 {
tty = ttyname()
}
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in return in, nil
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) return nil, errors.New("failed to open " + consoleDevice)
os.Exit(2)
} }
return in 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, 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() { func (r *LightRenderer) setupTerminal() {
terminal.MakeRaw(r.fd()) term.MakeRaw(r.fd())
} }
func (r *LightRenderer) restoreTerminal() { func (r *LightRenderer) restoreTerminal() {
terminal.Restore(r.fd(), r.origState) term.Restore(r.fd(), r.origState)
} }
func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) updateTerminalSize() {
width, height, err := terminal.GetSize(r.fd()) width, height, err := term.GetSize(r.fd())
if err == nil { if err == nil {
r.width = width r.width = width
@ -85,9 +123,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
var err error
bytes := []byte{} bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ { 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) offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 { if len(offsets) > 3 {
// Add anything we skipped over to the input buffer // Add anything we skipped over to the input buffer
@ -108,3 +151,11 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
return int(b[0]), true return int(b[0]), true
} }
func (r *LightRenderer) Size() TermSize {
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
if err != nil {
return TermSize{}
}
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
}

View File

@ -1,15 +1,20 @@
//+build windows //go:build windows
package tui package tui
import ( import (
"os" "os"
"syscall" "syscall"
"time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
const (
timeoutInterval = 10
)
var ( var (
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
@ -60,14 +65,14 @@ func (r *LightRenderer) initPlatform() error {
// channel for non-blocking reads. Buffer to make sure // channel for non-blocking reads. Buffer to make sure
// we get the ESC sets: // we get the ESC sets:
r.ttyinChannel = make(chan byte, 12) r.ttyinChannel = make(chan byte, 1024)
// the following allows for non-blocking IO. // the following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows. // syscall.SetNonblock() is a NOOP under Windows.
go func() { go func() {
fd := int(r.inHandle) fd := int(r.inHandle)
b := make([]byte, 1) b := make([]byte, 1)
for { for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@ -86,9 +91,14 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
// not used // not used
return nil return nil, nil
}
func openTtyOut() (*os.File, error) {
// not used
return nil, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {
@ -105,16 +115,24 @@ func (r *LightRenderer) restoreTerminal() error {
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
} }
func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) Size() TermSize {
var w, h int
var bufferInfo windows.ConsoleScreenBufferInfo var bufferInfo windows.ConsoleScreenBufferInfo
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
r.width = getEnv("COLUMNS", defaultWidth) w = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) h = r.maxHeightFunc(getEnv("LINES", defaultHeight))
} else { } else {
r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left) w = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top)) h = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
} }
return TermSize{h, w, 0, 0}
}
func (r *LightRenderer) updateTerminalSize() {
size := r.Size()
r.width = size.Columns
r.height = size.Lines
} }
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
@ -130,7 +148,7 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
select { select {
case bc := <-r.ttyinChannel: case bc := <-r.ttyinChannel:
return int(bc), true return int(bc), true
default: case <-time.After(timeoutInterval * time.Millisecond):
return 0, false return 0, false
} }
} else { } else {

View File

@ -1,30 +1,41 @@
// +build tcell windows //go:build tcell || windows
package tui package tui
import ( import (
"os" "os"
"time" "time"
"unicode/utf8"
"runtime" "github.com/gdamore/tcell/v2"
"github.com/junegunn/fzf/src/util"
"github.com/gdamore/tcell" "github.com/rivo/uniseg"
"github.com/gdamore/tcell/encoding"
"github.com/mattn/go-runewidth"
) )
func HasFullscreenRenderer() bool { func HasFullscreenRenderer() bool {
return true return true
} }
func (p ColorPair) style() tcell.Style { var DefaultBorderShape BorderShape = BorderSharp
style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) func asTcellColor(color Color) tcell.Color {
if color == colDefault {
return tcell.ColorDefault
}
value := uint64(tcell.ColorValid) + uint64(color)
if color.is24() {
value = value | uint64(tcell.ColorIsRGB)
}
return tcell.Color(value)
} }
type Attr tcell.Style func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
}
type Attr int32
type TcellWindow struct { type TcellWindow struct {
color bool color bool
@ -63,8 +74,6 @@ func (w *TcellWindow) Refresh() {
} }
w.lastX = 0 w.lastX = 0
w.lastY = 0 w.lastY = 0
w.drawBorder()
} }
func (w *TcellWindow) FinishFill() { func (w *TcellWindow) FinishFill() {
@ -72,12 +81,13 @@ func (w *TcellWindow) FinishFill() {
} }
const ( const (
Bold Attr = Attr(tcell.AttrBold) Bold Attr = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim) Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink) Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse) Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline) Underline = Attr(tcell.AttrUnderline)
Italic = Attr(tcell.AttrItalic) StrikeThrough = Attr(tcell.AttrStrikeThrough)
Italic = Attr(tcell.AttrItalic)
) )
const ( const (
@ -86,6 +96,13 @@ const (
AttrClear = Attr(1 << 8) AttrClear = Attr(1 << 8)
) )
func (r *FullscreenRenderer) PassThrough(str string) {
// No-op
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
}
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme { func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 { if _screen.Colors() >= 256 {
return Dark256 return Dark256
@ -120,17 +137,21 @@ func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
} }
// handle the following as private members of FullscreenRenderer instance
// they are declared here to prevent introducing tcell library in non-windows builds
var ( var (
_screen tcell.Screen _screen tcell.Screen
_prevMouseButton tcell.ButtonMask
_initialResize bool = true
) )
func (r *FullscreenRenderer) initScreen() { func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen() s, e := tcell.NewScreen()
if e != nil { if e != nil {
errorExit(e.Error()) return e
} }
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
errorExit(e.Error()) return e
} }
if r.mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
@ -138,16 +159,25 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse() s.DisableMouse()
} }
_screen = s _screen = s
return nil
} }
func (r *FullscreenRenderer) Init() { func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" { if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "") os.Setenv("TERM", "")
} }
encoding.Register()
r.initScreen() if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
func (r *FullscreenRenderer) Top() int {
return 0
} }
func (r *FullscreenRenderer) MaxX() int { func (r *FullscreenRenderer) MaxX() int {
@ -173,49 +203,87 @@ func (r *FullscreenRenderer) Clear() {
_screen.Clear() _screen.Clear()
} }
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
return true
}
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool {
return true
}
func (r *FullscreenRenderer) Refresh() { func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
// TODO: Pixel width and height not implemented
func (r *FullscreenRenderer) Size() TermSize {
cols, lines := _screen.Size()
return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
case *tcell.EventResize: case *tcell.EventResize:
// Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
if _initialResize {
_initialResize = false
return Event{Invalid, 0, nil}
}
return Event{Resize, 0, nil} return Event{Resize, 0, nil}
// process mouse events: // process mouse events:
case *tcell.EventMouse: case *tcell.EventMouse:
// mouse down events have zeroed buttons, so we can't use them
// mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed buttons
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
// dragging has same structure, it only repeats the middle (main) event appropriately
x, y := ev.Position() x, y := ev.Position()
button := ev.Buttons()
mod := ev.Modifiers() != 0 mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 {
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
prevButton, button := _prevMouseButton, ev.Buttons()
_prevMouseButton = button
drag := prevButton == button
switch {
case button&tcell.WheelDown != 0:
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
} else if button&tcell.WheelUp != 0 { case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
} else if runtime.GOOS != "windows" { case button&tcell.Button1 != 0:
double := false
if !drag {
// all potential double click events put their coordinates in the clicks array
// double click event has two conditions, temporal and spatial, the first is checked here
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clicks = append(r.clicks, [2]int{x, y})
} else {
r.clicks = [][2]int{{x, y}}
}
r.prevDownTime = now
// detect double clicks (also check for spatial condition)
n := len(r.clicks)
double = n > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1]
if double {
// make sure two consecutive double clicks require four clicks
r.clicks = [][2]int{}
}
}
// fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
case button&tcell.Button2 != 0:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
default:
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
// to consume them. // to consume them.
left := button&tcell.Button1 != 0 left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0 down := left || button&tcell.Button3 != 0
double := false double := false
if down {
now := time.Now()
if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x)
} else {
r.clickY = []int{x}
r.prevDownTime = now
}
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
@ -223,9 +291,13 @@ func (r *FullscreenRenderer) GetChar() Event {
// process keyboard: // process keyboard:
case *tcell.EventKey: case *tcell.EventKey:
mods := ev.Modifiers() mods := ev.Modifiers()
none := mods == tcell.ModNone
alt := (mods & tcell.ModAlt) > 0 alt := (mods & tcell.ModAlt) > 0
ctrl := (mods & tcell.ModCtrl) > 0
shift := (mods & tcell.ModShift) > 0 shift := (mods & tcell.ModShift) > 0
ctrlAlt := ctrl && alt
altShift := alt && shift altShift := alt && shift
keyfn := func(r rune) Event { keyfn := func(r rune) Event {
if alt { if alt {
return CtrlAltKey(r) return CtrlAltKey(r)
@ -233,6 +305,7 @@ func (r *FullscreenRenderer) GetChar() Event {
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent() return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
} }
switch ev.Key() { switch ev.Key() {
// section 1: Ctrl+(Alt)+[a-z]
case tcell.KeyCtrlA: case tcell.KeyCtrlA:
return keyfn('a') return keyfn('a')
case tcell.KeyCtrlB: case tcell.KeyCtrlB:
@ -248,7 +321,21 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyCtrlG: case tcell.KeyCtrlG:
return keyfn('g') return keyfn('g')
case tcell.KeyCtrlH: case tcell.KeyCtrlH:
return keyfn('h') switch ev.Rune() {
case 0:
if ctrl {
return Event{Backspace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
case ctrl:
return keyfn('h')
case alt:
return Event{AltBackspace, 0, nil}
case none, shift:
return Event{Backspace, 0, nil}
}
}
case tcell.KeyCtrlI: case tcell.KeyCtrlI:
return keyfn('i') return keyfn('i')
case tcell.KeyCtrlJ: case tcell.KeyCtrlJ:
@ -285,26 +372,31 @@ func (r *FullscreenRenderer) GetChar() Event {
return keyfn('y') return keyfn('y')
case tcell.KeyCtrlZ: case tcell.KeyCtrlZ:
return keyfn('z') return keyfn('z')
// section 2: Ctrl+[ \]_]
case tcell.KeyCtrlSpace: case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash: case tcell.KeyCtrlBackslash:
return Event{CtrlBackSlash, 0, nil} return Event{CtrlBackSlash, 0, nil}
case tcell.KeyCtrlRightSq: case tcell.KeyCtrlRightSq:
return Event{CtrlRightBracket, 0, nil} return Event{CtrlRightBracket, 0, nil}
case tcell.KeyCtrlCarat:
return Event{CtrlCaret, 0, nil}
case tcell.KeyCtrlUnderscore: case tcell.KeyCtrlUnderscore:
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
} }
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp: case tcell.KeyUp:
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
if shift { if shift {
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
} }
if alt { if alt {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
@ -312,10 +404,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil} return Event{Up, 0, nil}
case tcell.KeyDown: case tcell.KeyDown:
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
if shift { if shift {
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
} }
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
@ -323,10 +415,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil} return Event{Down, 0, nil}
case tcell.KeyLeft: case tcell.KeyLeft:
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
if shift { if shift {
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
@ -334,32 +426,37 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil} return Event{Left, 0, nil}
case tcell.KeyRight: case tcell.KeyRight:
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
if shift { if shift {
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
} }
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
} }
return Event{Right, 0, nil} return Event{Right, 0, nil}
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
case tcell.KeyInsert: case tcell.KeyInsert:
return Event{Insert, 0, nil} return Event{Insert, 0, nil}
case tcell.KeyHome: case tcell.KeyHome:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case tcell.KeyDelete: case tcell.KeyDelete:
return Event{Del, 0, nil} if ctrl {
return Event{CtrlDelete, 0, nil}
}
if shift {
return Event{ShiftDelete, 0, nil}
}
return Event{Delete, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
case tcell.KeyPgUp: case tcell.KeyPgUp:
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case tcell.KeyPgDn: case tcell.KeyPgDn:
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case tcell.KeyBacktab: case tcell.KeyBacktab:
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case tcell.KeyF1: case tcell.KeyF1:
return Event{F1, 0, nil} return Event{F1, 0, nil}
case tcell.KeyF2: case tcell.KeyF2:
@ -385,20 +482,31 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyF12: case tcell.KeyF12:
return Event{F12, 0, nil} return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space: // section 6: (Ctrl+Alt)+'rune'
case tcell.KeyRune: case tcell.KeyRune:
r := ev.Rune() r := ev.Rune()
if alt {
switch {
// translate native key events to ascii control characters
case r == ' ' && ctrl:
return Event{CtrlSpace, 0, nil}
// handle AltGr characters
case ctrlAlt:
return Event{Rune, r, nil} // dropping modifiers
// simple characters (possibly with modifier)
case alt:
return AltKey(r) return AltKey(r)
default:
return Event{Rune, r, nil}
} }
return Event{Rune, r, nil}
// section 7: Esc
case tcell.KeyEsc: case tcell.KeyEsc:
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
} }
// section 8: Invalid
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@ -431,7 +539,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
if preview { if preview {
normal = ColPreview normal = ColPreview
} }
return &TcellWindow{ w := &TcellWindow{
color: r.theme.Colored, color: r.theme.Colored,
preview: preview, preview: preview,
top: top, top: top,
@ -440,6 +548,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height, height: height,
normal: normal, normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle}
w.Erase()
return w
} }
func (w *TcellWindow) Close() { func (w *TcellWindow) Close() {
@ -455,7 +565,17 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height, 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)
}
func (w *TcellWindow) EraseMaybe() bool {
w.Erase()
return true
} }
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
@ -482,7 +602,6 @@ func (w *TcellWindow) Print(text string) {
} }
func (w *TcellWindow) printString(text string, pair ColorPair) { func (w *TcellWindow) printString(text string, pair ColorPair) {
t := text
lx := 0 lx := 0
a := pair.Attr() a := pair.Attr()
@ -491,38 +610,35 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
style = style. style = style.
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0). Italic(a&Attr(tcell.AttrItalic) != 0).
Blink(a&Attr(tcell.AttrBlink) != 0). Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0) Dim(a&Attr(tcell.AttrDim) != 0)
} }
for { gr := uniseg.NewGraphemes(text)
if len(t) == 0 { for gr.Next() {
break st := style
} rs := gr.Runes()
r, size := utf8.DecodeRuneInString(t)
t = t[size:]
if r < rune(' ') { // ignore control characters if len(rs) == 1 {
continue r := rs[0]
} if r == '\r' {
st = style.Dim(true)
if r == '\n' { rs[0] = '␍'
w.lastY++ } else if r == '\n' {
lx = 0 st = style.Dim(true)
} else { rs[0] = '␊'
} else if r < rune(' ') { // ignore control characters
if r == '\u000D' { // skip carriage return
continue continue
} }
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
} }
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
}
lx += util.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
} }
@ -547,32 +663,44 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Dim(a&Attr(tcell.AttrDim) != 0). Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0) Italic(a&Attr(tcell.AttrItalic) != 0)
for _, r := range text { gr := uniseg.NewGraphemes(text)
if r == '\n' { Loop:
w.lastY++ for gr.Next() {
w.lastX = 0 st := style
lx = 0 rs := gr.Runes()
} else { if len(rs) == 1 {
var xPos = w.left + w.lastX + lx r := rs[0]
switch r {
// word wrap: case '\r':
if xPos >= (w.left + w.width) { st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++ w.lastY++
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
xPos = w.left continue Loop
} }
var yPos = w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
} }
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
yPos := w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
lx += util.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
if w.lastX == w.width { if w.lastX == w.width {
@ -598,7 +726,15 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a)) return w.fillString(str, NewColorPair(fg, bg, a))
} }
func (w *TcellWindow) drawBorder() { func (w *TcellWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
shape := w.borderStyle.shape shape := w.borderStyle.shape
if shape == BorderNone { if shape == BorderNone {
return return
@ -620,35 +756,52 @@ func (w *TcellWindow) drawBorder() {
style = w.normal.style() style = w.normal.style()
} }
hw := runeWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
for x := left; x < right; x++ { max := right - 2*hw
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) if shape == BorderHorizontal || shape == BorderTop {
max = right - hw
}
// tcell has an issue displaying two overlapping wide runes
// e.g. SetContent( HH )
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderHorizontal, BorderBottom: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
for x := left; x < right; x++ { max := right - 2*hw
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
}
}
if !onlyHorizontal {
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runeWidth(w.borderStyle.right)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
}
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderVertical, BorderLeft: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderVertical, BorderRight:
for y := top; y < bot; y++ {
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style) _screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style) _screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
} }
} }

394
src/tui/tcell_test.go Normal file
View File

@ -0,0 +1,394 @@
//go:build tcell || windows
package tui
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/junegunn/fzf/src/util"
)
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
if got == want {
return true
} else {
t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want)
return false
}
}
// Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) {
if util.ToTty() {
// This test is skipped when output goes to terminal, because it causes
// some glitches:
// - output lines may not start at the beginning of a row which makes
// the output unreadable
// - terminal may get cleared which prevents you from seeing results of
// previous tests
// Good ways to prevent the glitches are piping the output to a pager
// or redirecting to a file. I've found `less +G` to be trouble-free.
t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.")
} else if testing.Verbose() {
// I have observed a behaviour when this test outputted more than 8192
// bytes (32*256) into the 'less' pager, both the go's test executable
// and the pager hanged. The go's executable was blocking on printing.
// I was able to create minimal working example of that behaviour, but
// that example hanged after 12256 bytes (32*(256+127)).
t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.")
}
if !HasFullscreenRenderer() {
t.Skip("Can't test FullscreenRenderer.")
}
// construct test cases
type giveKey struct {
Type tcell.Key
Char rune
Mods tcell.ModMask
}
type wantKey = Event
type testCase struct {
giveKey
wantKey
}
/*
Some test cases are marked "fabricated". It means that giveKey value
is valid, but it is not what you get when you press the keys. For
example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX
(End-Of-Text character, causing SIGINT).
I was trying to accompany the fabricated test cases with real ones.
Some test cases are marked "unhandled". It means that giveKey.Type
is not present in tcell.go source code. It can still be handled via
implicit or explicit alias.
If not said otherwise, test cases are for US keyboard.
(tabstop=44)
*/
tests := []testCase{
// section 1: Ctrl+(Alt)+[a-z]
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}},
{giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated
{giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C)
{giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated
// KeyTab is alias for KeyTAB
{giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
// KeyEnter is alias for KeyCR
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
// Ctrl+Alt keys
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
// section 2: Ctrl+[ \]_]
{giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated
{giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' '
{giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}},
{giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}},
{giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated
{giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke
{giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}},
// section 3: (Alt)+Backspace2
// 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{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{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
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
// 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{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
{giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
// section 6: (Ctrl+Alt)+'rune'
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
{giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}},
{giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}},
{giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}},
/*
"Input method" in Windows Language options:
US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range
CS: "Czech keyboard"
DE: "German keyboard"
Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`.
*/
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level
{giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$"
{giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~"
{giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{"
{giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@"
{giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ"
// 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
// section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
{giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}},
{giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF
{giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
}
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
r.Init()
// run and evaluate the tests
initialResizeAsInvalid := true
for _, test := range tests {
// generate key event
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
_screen.PostEventWait(giveEvent)
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
// process the event in fzf and evaluate the test
gotEvent := r.GetChar()
// skip Resize events, those are sometimes put in the buffer outside of this test
if initialResizeAsInvalid && gotEvent.Type == Invalid {
t.Logf("Resize as Invalid swallowed")
initialResizeAsInvalid = false
gotEvent = r.GetChar()
}
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
}
r.Close()
}
/*
Quick reference
---------------
(tabstop=18)
(this is not mapping table, it merely puts multiple constants ranges in one table)
¹) the two columns are each other implicit alias
²) explicit aliases here
%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone
1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift
2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl
3 1 KeyCtrlC KeyETX = ^C CtrlC
4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt
5 1 KeyCtrlE KeyENQ = ^E CtrlE
6 1 KeyCtrlF KeyACK = ^F CtrlF
7 1 KeyCtrlG KeyBEL = ^G CtrlG
8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta
9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
11 1 KeyCtrlK KeyVT = ^K CtrlK
12 1 KeyCtrlL KeyFF = ^L CtrlL
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
14 1 KeyCtrlN KeySO = ^N CtrlN
15 1 KeyCtrlO KeySI = ^O CtrlO
16 1 KeyCtrlP KeyDLE = ^P CtrlP
17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ
18 1 KeyCtrlR KeyDC2 = ^R CtrlR
19 1 KeyCtrlS KeyDC3 = ^S CtrlS
20 1 KeyCtrlT KeyDC4 = ^T CtrlT
21 1 KeyCtrlU KeyNAK = ^U CtrlU
22 1 KeyCtrlV KeySYN = ^V CtrlV
23 1 KeyCtrlW KeyETB = ^W CtrlW
24 1 KeyCtrlX KeyCAN = ^X CtrlX
25 1 KeyCtrlY KeyEM = ^Y CtrlY
26 1 KeyCtrlZ KeySUB = ^Z CtrlZ
27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC
28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace
29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash
30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket
31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret
32 CtrlSlash
33 Invalid
34 Resize
35 Mouse
36 DoubleClick
37 LeftClick
38 RightClick
39 BTab
40 Backspace
41 Del
42 PgUp
43 PgDn
44 Up
45 Down
46 Left
47 Right
48 Home
49 End
50 Insert
51 SUp
52 SDown
53 ShiftLeft
54 SRight
55 F1
56 F2
57 F3
58 F4
59 F5
60 F6
61 F7
62 F8
63 F9
64 F10
65 F11
66 F12
67 Change
68 BackwardEOF
69 AltBackspace
70 AltUp
71 AltDown
72 AltLeft
73 AltRight
74 AltSUp
75 AltSDown
76 AltShiftLeft
77 AltShiftRight
78 Alt
79 CtrlAlt
..
127 3 KeyDEL KeyBackspace2
..
256 6 KeyRune
257 4 KeyUp
258 4 KeyDown
259 4 KeyRight
260 4 KeyLeft
261 8 KeyUpLeft
262 8 KeyUpRight
263 8 KeyDownLeft
264 8 KeyDownRight
265 8 KeyCenter
266 5 KeyPgUp
267 5 KeyPgDn
268 5 KeyHome
269 5 KeyEnd
270 5 KeyInsert
271 5 KeyDelete
272 8 KeyHelp
273 8 KeyExit
274 8 KeyClear
275 8 KeyCancel
276 8 KeyPrint
277 8 KeyPause
278 5 KeyBacktab
279 5 KeyF1
280 5 KeyF2
281 5 KeyF3
282 5 KeyF4
283 5 KeyF5
284 5 KeyF6
285 5 KeyF7
286 5 KeyF8
287 5 KeyF9
288 5 KeyF10
289 5 KeyF11
290 5 KeyF12
291 8 KeyF13
292 8 KeyF14
293 8 KeyF15
294 8 KeyF16
295 8 KeyF17
296 8 KeyF18
297 8 KeyF19
298 8 KeyF20
299 8 KeyF21
300 8 KeyF22
301 8 KeyF23
302 8 KeyF24
303 8 KeyF25
304 8 KeyF26
305 8 KeyF27
306 8 KeyF28
307 8 KeyF29
308 8 KeyF30
309 8 KeyF31
310 8 KeyF32
311 8 KeyF33
312 8 KeyF34
313 8 KeyF35
314 8 KeyF36
315 8 KeyF37
316 8 KeyF38
317 8 KeyF39
318 8 KeyF40
319 8 KeyF41
320 8 KeyF42
321 8 KeyF43
322 8 KeyF44
323 8 KeyF45
324 8 KeyF46
325 8 KeyF47
326 8 KeyF48
327 8 KeyF49
328 8 KeyF50
329 8 KeyF51
330 8 KeyF52
331 8 KeyF53
332 8 KeyF54
333 8 KeyF55
334 8 KeyF56
335 8 KeyF57
336 8 KeyF58
337 8 KeyF59
338 8 KeyF60
339 8 KeyF61
340 8 KeyF62
341 8 KeyF63
342 8 KeyF64
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods
*/

View File

@ -1,9 +1,9 @@
// +build !windows //go:build !windows
package tui package tui
import ( import (
"io/ioutil" "os"
"syscall" "syscall"
) )
@ -16,16 +16,28 @@ func ttyname() string {
} }
for _, prefix := range devPrefixes { for _, prefix := range devPrefixes {
files, err := ioutil.ReadDir(prefix) files, err := os.ReadDir(prefix)
if err != nil { if err != nil {
continue continue
} }
for _, file := range files { for _, file := range files {
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { info, err := file.Info()
if err != nil {
continue
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name() return prefix + file.Name()
} }
} }
} }
return "" return ""
} }
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
func TtyIn() *os.File {
if in, err := openTtyIn(); err == nil {
return in
}
return os.Stdin
}

View File

@ -1,7 +1,14 @@
// +build windows //go:build windows
package tui package tui
import "os"
func ttyname() string { func ttyname() string {
return "" return ""
} }
// TtyIn on Windows returns os.Stdin
func TtyIn() *os.File {
return os.Stdin
}

View File

@ -1,13 +1,16 @@
package tui package tui
import ( import (
"fmt"
"os"
"strconv" "strconv"
"time" "time"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
) )
// Types of user action // Types of user action
//
//go:generate stringer -type=EventType
type EventType int type EventType int
const ( const (
@ -39,8 +42,9 @@ const (
CtrlX CtrlX
CtrlY CtrlY
CtrlZ CtrlZ
ESC Esc
CtrlSpace CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash CtrlBackSlash
@ -48,19 +52,12 @@ const (
CtrlCaret CtrlCaret
CtrlSlash CtrlSlash
Invalid ShiftTab
Resize Backspace
Mouse
DoubleClick
LeftClick
RightClick
BTab Delete
BSpace PageUp
PageDown
Del
PgUp
PgDn
Up Up
Down Down
@ -70,10 +67,11 @@ const (
End End
Insert Insert
SUp ShiftUp
SDown ShiftDown
SLeft ShiftLeft
SRight ShiftRight
ShiftDelete
F1 F1
F2 F2
@ -88,23 +86,50 @@ const (
F11 F11
F12 F12
Change AltBackspace
BackwardEOF
AltBS
AltUp AltUp
AltDown AltDown
AltLeft AltLeft
AltRight AltRight
AltSUp AltShiftUp
AltSDown AltShiftDown
AltSLeft AltShiftLeft
AltSRight AltShiftRight
Alt Alt
CtrlAlt 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 { func (t EventType) AsEvent() Event {
@ -124,6 +149,31 @@ func (e Event) Comparable() Event {
return Event{e.Type, e.Char, nil} return Event{e.Type, e.Char, nil}
} }
func (e Event) KeyName() string {
if e.Type >= Invalid {
return ""
}
switch e.Type {
case Rune:
return string(e.Char)
case Alt:
return "alt-" + string(e.Char)
case CtrlAlt:
return "ctrl-alt-" + string(e.Char)
case CtrlBackSlash:
return "ctrl-\\"
case CtrlRightBracket:
return "ctrl-]"
case CtrlCaret:
return "ctrl-^"
case CtrlSlash:
return "ctrl-/"
}
return util.ToKebabCase(e.Type.String())
}
func Key(r rune) Event { func Key(r rune) Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
@ -248,25 +298,34 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
} }
type ColorTheme struct { type ColorTheme struct {
Colored bool Colored bool
Input ColorAttr Input ColorAttr
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
PreviewFg ColorAttr SelectedFg ColorAttr
PreviewBg ColorAttr SelectedBg ColorAttr
DarkBg ColorAttr SelectedMatch ColorAttr
Gutter ColorAttr PreviewFg ColorAttr
Prompt ColorAttr PreviewBg ColorAttr
Match ColorAttr DarkBg ColorAttr
Current ColorAttr Gutter ColorAttr
CurrentMatch ColorAttr Prompt ColorAttr
Spinner ColorAttr Match ColorAttr
Info ColorAttr Current ColorAttr
Cursor ColorAttr CurrentMatch ColorAttr
Selected ColorAttr Spinner ColorAttr
Header ColorAttr Info ColorAttr
Border ColorAttr Cursor ColorAttr
Marker ColorAttr
Header ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
Border ColorAttr
PreviewBorder ColorAttr
PreviewScrollbar ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
} }
type Event struct { type Event struct {
@ -275,6 +334,15 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int
@ -291,6 +359,10 @@ const (
BorderNone BorderShape = iota BorderNone BorderShape = iota
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold
BorderBlock
BorderThinBlock
BorderDouble
BorderHorizontal BorderHorizontal
BorderVertical BorderVertical
BorderTop BorderTop
@ -299,10 +371,36 @@ const (
BorderRight 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
return false
}
return true
}
func (s BorderShape) HasTop() bool {
switch s {
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false
}
return true
}
type BorderStyle struct { type BorderStyle struct {
shape BorderShape shape BorderShape
horizontal rune top rune
vertical rune bottom rune
left rune
right rune
topLeft rune topLeft rune
topRight rune topRight rune
bottomLeft rune bottomLeft rune
@ -312,64 +410,143 @@ type BorderStyle struct {
type BorderCharacter int type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode { if !unicode {
if shape == BorderRounded {
return BorderStyle{
shape: shape,
horizontal: '─',
vertical: '│',
topLeft: '╭',
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
}
}
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '─', top: '-',
vertical: '│', bottom: '-',
left: '|',
right: '|',
topLeft: '+',
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
}
}
switch shape {
case BorderSharp:
return BorderStyle{
shape: shape,
top: '─',
bottom: '─',
left: '│',
right: '│',
topLeft: '┌', topLeft: '┌',
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
bottomRight: '┘', bottomRight: '┘',
} }
case BorderBold:
return BorderStyle{
shape: shape,
top: '━',
bottom: '━',
left: '┃',
right: '┃',
topLeft: '┏',
topRight: '┓',
bottomLeft: '┗',
bottomRight: '┛',
}
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
// ▌ ▐
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
return BorderStyle{
shape: shape,
top: '▀',
bottom: '▄',
left: '▌',
right: '▐',
topLeft: '▛',
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
}
case BorderThinBlock:
// 🭽▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔🭾
// ▏ ▕
// 🭼▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁🭿
return BorderStyle{
shape: shape,
top: '▔',
bottom: '▁',
left: '▏',
right: '▕',
topLeft: '🭽',
topRight: '🭾',
bottomLeft: '🭼',
bottomRight: '🭿',
}
case BorderDouble:
return BorderStyle{
shape: shape,
top: '═',
bottom: '═',
left: '║',
right: '║',
topLeft: '╔',
topRight: '╗',
bottomLeft: '╚',
bottomRight: '╝',
}
} }
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '-', top: '─',
vertical: '|', bottom: '─',
topLeft: '+', left: '│',
topRight: '+', right: '│',
bottomLeft: '+', topLeft: '╭',
bottomRight: '+', topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
} }
} }
func MakeTransparentBorder() BorderStyle { func MakeTransparentBorder() BorderStyle {
return BorderStyle{ return BorderStyle{
shape: BorderRounded, shape: BorderRounded,
horizontal: ' ', top: ' ',
vertical: ' ', bottom: ' ',
left: ' ',
right: ' ',
topLeft: ' ', topLeft: ' ',
topRight: ' ', topRight: ' ',
bottomLeft: ' ', bottomLeft: ' ',
bottomRight: ' '} bottomRight: ' '}
} }
type TermSize struct {
Lines int
Columns int
PxWidth int
PxHeight int
}
type Renderer interface { type Renderer interface {
Init() Init() error
Resize(maxHeightFunc func(int) int)
Pause(clear bool) Pause(clear bool)
Resume(clear bool, sigcont bool) Resume(clear bool, sigcont bool)
Clear() Clear()
RefreshWindows(windows []Window) RefreshWindows(windows []Window)
Refresh() Refresh()
Close() Close()
PassThrough(string)
NeedScrollbarRedraw() bool
ShouldEmitResizeEvent() bool
GetChar() Event GetChar() Event
Top() int
MaxX() int MaxX() int
MaxY() int MaxY() int
Size() TermSize
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
} }
@ -379,6 +556,8 @@ type Window interface {
Width() int Width() int
Height() int Height() int
DrawBorder()
DrawHBorder()
Refresh() Refresh()
FinishFill() FinishFill()
Close() Close()
@ -394,6 +573,7 @@ type Window interface {
Fill(text string) FillReturn Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn CFill(fg Color, bg Color, attr Attr, text string) FillReturn
Erase() Erase()
EraseMaybe() bool
} }
type FullscreenRenderer struct { type FullscreenRenderer struct {
@ -401,7 +581,7 @@ type FullscreenRenderer struct {
mouse bool mouse bool
forceBlack bool forceBlack bool
prevDownTime time.Time prevDownTime time.Time
clickY []int clicks [][2]int
} }
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@ -410,7 +590,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse, mouse: mouse,
forceBlack: forceBlack, forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0), prevDownTime: time.Unix(0, 0),
clickY: []int{}} clicks: [][2]int{}}
return r return r
} }
@ -426,133 +606,186 @@ var (
ColMatch ColorPair ColMatch ColorPair
ColCursor ColorPair ColCursor ColorPair
ColCursorEmpty ColorPair ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair ColCurrent ColorPair
ColCurrentMatch ColorPair ColCurrentMatch ColorPair
ColCurrentCursor ColorPair ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair ColSpinner ColorPair
ColInfo ColorPair ColInfo ColorPair
ColHeader ColorPair ColHeader ColorPair
ColSeparator ColorPair
ColScrollbar ColorPair
ColBorder ColorPair ColBorder ColorPair
ColPreview ColorPair ColPreview ColorPair
ColPreviewBorder ColorPair ColPreviewBorder ColorPair
ColBorderLabel ColorPair
ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
ColPreviewSpinner ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined}, Match: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined}, Current: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined}, CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
CurrentMatch: ColorAttr{colUndefined, AttrUndefined}, Spinner: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colUndefined, AttrUndefined}, Info: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined}, Marker: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}} BorderLabel: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
} }
func NoColorTheme() *ColorTheme { func NoColorTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: false, Colored: false,
Input: ColorAttr{colDefault, AttrRegular}, Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrRegular}, Fg: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrRegular}, Bg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrRegular}, SelectedFg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrRegular}, SelectedBg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrRegular}, SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrRegular}, DarkBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrRegular}, Prompt: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrRegular}, Match: ColorAttr{colDefault, Underline},
Match: ColorAttr{colDefault, Underline}, Current: ColorAttr{colDefault, Reverse},
Current: ColorAttr{colDefault, Reverse}, CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline}, Spinner: ColorAttr{colDefault, AttrUndefined},
Spinner: ColorAttr{colDefault, AttrRegular}, Info: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrRegular}, Cursor: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrRegular}, Marker: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrRegular}, Header: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrRegular}, Border: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrRegular}} BorderLabel: ColorAttr{colDefault, AttrUndefined},
} Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrUndefined},
func errorExit(message string) { PreviewBg: ColorAttr{colDefault, AttrUndefined},
fmt.Fprintln(os.Stderr, message) Gutter: ColorAttr{colDefault, AttrUndefined},
os.Exit(2) PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
}
} }
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Current: ColorAttr{colYellow, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined}, CurrentMatch: ColorAttr{colGreen, AttrUndefined},
CurrentMatch: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Info: ColorAttr{colWhite, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}} BorderLabel: ColorAttr{colWhite, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Current: ColorAttr{254, AttrUndefined},
Current: ColorAttr{254, AttrUndefined}, CurrentMatch: ColorAttr{151, AttrUndefined},
CurrentMatch: ColorAttr{151, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}} BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Current: ColorAttr{237, AttrUndefined},
Current: ColorAttr{237, AttrUndefined}, CurrentMatch: ColorAttr{23, AttrUndefined},
CurrentMatch: ColorAttr{23, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}} BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
} }
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
@ -571,13 +804,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
return c return c
} }
theme.Input = o(baseTheme.Input, theme.Input) theme.Input = o(baseTheme.Input, theme.Input)
theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled))
theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg) theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match) theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current) theme.Current = o(baseTheme.Current, theme.Current)
@ -585,9 +814,24 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info) theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor) 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.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border) 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)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
initPalette(theme) initPalette(theme)
} }
@ -604,22 +848,38 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg) ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg) ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg) ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg) ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg) ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter) ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, 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) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg) ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg) ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg) ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg) ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg) ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg)
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg) ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.Border, theme.PreviewBg) ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
}
func runeWidth(r rune) int {
return uniseg.StringWidth(string(r))
} }

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

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

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

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

View File

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

View File

@ -9,7 +9,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtClose
) )
func TestEventBox(t *testing.T) { func TestEventBox(t *testing.T) {

View File

@ -3,26 +3,55 @@ package util
import ( import (
"math" "math"
"os" "os"
"strings"
"time" "time"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth" "github.com/rivo/uniseg"
) )
var _runeWidths = make(map[rune]int) // StringWidth returns string width where each CR/LF character takes 1 column
func StringWidth(s string) int {
return uniseg.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
}
// RuneWidth returns rune width // RunesWidth returns runes width
func RuneWidth(r rune, prefixWidth int, tabstop int) int { func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
if r == '\t' { width := 0
return tabstop - prefixWidth%tabstop gr := uniseg.NewGraphemes(string(runes))
} else if w, found := _runeWidths[r]; found { idx := 0
return w for gr.Next() {
} else if r == '\n' || r == '\r' { rs := gr.Runes()
return 1 var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop
} else {
w = StringWidth(string(rs))
}
width += w
if width > limit {
return width, idx
}
idx += len(rs)
} }
w := runewidth.RuneWidth(r) return width, -1
_runeWidths[r] = w }
return w
// Truncate returns the truncated runes and its width
func Truncate(input string, limit int) ([]rune, int) {
runes := []rune{}
width := 0
gr := uniseg.NewGraphemes(input)
for gr.Next() {
rs := gr.Runes()
w := StringWidth(string(rs))
if width+w > limit {
return runes, width
}
width += w
runes = append(runes, rs...)
}
return runes, width
} }
// Max returns the largest integer // Max returns the largest integer
@ -108,11 +137,16 @@ func DurWithin(
return val return val
} }
// IsTty returns true is stdin is a terminal // IsTty returns true if stdin is a terminal
func IsTty() bool { func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd()) return isatty.IsTerminal(os.Stdin.Fd())
} }
// ToTty returns true if stdout is a terminal
func ToTty() bool {
return isatty.IsTerminal(os.Stdout.Fd())
}
// Once returns a function that returns the specified boolean value only once // Once returns a function that returns the specified boolean value only once
func Once(nextResponse bool) func() bool { func Once(nextResponse bool) func() bool {
state := nextResponse state := nextResponse
@ -122,3 +156,35 @@ func Once(nextResponse bool) func() bool {
return prevState return prevState
} }
} }
// RepeatToFill repeats the given string to fill the given width
func RepeatToFill(str string, length int, limit int) string {
times := limit / length
rest := limit % length
output := strings.Repeat(str, times)
if rest > 0 {
for _, r := range str {
rest -= uniseg.StringWidth(string(r))
if rest < 0 {
break
}
output += string(r)
if rest == 0 {
break
}
}
}
return output
}
// ToKebabCase converts the given CamelCase string to kebab-case
func ToKebabCase(s string) string {
name := ""
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
}

View File

@ -1,14 +1,76 @@
package util package util
import "testing" import (
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) { func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 { if Max(-2, 5) != 5 {
t.Error("Invalid result") t.Error("Expected", 5)
} }
} }
func TestContrain(t *testing.T) { func TestMax16(t *testing.T) {
if Max16(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max16(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
t.Error("Expected", math.MaxInt16)
}
if Max16(0, math.MinInt16) != 0 {
t.Error("Expected", 0)
}
}
func TestMax32(t *testing.T) {
if Max32(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max32(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
t.Error("Expected", math.MaxInt32)
}
if Max32(0, math.MinInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestMin(t *testing.T) {
if Min(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min(-2, 5) != -2 {
t.Error("Expected", -2)
}
}
func TestMin32(t *testing.T) {
if Min32(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min32(-2, 5) != -2 {
t.Error("Expected", -2)
}
if Min32(math.MaxInt32, 0) != 0 {
t.Error("Expected", 0)
}
if Min32(0, math.MinInt32) != math.MinInt32 {
t.Error("Expected", math.MinInt32)
}
}
func TestConstrain(t *testing.T) {
if Constrain(-3, -1, 3) != -1 { if Constrain(-3, -1, 3) != -1 {
t.Error("Expected", -1) t.Error("Expected", -1)
} }
@ -21,6 +83,55 @@ func TestContrain(t *testing.T) {
} }
} }
func TestConstrain32(t *testing.T) {
if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1)
}
if Constrain32(2, -1, 3) != 2 {
t.Error("Expected", 2)
}
if Constrain32(5, -1, 3) != 3 {
t.Error("Expected", 3)
}
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestAsUint16(t *testing.T) {
if AsUint16(5) != 5 {
t.Error("Expected", 5)
}
if AsUint16(-10) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
if AsUint16(math.MinInt32) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MinInt16) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
}
func TestDurWithIn(t *testing.T) {
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
t.Error("Expected", time.Duration(0))
}
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
t.Error("Expected", time.Second)
}
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
t.Error("Expected", time.Second)
}
}
func TestOnce(t *testing.T) { func TestOnce(t *testing.T) {
o := Once(false) o := Once(false)
if o() { if o() {
@ -38,3 +149,57 @@ func TestOnce(t *testing.T) {
t.Error("Expected: false") t.Error("Expected: false")
} }
} }
func TestRunesWidth(t *testing.T) {
for _, args := range [][]int{
{100, 5, -1},
{3, 4, 3},
{0, 1, 0},
} {
width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0])
if width != args[1] {
t.Errorf("Expected width: %d, actual: %d", args[1], width)
}
if overflowIdx != args[2] {
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
}
}
for _, input := range []struct {
s string
w int
}{
{"▶", 1},
{"▶️", 2},
} {
width, _ := RunesWidth([]rune(input.s), 0, 0, 100)
if width != input.w {
t.Errorf("Expected width of %s: %d, actual: %d", input.s, input.w, width)
}
}
}
func TestTruncate(t *testing.T) {
truncated, width := Truncate("가나다라마", 7)
if string(truncated) != "가나다" {
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
}
if width != 6 {
t.Errorf("Expected: 6, actual: %d", width)
}
}
func TestRepeatToFill(t *testing.T) {
if RepeatToFill("abcde", 10, 50) != strings.Repeat("abcde", 5) {
t.Error("Expected:", strings.Repeat("abcde", 5))
}
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
}
}
func TestStringWidth(t *testing.T) {
w := StringWidth("─")
if w != 1 {
t.Errorf("Expected: %d, Actual: %d", 1, w)
}
}

View File

@ -1,31 +1,73 @@
// +build !windows //go:build !windows
package util package util
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL type Executor struct {
func ExecCommand(command string, setpgid bool) *exec.Cmd { shell string
shell := os.Getenv("SHELL") args []string
if len(shell) == 0 { escaper *strings.Replacer
shell = "sh"
}
return ExecCommandWith(shell, command, setpgid)
} }
// ExecCommandWith executes the given command with the specified shell func NewExecutor(withShell string) *Executor {
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { shell := os.Getenv("SHELL")
cmd := exec.Command(shell, "-c", command) args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
args = args[1:]
} else {
if len(shell) == 0 {
shell = "sh"
}
args = []string{"-c"}
}
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}
}
// 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 { if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
} }
return cmd 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 // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
@ -45,3 +87,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(int(fd), b) return syscall.Read(int(fd), b)
} }
func SetStdin(file *os.File) {
unix.Dup2(int(file.Fd()), 0)
}

View File

@ -1,4 +1,4 @@
// +build windows //go:build windows
package util package util
@ -6,28 +6,164 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp"
"strings"
"sync/atomic"
"syscall" "syscall"
) )
// ExecCommand executes the given command with cmd type shellType int
func ExecCommand(command string, setpgid bool) *exec.Cmd {
return ExecCommandWith("cmd", command, setpgid) const (
shellTypeUnknown shellType = iota
shellTypeCmd
shellTypePowerShell
)
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
type Executor struct {
shell string
shellType shellType
args []string
shellPath atomic.Value
} }
// ExecCommandWith executes the given command with cmd. _shell parameter is func NewExecutor(withShell string) *Executor {
// ignored on Windows. 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
// FIXME: setpgid is unused. We set it in the Unix implementation so that we // FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once. // can kill preview process with its child processes at once.
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd { // NOTE: For "powershell", we should ideally set output encoding to UTF8,
cmd := exec.Command("cmd") // but it is left as is now because no adverse effect has been observed.
cmd.SysProcAttr = &syscall.SysProcAttr{ func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
HideWindow: false, shell := x.shell
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command), if cached := x.shellPath.Load(); cached != nil {
CreationFlags: 0, shell = cached.(string)
} else {
if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
shell = strings.Trim(string(out), "\n")
}
}
x.shellPath.Store(shell)
}
var cmd *exec.Cmd
if x.shellType == shellTypeCmd {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
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
} }
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)
}
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, "'", "'\\''") + "'"
}
}
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return cmd.Process.Kill() return cmd.Process.Kill()
@ -47,3 +183,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(syscall.Handle(fd), b) return syscall.Read(syscall.Handle(fd), b)
} }
func SetStdin(file *os.File) {
// No-op
}

File diff suppressed because it is too large Load Diff

10
typos.toml Normal file
View File

@ -0,0 +1,10 @@
# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos
[default.extend-words]
ba = "ba"
fo = "fo"
enew = "enew"
tabe = "tabe"
Iterm = "Iterm"
[files]
extend-exclude = ["README.md"]

View File

@ -51,13 +51,8 @@ remove() {
} }
remove_line() { remove_line() {
src=$(readlink "$1") src=$1
if [ $? -eq 0 ]; then echo "Remove from $1:"
echo "Remove from $1 ($src):"
else
src=$1
echo "Remove from $1:"
fi
shift shift
line_no=1 line_no=1
@ -75,8 +70,9 @@ remove_line() {
echo " - Line #$line_no: $content" echo " - Line #$line_no: $content"
[ "$content" = "$1" ] || ask " - Remove?" [ "$content" = "$1" ] || ask " - Remove?"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" && temp=$(mktemp)
mv "$src.bak" "$src" || break awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
cat "$temp" > "$src" && rm -f "$temp" || break
echo " - Removed" echo " - Removed"
else else
echo " - Skipped" echo " - Skipped"
@ -98,16 +94,17 @@ done
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ -f "$bind_file" ]; then if [ -f "$bind_file" ]; then
remove_line "$bind_file" "fzf_key_bindings" remove_line "$bind_file" "fzf_key_bindings"
remove_line "$bind_file" "fzf --fish | source"
fi fi
if [ -d "${fish_dir}/functions" ]; then if [ -d "${fish_dir}/functions" ]; then
remove "${fish_dir}/functions/fzf.fish" remove "${fish_dir}/functions/fzf.fish"
remove "${fish_dir}/functions/fzf_key_bindings.fish" remove "${fish_dir}/functions/fzf_key_bindings.fish"
if [ "$(ls -A "${fish_dir}/functions")" ]; then if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
else
rmdir "${fish_dir}/functions" rmdir "${fish_dir}/functions"
else
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
fi fi
fi fi