Compare commits

..

303 Commits

Author SHA1 Message Date
Junegunn Choi
b908f7a0ec 0.68.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2026-02-20 11:03:46 +09:00
Kyle Tse
1a50a3c082 Fix FZF_COMPLETION_{DIR,PATH}_OPTS ignored with custom compgen functions (#4679)
When users define custom _fzf_compgen_path or _fzf_compgen_dir functions,
FZF_COMPLETION_PATH_OPTS and FZF_COMPLETION_DIR_OPTS were not applied
because the options were only computed inside the walker fallback branch.

Close #4592
2026-02-20 10:59:43 +09:00
Junegunn Choi
fefea8d885 zsh: Make _fzf_compgen_{path,dir} respect FZF_COMPLETION_{PATH,DIR}_OPTS
Authored by: @LangLangBart

Fix #4592
2026-02-20 10:31:46 +09:00
bitraid
385cccd362 fish: Command history improvements (#4672)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
This change provides the following improvements:

- Changes the view of the command history list, so that the script no longer depends on perl for performance.
- Enables syntax color highlighting on fish v4.3.3 and newer.
- Provides a preview window with the selected commands, and expanded view of the highlighted command if available.
- Improves the delete functionality, by successfully handling very large numbers of selected commands.
- Inserts commands in their formatted form with `<Alt-Enter>`.

---

* fish: Change history list view

The view of the command history is changed, so that no manipulation is
performed on the output of history command: The history item count
number is replaced by the Unix time of the command, multi-line display
is disabled and line wrapping is enabled by default. On fish v4.3.3
and newer, the use of ANSI color codes for syntax highlighting is now
possible, while the script no longer depends on perl for performance.

Fixes #4661

* fish: Reformat selected history commands with ALT-ENTER

* Add $FZF_WRAP environment variable

The variable is set when line wrapping is enabled. It has the value
`word` if word wrapping mode is set, otherwise it has the value `char`.

* fish: Add command history preview

The preview shows the highlighted command and any selected commands,
after first being formatted and colorized by fish_indent. The timestamp
of the highlighted command in the preview is converted to strftime
format "%F %a %T" if the date command is available.

The preview is hidden on start, and is displayed if more than 100
columns are available and one of the following conditions are met:
- The highlighted item doesn't completely fit in list view (line
wrapping is enabled for the preview and is now disabled for the list).
- The highlighted item contains newlines (multi-line commands or
strings).
- The highlighted item contains chained commands in a single line, that
can be broken down by the formatter for cleaner view.
- One or more commands are marked as selected.

* fish: Handle deletion of large number of selected history entries

* fish: Change wrapping options for the preview-window
2026-02-19 23:41:26 +09:00
Junegunn Choi
4a684b6c78 Fix test failure 2026-02-19 21:37:19 +09:00
Junegunn Choi
4a195e6323 Add --preview-wrap-sign 2026-02-19 21:30:23 +09:00
Junegunn Choi
0ecbf3f475 Fix missing wrap sign at ANSI color boundary 2026-02-19 19:49:13 +09:00
Junegunn Choi
4522868fc0 Year 2026
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-19 00:20:09 +09:00
Junegunn Choi
111a62f1ea Update bash fzf option completion 2026-02-19 00:15:08 +09:00
Junegunn Choi
33cac3f0e7 Fix test case 2026-02-18 23:00:33 +09:00
Junegunn Choi
74e98cac5c Fix --preview-window follow not working correctly with wrapping (contd.) 2026-02-18 21:55:28 +09:00
Junegunn Choi
c338df02c4 Fix --preview-window follow not working correctly with wrapping
Fix #3243
Fix #4258
2026-02-18 21:36:35 +09:00
Junegunn Choi
69e9abdab4 Implement word wrapping in the list section
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-18 15:20:56 +09:00
Junegunn Choi
b6411beaa1 Implement word wrapping in the preview window
Example:
  fzf --preview 'bat --style=plain --color=always {}' \
      --preview-window wrap-word \
      --bind space:toggle-preview-wrap-word

Close https://github.com/junegunn/fzf/discussions/3383
2026-02-18 13:35:02 +09:00
Junegunn Choi
b56d614ba2 Add underline style variants and underline color support
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Support double, curly, dotted, and dashed underline styles via --color
(e.g. underline-curly) and ANSI passthrough (SGR 4:N, 58, 59) with --ansi.

Close #4633
Close #4678

Thanks to @shtse8 for the test cases.
2026-02-15 01:06:46 +09:00
Junegunn Choi
49ab253555 Run sponsors workflow at midnight KST on Sundays 2026-02-15 01:00:42 +09:00
Junegunn Choi
0e859a18ed Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-09 09:43:47 +09:00
Sam Killen
880dd20b18 Make symlinks to directories to return as directories, not files (#4676)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4675
2026-02-08 12:57:45 +09:00
junegunn
91aa25c863 Deploying to master from @ junegunn/fzf@7e62b34087 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-08 00:06:19 +00:00
LM
7e62b34087 Add fish completion support (#4605)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2026-02-05 12:50:26 +09:00
Stephen Bolton
f9f0014c16 Fix bug in advanced guide for using fzf as a ripgrep launcher (#4670)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
With the previous version if you passed in an initial search term that
resulted in 0 results you would get rg error output in the prompt. It
looks like down the change path the `|| true` clause was added but it
also needs to be on the initial query.
2026-02-04 17:14:02 +09:00
Junegunn Choi
ab3b9fef52 Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-02 09:40:38 +09:00
LangLangBart
7d9724157c fix(terminal): handle SIGHUP signal (#4668)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-02-01 19:51:49 +09:00
Junegunn Choi
bc8967632b Fix preview process not killed on exit
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Thanks to @LangLangBart for the investigation and the suggested fix.

Fix #4667
2026-02-01 11:32:40 +09:00
Junegunn Choi
6360c9261c Fix coloring of items with zero-width characters
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
This commit fixes incorrect coloring for items that contain zero-width
characters. It also makes ellipsis coloring consistent when text is
trimmed from either the left or the right.

Fix #4620
Close #4646
2026-02-01 11:08:23 +09:00
Junegunn Choi
e653628458 lint: test code 2026-02-01 11:08:23 +09:00
junegunn
28747e5cb6 Deploying to master from @ junegunn/fzf@9725eac314 🚀 2026-02-01 00:03:06 +00:00
Rahul Narsingipyta
9725eac314 fzf-tmux: Use mktemp, and fix TERM quoting (#4664)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2026-01-28 00:39:05 +09:00
Junegunn Choi
b389616030 Fix track-current unset after a combined movement action
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4649
Close #4663
2026-01-26 22:00:30 +09:00
junegunn
25b2248f11 Deploying to master from @ junegunn/fzf@14564e4fc7 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-01-25 00:02:24 +00:00
Junegunn Choi
14564e4fc7 Avoid clearing the rest of the current line on start
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4652

Code was reorganized so that it doesn't delete the current line if the
cursor is not at the front. This should be acceptable in almost all
cases.

Thanks to @phanen for reporting the issue and suggesting the fix.
2026-01-22 12:23:29 +09:00
junegunn
d01eaa9de3 Deploying to master from @ junegunn/fzf@aad805d0d3 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-01-18 00:02:25 +00:00
Junegunn Choi
aad805d0d3 Fix x-api-key header not required for GET requests
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4627
2026-01-17 15:13:57 +09:00
junegunn
d1f037059a Deploying to master from @ junegunn/fzf@3f94bcb5bf 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-01-11 00:02:24 +00:00
Junegunn Choi
3f94bcb5bf Cancel key reading when 'execute' triggered via a server request (#4653)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4524
Close #4648
2026-01-09 00:29:40 +09:00
junegunn
3c7cbc9d47 Deploying to master from @ junegunn/fzf@28e2a067e5 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-01-04 00:02:18 +00:00
James Lazo (jazo)
28e2a067e5 fix: rebind readline command redraw-current-line (#4635)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Close #4634
2026-01-03 01:44:49 +09:00
junegunn
b29e2ee2d1 Deploying to master from @ junegunn/fzf@029b241dbb 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-28 00:02:17 +00:00
mrclmr
029b241dbb Fix goreleaser deprecation warnings (#4644)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Current goreleaser v2.13.1 has several deprecations:
* since v2.2: https://goreleaser.com/deprecations#snapshotname_template
* since v2.6: https://goreleaser.com/deprecations#archivesformat
* since v2.6: https://goreleaser.com/deprecations#archivesformat_overridesformat
* since v2.8: https://goreleaser.com/deprecations#archivesbuilds

Check build output locally: `goreleaser release --clean --snapshot`
2025-12-24 08:57:43 +09:00
Marcel Meyer
d6ded42026 Replace nested max calls with single max
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-23 09:14:33 +09:00
Marcel Meyer
6eb4b41e34 Add generic utils constraint function 2025-12-23 09:14:33 +09:00
Marcel Meyer
14b5e1d88c Replace utils Min, Max with builtin min, max 2025-12-23 09:14:33 +09:00
junegunn
603240122e Deploying to master from @ junegunn/fzf@8d688521fe 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-21 00:02:22 +00:00
Charalambos Emmanouilidis
8d688521fe Fix --accept-nth being ignored in filter mode (#4636)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
The --accept-nth option was not being respected when using --filter mode.
This caused fzf to output entire lines instead of only the specified fields.

Added buildItemTransformer() helper function to consistently apply field
transformations across filter mode (both streaming and non-streaming) and
select1/exit0 modes.

Fixes #4615
2025-12-19 18:31:39 +09:00
Jean-Yves LENHOF
775129367a docs(README): add mise alternative method installation (#4637)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Signed-off-by: jylenhof <jygithub@lenhof.eu.org>
2025-12-18 09:59:11 +09:00
junegunn
b3b221854b Deploying to master from @ junegunn/fzf@c8cf0992c1 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-14 00:02:25 +00:00
LangLangBart
c8cf0992c1 zsh foreign test (#4622)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
* test(zsh): add test for C-r with foreign commands

* ci: make docker command configurable via variable

Allows using alternative container runtimes, e.g., DOCKER=podman make docker-test

* test(zsh): use unique histfile for foreign commands test

* test(zsh): use multi select in foreign test
2025-12-08 11:15:44 +09:00
junegunn
33d8d51c8a Deploying to master from @ junegunn/fzf@b473477c22 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-07 00:02:17 +00:00
Junegunn Choi
b473477c22 Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-12-04 08:36:49 +09:00
LangLangBart
fcc4178bca Updates man page documentation (#4607)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
* docs(man): Add example for {+f} in preview command
* docs(man): replace echo -e with printf to improve portability across different shells
2025-12-02 18:29:46 +09:00
LangLangBart
cfc37caabc feat(zsh): Handle multi-line history selection (#4595)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-30 21:32:55 +09:00
Junegunn Choi
af2a81dc02 Add Goods code
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-11-29 14:39:43 +09:00
Junegunn Choi
be5a687281 Goods 2025-11-29 13:59:43 +09:00
RT
771e35b972 feat: add alt-gutter color option (#4602)
* Add alt-gutter color option

* Simplify the code

---------

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-11-29 10:43:13 +09:00
Junegunn Choi
60a5be1e65 Do not allow very long queries in FuzzyMatchV2
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4608
2025-11-28 18:41:45 +09:00
junegunn
1d5e87f5e4 Deploying to master from @ junegunn/fzf@3db63f5e52 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-23 00:02:24 +00:00
LangLangBart
3db63f5e52 fix(terminal): correct display width calculation with maxWidth (#4596)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
fix #4593

* test(core): add test for --freeze-right with long ellipsis
2025-11-20 09:09:36 +09:00
Junegunn Choi
2ab923f3ae 0.67.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-16 20:02:39 +09:00
Massimo Mund
c3e6d9a8f9 Distinguish between Ctrl-H and Ctrl-Backspace in Windows (#4590)
Since you can actually distinguish between Ctrl-H and Ctrl-Backspace in Windows we need to reintroduce the tui.CtrlH constant. On *nix systems we map all Ctrl(-Alt)-h to Ctrl(-Alt)-Backspace internally, but you can use either in --bind.
2025-11-16 20:00:24 +09:00
Junegunn Choi
2471edf3ff Make ctrl-alt-h a synonym of ctrl-alt-backspace on non-Windows environment (#4589) 2025-11-16 16:33:53 +09:00
junegunn
53a8aeeb72 Deploying to master from @ junegunn/fzf@60b35e748b 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-16 00:02:13 +00:00
Junegunn Choi
60b35e748b Header and footer should not be wider than the list
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Example:
  WIDE=$(printf 'x%.0s' {1..1000})
  (echo $WIDE; echo $WIDE) |
    fzf --header-lines 1 --style full --ellipsis XX --header "$WIDE" \
        --no-header-lines-border --footer "$WIDE" --no-footer-border
2025-11-15 11:41:51 +09:00
Junegunn Choi
3f499f055e Avoid truncating ellipsis to avoid confusion
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-13 23:00:32 +09:00
Junegunn Choi
1df99db0b2 Keep the previous delimiter before frozen columns 2025-11-13 22:38:49 +09:00
dependabot[bot]
535b610a6b Bump github/codeql-action from 3 to 4 (#4554)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [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/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  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>
2025-11-13 10:48:43 +09:00
phanium
91fab3b3c2 Fix lint warnings (#4586)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
2025-11-12 22:05:17 +09:00
Junegunn Choi
b9f2bf64ff Add --freeze-right=N option to keep the rightmost N fields visible 2025-11-12 22:00:27 +09:00
Junegunn Choi
07d53cb7e4 Add --freeze-left=N option to keep the leftmost N fields visible 2025-11-12 22:00:27 +09:00
Massimo Mund
ead534a1be Fix modifier detection for Backspace / Ctrl-H on Windows (#4582)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Windows sends different key events and modifier combinations to theFullscreenRenderer than a tcell FullscreenRenderer on Linux (-tags tcell).
This led to Ctrl+H being misinterpreted (and therefore unbindable) on some Windows builds.

Basically reverts changes to `src/tui/tcell.go` introduced by `a0cabe0`.
2025-11-10 19:12:01 +09:00
Junegunn Choi
8a05083503 Fix reading an extra key after a terminal action
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4578
2025-11-09 15:36:07 +09:00
phanium
e659b46ff5 feat: append spinner in the end when --info=inline (#4567)
Test:
  go run main.go --query "$(seq 100)" --info inline --border < <(sleep 60)
  go run main.go --query "$(seq 100)" --info inline --info-command 'echo hello' --border < <(sleep 60)

Close #4344
Close #619

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-11-09 10:44:27 +09:00
Junegunn Choi
991c36453c [man] Add --gutter-raw
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Close #4579
2025-11-08 17:50:25 +09:00
junegunn
4d563c6dfa Deploying to master from @ junegunn/fzf@5cb695744f 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-02 00:02:16 +00:00
Koichi Murase
5cb695744f [bash,zsh] Fix the version check for mawk (#4574)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
We have been checking the mawk version by extracting <x>, <y>, <z>,
and <d> part from "mawk <x>.<y>.<z> <d>" in the output of the "mawk -W
version" and testing <x>, <y>, <z>, and <d> using an arithmetic
evalaution.  However, <d> is ensured to be an integer only in "x.y.z
>= 1.3.4".  Otherwise, it may cause a syntax error in the arithmetic
evaluation.  The mawk started to include the date as an integer in the
<d> position only from mawk-1.3.3-20090721.  We should first check
that "x.y.z >= 1.3.4" and then check the value of "d".  In case, "mawk
-W version" produces a completely different text, we should also
redirect stderr of the arithmetic commands to /dev/null.
2025-10-31 21:14:41 +09:00
Junegunn Choi
c1b259c042 0.66.1
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-26 15:11:51 +09:00
junegunn
1a0371e2c7 Deploying to master from @ junegunn/fzf@aa259fdc19 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-26 00:02:16 +00:00
Junegunn Choi
aa259fdc19 Fix regression in --no-color / NO_COLOR theme
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4561
2025-10-21 19:49:43 +09:00
junegunn
b852dc8a56 Deploying to master from @ junegunn/fzf@a0cabe021d 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-19 00:02:19 +00:00
Junegunn Choi
a0cabe021d Fix bug preventing 'ctrl-h' from being bound to an action
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4556
2025-10-15 12:16:09 +09:00
Junegunn Choi
8cdfb23df6 0.66.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-12 22:17:52 +09:00
Junegunn Choi
4ffde48e2f Fix --bold inheritance
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4548
2025-10-12 13:58:46 +09:00
Junegunn Choi
f2b33f038a Revert "Make query string in --disabled state bold as before"
This reverts commit ab407c4645.
2025-10-12 13:58:46 +09:00
junegunn
d5913bf86e Deploying to master from @ junegunn/fzf@0e9026b817 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-12 00:02:08 +00:00
Jacobo de Vera
0e9026b817 feat: Allow disabling Ctrl-R binding in shell integration (#4535)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Close #4417
2025-10-12 01:57:31 +09:00
Junegunn Choi
ab407c4645 Make query string in --disabled state bold as before
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4546
2025-10-11 09:35:48 +09:00
Junegunn Choi
91c4bef35f Update CHANGELOG
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-10 03:41:37 +09:00
Junegunn Choi
bf77206221 Improve Unix domain socket handling
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
- Check if the file is in use
- Change the permission to 0600
2025-10-09 13:52:10 +09:00
Junegunn Choi
0cb1be3f04 Fix --help output: socket path cannot be omitted
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-09 01:12:30 +09:00
Junegunn Choi
01cb38a5fb Add Unix domain socket support for --listen
Close #4541
2025-10-09 01:07:59 +09:00
Junegunn Choi
c38c6cad79 Update CHANGELOG 2025-10-09 00:17:00 +09:00
Junegunn Choi
ba6fc40cfd Add 'best' to man page 2025-10-09 00:17:00 +09:00
Junegunn Choi
dd46a256c0 Fix offset-up and offset-down with --layout=reverse-list
Related: 3df06a1c68
2025-10-09 00:17:00 +09:00
Junegunn Choi
d19ce0ad8d Add 'best' action 2025-10-09 00:17:00 +09:00
Junegunn Choi
ed7becfb47 Go to the closest match when disabling raw mode 2025-10-09 00:17:00 +09:00
Junegunn Choi
9ace1351ff ADD $FZF_DIRECTION 2025-10-09 00:17:00 +09:00
Junegunn Choi
e1de29bc40 CTRL-R: Bind ALT-R to toggle-raw 2025-10-09 00:17:00 +09:00
Junegunn Choi
0df7d10550 Rename: '--color hidden' to '--color nomatch' 2025-10-09 00:17:00 +09:00
Junegunn Choi
91e119a77e Fix non-matching items not refreshing after clearing query 2025-10-09 00:17:00 +09:00
Junegunn Choi
3984161f6c Fix: 'hidden' style not applied to text without colors 2025-10-09 00:17:00 +09:00
Junegunn Choi
91beacf0f4 Add special 'strip' style attribute for stripping colors
Test cases:
  fd --color always | fzf --ansi --delimiter /
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim,nth:regular
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strikethrough --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strip:strikethrough --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strip:dim:strikethrough --raw
2025-10-09 00:17:00 +09:00
Junegunn Choi
e6ad01fb90 Revise color configuration 2025-10-09 00:17:00 +09:00
Junegunn Choi
ce2200e908 Do not allow gutter characters with width other than 1 2025-10-09 00:17:00 +09:00
Junegunn Choi
548061dbde --gutter ' ' --color gutter:reverse 2025-10-09 00:17:00 +09:00
Junegunn Choi
8f0c91545d Add $FZF_RAW for conditional actions 2025-10-09 00:17:00 +09:00
Junegunn Choi
0eefcf348e Update CHANGELOG 2025-10-09 00:17:00 +09:00
Junegunn Choi
c1f8d18a0c Add enable-raw and disable-raw actions 2025-10-09 00:17:00 +09:00
Junegunn Choi
8585969d6d Refactor action implementation 2025-10-09 00:17:00 +09:00
Junegunn Choi
8a943a9b1a Remove TODO comments 2025-10-09 00:17:00 +09:00
Junegunn Choi
c87a8eccd4 Add '--bind ctrl-x:toggle-raw' to CTRL-R bindings 2025-10-09 00:17:00 +09:00
Junegunn Choi
65df0abf0e Introduce 'raw' mode 2025-10-09 00:17:00 +09:00
junegunn
b51bc6b50e Deploying to master from @ junegunn/fzf@febaadbee5 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-10-05 00:02:16 +00:00
Junegunn Choi
febaadbee5 Fix stray character artifacts when scrollbar is hidden
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4537
2025-10-04 21:56:56 +09:00
dependabot[bot]
0e67c5aa7a Bump actions/setup-go from 5 to 6 (#4513)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  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>
2025-09-29 21:20:26 +09:00
mickychang9
760d1b7c58 refactor: use maps.Copy and maps.Clone (#4518)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Signed-off-by: mickychang9 <mickychang9@outlook.com>
2025-09-29 18:11:19 +09:00
Junegunn Choi
9bdacc8df2 Update --help output to avoid confusion
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-28 23:56:51 +09:00
junegunn
8e936ecfa7 Deploying to master from @ junegunn/fzf@db2e95b1f2 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-28 00:02:15 +00:00
Junegunn Choi
db2e95b1f2 Remove unused field
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-09-27 22:34:12 +09:00
alex-huff
687074e772 merger: fix chunk cache never getting cleared (#4531)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Commit 7fc13c5 indroduced less aggressive cache invalidation for the
chunk cache but saved the new revision before comparing it with the old
one, and so the cache was never considered invalid.

Fixes #4529
2025-09-27 09:01:13 +09:00
Junegunn Choi
3401c2e0c7 Remove alignment in .tool-versions for RuboCop
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-24 22:41:54 +09:00
Junegunn Choi
e8cb315419 Apply shfmt to bash script files (make fmt) 2025-09-24 22:41:54 +09:00
Junegunn Choi
f0c4ee4047 make lint: Perform bash script linting 2025-09-24 22:41:54 +09:00
xieyonn
de0df2422a feat: add make fmt for *.sh *.bash
1. add .editorconfig file, add rules for .sh .bash files.
2. add make fmt target, use:
    - gofmt *.go.
    - shfmt *.sh *.bash, shell/completion.bash, shell/key-bindings.bash
        need a left indent due to an outermost if block.
3. add shfmt check for bash scripts in make lint target.
4. install shfmt in actions.
2025-09-24 22:41:54 +09:00
Massimo Mund
148b0a94cd tui/light: consume full 7-byte CSI sequences to prevent leftover printing (#4528)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
- Fix parsing in escSequence so 7-byte CSI forms (e.g. ESC [ 5 ; 10 ~) set *sz = 7 and the entire sequence is consumed.
- Prevents trailing bytes (like 10~) from remaining in the input buffer and being printed as stray characters.
2025-09-23 23:33:41 +09:00
Junegunn Choi
ca294109c3 Apply RuboCop suggestions
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-22 21:23:54 +09:00
Junegunn Choi
9cad2686e9 Update .tool-versions
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-21 21:19:49 +09:00
junegunn
9a45172232 Deploying to master from @ junegunn/fzf@2a92c7d792 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-21 00:02:16 +00:00
Junegunn Choi
2a92c7d792 Adjust base16 (16) theme (#4501)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Motivation:

`--color base16` can be a better default than `dark` or `light`, since it uses
the colors defined by the current theme. This usually blends in more
naturally and works well in both light and dark modes.

However, some elements were previously hard-coded with white or black
foreground colors, which can cause rendering issues in certain terminal
themes.
2025-09-17 19:38:49 +09:00
Junegunn Choi
f5975cf870 Add --gutter to --help and man page
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-16 21:30:01 +09:00
Junegunn Choi
a67aa85820 Style change: thinner gutter column (#4521) 2025-09-16 21:22:56 +09:00
dependabot[bot]
c5cabe1691 Bump github.com/charlievieth/fastwalk from 1.0.13 to 1.0.14 (#4522)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: github.com/charlievieth/fastwalk
  dependency-version: 1.0.14
  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>
2025-09-16 01:19:14 +09:00
Junegunn Choi
cbed41cd82 No emoji
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-14 21:08:48 +09:00
Junegunn Choi
6684771cbf Fix CTRL-Z for tcell renderer by using the official API
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
See https://github.com/gdamore/tcell/pull/431
2025-09-14 11:41:12 +09:00
Junegunn Choi
f5f894ea47 Fix rendering of multiple OSC 8 links in a single line
Fix #4517
2025-09-14 11:26:47 +09:00
junegunn
a0a334fc8d Deploying to master from @ junegunn/fzf@ae12e94b1f 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-09-07 00:02:03 +00:00
Massimo Mund
ae12e94b1f Add sub-word actions (#3997)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Add `backward-subword`, `forward-subword`, `kill-subword`, `backward-kill-subword` actions.
2025-09-05 19:38:22 +09:00
Massimo Mund
9ed971cc90 Add keybindings for CTRL, ALT, SHIFT + UP, DOWN, RIGHT, LEFT, HOME, END, BACKSPACE, DELETE & more (#3996)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
* Added tests for `LightRenderer`

* Added common SHIFT, ALT and ALT+SHIFT key sequences

* Added common CTRL key sequences

* Added common CTRL+ALT, CTRL+SHIFT, CTRL+ALT+SHIFT key sequences

* Added proper xterm META modifier handling

according to defc6dd568/input.c (L357-L375)

* Fix `ctrl-backspace` and `ctrl-alt-backspace`

* Fix broken tcell tests on windows by swallowing Resize events

* Added tests for  FullscreenRenderer

* Removed own fork of tcell and updated tcell to 2.9.0

tcell 2.9.0 is needed for `Ctrl-Alt-*` and `Ctrl-Alt-Shift-*` shortcuts in Windows

* Replace conditional checks with switch statements to improve readability

* Replace long conditionals with constant slices to improve readability

* Bind `ctrl-bspace` (`ctrl-h`) to `backward-delete-char` by default

Since we now distinguish between Backspace and Ctrl-Backspace, Ctrl-Backspace should trigger the same action as Backspace by default. In that way nothing changes for the user but you can bind other actions to Ctrl-Backspace when desired.
2025-09-05 14:56:51 +09:00
Junegunn Choi
129cb23078 Require Go 1.23 2025-09-05 14:45:17 +09:00
dependabot[bot]
d22812e917 Bump github.com/gdamore/tcell/v2 from 2.8.1 to 2.9.0 (#4503)
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.8.1 to 2.9.0.
- [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.8.1...v2.9.0)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-version: 2.9.0
  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>
2025-09-05 14:39:37 +09:00
Charlie Vieth
10d712824a mod: update charlievieth/fastwalk to v1.0.13 and min Go version to 1.21 (#4508)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
This commit updates github.com/charlievieth/fastwalk to v1.0.13 which
addresses fastwalk issue #61. It also updates the minimum supported Go
version to 1.21 (up from 1.20) since that is now the minimum version
supported by fastwalk.
2025-09-04 22:04:52 +09:00
Junegunn Choi
de4059c8fa Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-09-03 08:18:57 +09:00
Junegunn Choi
416aff86e9 0.65.2
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-31 22:18:44 +09:00
zhedazijingang
59dc7f178f refactor: replace []byte(fmt.Sprintf) with fmt.Appendf (#4507)
Signed-off-by: zhedazijingang <unwrap_or_else@outlook.com>
2025-08-31 22:01:35 +09:00
junegunn
a3c9f8bfee Deploying to master from @ junegunn/fzf@5546c65491 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-31 00:02:03 +00:00
Junegunn Choi
5546c65491 Fix rendering of items with tabs when using a non-default ellipsis
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4505
2025-08-27 23:31:31 +09:00
junegunn
f2179f015c Deploying to master from @ junegunn/fzf@9a53d84b9c 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-24 00:02:30 +00:00
Junegunn Choi
9a53d84b9c Update README.md
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-08-22 22:51:04 +09:00
Junegunn Choi
0a8ff7899c Do not unset FZF_DEFAULT_* variables when using winpty
Fix #4497
Fix #4400
2025-08-22 19:24:01 +09:00
xty
f9d7877d8b [bash 3] Fix CTRL-T and ALT-C to preserve the last yank (#4496)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-19 23:31:02 +09:00
Peter Sideris
9fe9976591 Fix a typo in man page (#4495) 2025-08-19 23:25:57 +09:00
Chayoung You
de1824f71d [install] Support old uname in macOS (#4492)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-17 11:54:32 +09:00
dependabot[bot]
19a9296c47 Bump actions/checkout from 4 to 5 (#4485)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [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/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-15 21:17:29 +09:00
Ioannis Pinakoulakis
49967f3d45 Use fixed-length array when possible (#4488) 2025-08-15 21:16:41 +09:00
longhutianjie
978b6254c7 chore: remove redundant word in comment (#4490)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Signed-off-by: longhutianjie <keplrnewton@icloud.com>
2025-08-14 13:26:29 +09:00
Junegunn Choi
1afd143810 Fix incorrect truncation of --info-command with --info=inline-right
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4479
2025-08-08 18:51:24 +09:00
Junegunn Choi
e5cd7f0a3a 0.65.1
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-03 14:41:56 +09:00
junegunn
51d3940c63 Deploying to master from @ junegunn/fzf@179aec1578 🚀 2025-08-03 00:02:30 +00:00
Junegunn Choi
179aec1578 Fix '--color nth:regular' not to reset ANSI attributes of the original text
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-08-03 00:54:26 +09:00
Junegunn Choi
af0014aba8 Fix a bug where you cannot unset the default --nth using change-nth
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-08-03 00:29:05 +09:00
Junegunn Choi
da3d995709 Fix $FZF_CLICK_{HEADER,FOOTER}_WORD with ANSI colors and tabs 2025-08-02 16:47:09 +09:00
Junegunn Choi
04c4269db3 0.65.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-27 10:39:41 +09:00
junegunn
78f238294f Deploying to master from @ junegunn/fzf@354d0468c1 🚀 2025-07-27 00:02:23 +00:00
LangLangBart
354d0468c1 fix(shell): check for mawk existence before version check (#4468)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
close #4463
2025-07-25 17:33:18 +09:00
Junegunn Choi
4efcc344c3 Add 'trigger(KEY_OR_EVENT[,...])' action
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-23 19:41:06 +09:00
Junegunn Choi
5818b58350 Better fix for #4465 - remove unnecessary erase 2025-07-23 19:30:52 +09:00
Junegunn Choi
7941129cc4 Add 'click-footer' event
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-22 23:24:23 +09:00
Junegunn Choi
069d71a840 Fix rendering error when hiding a preview window without border
This was a regression introduced in cdcab267.

Fix #4465
2025-07-22 19:23:10 +09:00
Junegunn Choi
08027e7a79 Fix --no-header-lines-border behavior
It should be different from --header-lines-border=none according to the
man page. It should merge two headers unlike the latter.
2025-07-22 19:16:55 +09:00
Junegunn Choi
ead302981c Add support for {*n} and {*nf} placeholder
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4458
2025-07-20 10:53:58 +09:00
junegunn
fe0ffa14ff Deploying to master from @ junegunn/fzf@821b8e70a8 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-20 00:02:23 +00:00
Junegunn Choi
821b8e70a8 [neovim] Fix margin background color when &winborder is used
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4453
2025-07-19 16:19:48 +09:00
Jaseem Abid
8ceda54c7d Fix a typo in README.md (#4459)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-16 23:19:43 +09:00
junegunn
84e515bd6e Deploying to master from @ junegunn/fzf@dea1df6878 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-13 00:02:29 +00:00
Junegunn Choi
dea1df6878 Add missing mention of 'bg-cancel' to the man page
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-07-12 20:09:54 +09:00
Junegunn Choi
0076ec2e8d 0.64.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-06 22:11:36 +09:00
Junegunn Choi
82c9671f79 Fix selection lost on revision bump 2025-07-06 22:02:12 +09:00
Junegunn Choi
d364a1122e Fix regression where header is not updated 2025-07-06 20:24:23 +09:00
Junegunn Choi
fb570e94e7 Update: make generate 2025-07-06 20:03:13 +09:00
Junegunn Choi
6e3c830cd2 Add 'multi' event triggered on multi-selection changes
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-06 10:05:25 +09:00
junegunn
d7db7fc132 Deploying to master from @ junegunn/fzf@ff1550bb38 🚀 2025-07-06 00:02:27 +00:00
Junegunn Choi
ff1550bb38 Normalize halfwidth and fullwidth characers for matching
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-07-03 20:57:19 +09:00
Junegunn Choi
976001e474 Explain the need to escape placeholders in transform actions
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-07-02 22:26:56 +09:00
Junegunn Choi
531dd6fb4f Update copyright year 2025-07-02 22:10:05 +09:00
Junegunn Choi
ba035f2a76 Run preview command when preview window appears after CTRL-Z
80b8846318
2025-07-02 21:40:02 +09:00
Junegunn Choi
d34675d3c9 Fix panic caused by incorrect update ordering
Fix #4442

Make sure to prepare windows before rendering elements.

Thanks to @nugged for the report.
2025-07-02 21:28:11 +09:00
junegunn
ce95adc66c Deploying to master from @ junegunn/fzf@397fe8e395 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-29 00:02:28 +00:00
Junegunn Choi
397fe8e395 0.63.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-06-28 01:11:00 +09:00
Junegunn Choi
111266d832 Reset full-background property after a new line 2025-06-27 23:21:48 +09:00
dependabot[bot]
19d858f9b6 Bump github.com/charlievieth/fastwalk from 1.0.10 to 1.0.12 (#4431)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.10 to 1.0.12.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.10...v1.0.12)

---
updated-dependencies:
- dependency-name: github.com/charlievieth/fastwalk
  dependency-version: 1.0.12
  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>
2025-06-27 15:47:41 +09:00
Junegunn Choi
79690724d8 Fix exact boundary match with --scheme=path or --tiebreak end
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4438
2025-06-26 22:33:58 +09:00
Junegunn Choi
5ed87ffcb9 Fix highlight offsets of multi-line entries
Fix regression from 4811e52a
2025-06-26 20:48:34 +09:00
Junegunn Choi
b99cb6323f Update CHANGELOG
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-25 08:34:06 +09:00
Junegunn Choi
debf3d8a8a Refactor ANSI parser 2025-06-25 08:26:14 +09:00
Junegunn Choi
4811e52af3 Support full-line background color in the list section
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4432
2025-06-25 02:12:10 +09:00
Junegunn Choi
8d81730ec2 with-nth: Do not trim trailing whitespaces with background colors
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Example:
  echo -en '  \e[48;5;232mhello\e[48;5;147m  ' | fzf --ansi --with-nth 1
2025-06-24 20:27:24 +09:00
Junegunn Choi
330a85c25c Allow \e[K in addition to \e[0K for full-line background
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-23 22:12:32 +09:00
Junegunn Choi
3a21116307 Terminate running background transform on exit (addendum)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Close #4422
2025-06-22 01:53:25 +09:00
Junegunn Choi
247d168af6 Terminate running background transform on exit
Close #4422
2025-06-21 23:24:38 +09:00
Junegunn Choi
b2a8a283c7 Reorganize code to ensure deletion of temp files 2025-06-21 23:06:46 +09:00
Junegunn Choi
c36ddce36f Add bg-cancel action to ignore running background transforms
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4430

Example:

  # Implement popup that disappears after 1 second
  #   * Use footer as the popup
  #   * Use `bell` to ring the terminal bell
  #   * Use `bg-transform-footer` to clear the footer after 1 second
  #   * Use `bg-cancel` to ignore currently running background transform actions
  fzf --multi --list-border \
      --bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
      --bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
      --bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
2025-06-21 17:28:48 +09:00
Junegunn Choi
c35d9cff7d Avoid full redraw when changing header and footer windows 2025-06-21 12:40:56 +09:00
Junegunn Choi
549ce3cf6c Do not reserve a single column at the end when scrollbar is hidden
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4410

Example:
    fzf --pointer '' --marker '' --no-scrollbar --wrap --wrap-sign ''
2025-06-20 08:22:58 +09:00
Junegunn Choi
575bc0768c Update .goreleaser to build android_arm64 binary (#4428)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-19 22:57:16 +09:00
Junegunn Choi
89334e881e Update man page and changelog 2025-06-19 22:56:41 +09:00
Junegunn Choi
dcec6354f5 Add {*} placeholder flag 2025-06-19 22:35:23 +09:00
Junegunn Choi
16d338da84 Revert "Add {*} placeholder flag"
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
This reverts commit 27258f7207.
2025-06-19 12:39:31 +09:00
Junegunn Choi
27258f7207 Add {*} placeholder flag
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-19 01:04:59 +09:00
曹家巧
4d2d6a5ced chore: fix function name (#4425)
Signed-off-by: xiaoxiangirl <caojiaqiao@outlook.com>
2025-06-19 00:47:14 +09:00
Junegunn Choi
0c00b203e6 Implement asynchronous transform actions (#4419)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4418

Example:

    fzf --bind 'focus:bg-transform-header(sleep 2; date; echo {})'
2025-06-16 00:39:11 +09:00
Junegunn Choi
3b68dcdd81 Add footer
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Options:
  --footer=STR             String to print as footer
  --footer-border[=STYLE]  Draw border around the footer section
                           [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
                            top|bottom|left|right|line|none] (default: line)
  --footer-label=LABEL     Label to print on the footer border
  --footer-label-pos=COL   Position of the footer label
                           [POSITIVE_INTEGER: columns from left|
                            NEGATIVE_INTEGER: columns from right][:bottom]
                           (default: 0 or center)

The default border type for footer is 'line', which draws a single
separator between the footer and the list. It changes its position
depending on `--layout`, so you don't have to manually switch between
'top' and 'bottom'

The 'line' style is now supported by other border types as well.
`--list-border` is the only exception.
2025-06-10 23:02:23 +09:00
Junegunn Choi
39db026161 Fix inconsistent placement of header-lines with border options
fzf displayed --header-lines inconsistently depending on the presence of borders:

  # --header and --header-lines co-located
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first

  # --header and --header-lines separated
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first --header-lines-border

This commit fixes the inconsistency with the following logic:

* If only one of --header or --header-lines is provided, --header-first
  applies to that single header.
* If both are present, --header-first affects only the regular --header,
  not --header-lines.
2025-06-10 23:02:23 +09:00
Koichi Murase
f6c589c606 [bash,zsh] Skip comments in ~/.ssh/config
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
For the line "Host host1 # this is a comment", the current
implementation generates words in an inline comment as hostnames.
This patch removes the comment before generating the hostname.
2025-06-09 21:46:53 +09:00
Koichi Murase
2bd29c3172 [bash,zsh] Support "=" after "Hostname" and "Host" in ~/.ssh/config
In ~/.ssh/config, "=" can also be used as a separator between the
field name and the value.  The current master does not properly handle
this and generate a hostname "=" or one starting with "=".  This patch
correctly handles it.
2025-06-09 21:46:53 +09:00
Koichi Murase
4a61f53b85 [bash,zsh] Remove redundant filtering-out of comment/blank lines
Comments are anyway removed in the subsequent call to `sub(/#.*/,
"")`, and it becomes a blank line.  Blank lines do not have fields, so
they are ignored in the next for-loop.
2025-06-09 21:46:53 +09:00
Koichi Murase
adc9ad28da [bash,zsh] Correctly exclude the hostname "0.0.0.0"
In the current implementation, any hostnames in /etc/hosts containing
"0.0.0.0" as a part (such as "110.0.0.0" would be excluded.  "0.0.0.0"
should be checked by the exact match.
2025-06-09 21:46:53 +09:00
Koichi Murase
585cfaef8b [bash,zsh] Do not end the hostname analysis on "]" in ~/.ssh/known_hosts
An entry of the form `[example.com]:port,192.168.0.1 ...` in
~/.ssh/known_hosts are not properly processed.  The current
implementation gives up the matching on the first occurrence of `]`,
the subsequent 192.168.0.1 would not be extracted.  This patch
continues the analysis and removes "]" together with "[".

This patch also removes the ":port" part from the hostnames in
~/.ssh/known_hosts.  One cannot use the form "hostname:port" in the
arguments to the ssh command anyway.
2025-06-09 21:46:53 +09:00
Koichi Murase
b5cd8880b1 [bash,zsh] Process hostnames with uppercase letters in known_hosts 2025-06-09 21:46:53 +09:00
junegunn
44ddab881e Deploying to master from @ junegunn/fzf@bfa287b66d 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-08 00:02:27 +00:00
Koichi Murase
bfa287b66d [bash,zsh] Separate common functions into "shell/common.sh"
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-06-08 00:00:17 +09:00
Koichi Murase
243e52fa11 [bash,zsh] Work around mawk 1.3.3-20090705 not supporting the POSIX brackets 2025-06-08 00:00:17 +09:00
Koichi Murase
c166eaba6d [bash,zsh] Work around Solaris awk, which is non-standard
Solaris awk at /usr/bin/awk is meant for backward compatibility with
an ancient implementation of 1977 awk in the original UNIX.  It lacks
many features of POSIX awk.  To use a standard-conforming version in
Solaris, one needs to explicitly use /usr/xpg4/bin/awk.
2025-06-08 00:00:17 +09:00
Koichi Murase
09194c24f2 [bash,zsh] Work around a quirk of macOS awk
macOS awk is a variant of nawk, but it contains a unique patch for the
UTF-8 support.  However, this patch causes the problem.  If the input
contains any non-UTF-8 data, macOS awk stops processing and does not
do anything, instead of ignoring the unrecognized data and continue
the processing.  However, the contents of the ssh configuration and
/etc/hosts is not under the control of fzf, so we cannot fix the input
when those files contain non-UTF-8 data.  To work around this
behavior, one can set the locale to LC_ALL=C to treat the input data
with the plain 8-bit encoding.
2025-06-08 00:00:17 +09:00
Koichi Murase
ec521e47aa [bash,zsh] Reduce the number of fork & exec
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-05 13:02:11 +09:00
Koichi Murase
e3f4a51c18 [zsh] Set shell options for pathname expansion "~/.ssh/config.d/*"
This applies the same changes as commit 0a06fd6f for Bash (GitHub PR
2025-06-05 13:02:11 +09:00
Koichi Murase
0a06fd6f63 [bash] Set shell options for pathname expansion "~/.ssh/config.d/*" (#4405)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-03 20:47:28 +09:00
Koichi Murase
70eace5290 Fix the CI failure for PR caused by a spelling mistake (#4406) 2025-06-03 19:41:47 +09:00
junegunn
40f9f254a9 Deploying to master from @ junegunn/fzf@15d6c17390 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-06-01 00:02:34 +00:00
Junegunn Choi
15d6c17390 Fix ANSI attributes lost when nth:regular is set
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Example:
  # foo was not displayed in italic
  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color fg:dim,nth:regular --nth 1
2025-05-30 21:02:35 +09:00
Junegunn Choi
a9d1d42436 Fix ANSI attributes lost when 'regular' attribute is set to fg or nth
Examples:

  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color fg:regular
  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color nth:regular
2025-05-30 20:43:20 +09:00
Junegunn Choi
1ecfa38eee [bash] Fix 'complete' errors when IFS is newline
Fix #4342
2025-05-30 20:41:50 +09:00
Junegunn Choi
54fd92b7dd --no-color: Keep ANSI attributes in the list
Example:
  echo -e "\x1b[33;3mfoo \x1b[34;4mbar\x1b[m baz" | fzf --ansi --no-color
2025-05-30 20:33:21 +09:00
Junegunn Choi
835906d392 --no-color: Keep ANSI attributes in preview window
Example:
  fzf --preview 'echo -e "\x1b[33;3mfoo \x1b[34;4mbar\x1b[m baz"' --no-color
2025-05-30 20:26:53 +09:00
Junegunn Choi
1721e6a1ed Do not apply 'nth' attributes to trailing whitespaces
# foo  bar
    # -----    <- previously underlined trailing whitespace
    # ---      <- with the fix, trailing whitespace is excluded
    fzf --color nth:underline --nth 1 <<< 'foo  bar'
2025-05-30 19:43:10 +09:00
Junegunn Choi
c7ee3b833f Fix FZF_CLICK_HEADER_NTH for multi-line headers 2025-05-30 17:10:26 +09:00
Junegunn Choi
ffb6e28ca7 Allow customizing --ghost color via '--color ghost'
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Examples:

  # Dimmed red
  fzf --ghost booya --color ghost:red

  # Regular red
  fzf --ghost booya --color ghost:red:regular

Close #4398
2025-05-28 00:27:33 +09:00
Junegunn Choi
a4c6846851 Fix background color of 'disabled' query
fzf --color disabled:red,list-bg:blue --disabled --query foo --input-border
2025-05-28 00:17:41 +09:00
Junegunn Choi
d18c0bf694 [man] Add GET endpoint example
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-05-22 21:57:04 +09:00
Junegunn Choi
4e3f9854e6 Update README.md
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-05-17 22:06:06 +09:00
Junegunn Choi
b27943423e Show ellipsis for truncated labels
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4390
2025-05-17 11:25:15 +09:00
Junegunn Choi
894a1016bc RuboCop lint 2025-05-17 11:20:29 +09:00
Junegunn Choi
efe6cddd34 Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-05-16 22:15:38 +09:00
Junegunn Choi
f1c6bdf3e8 Update README 2025-05-16 22:15:06 +09:00
Junegunn Choi
710659bcf5 Update SECURITY.md
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-05-14 11:06:15 +09:00
Josef Andersson
be67775da4 Add initial security policy (#4379)
Signed-off-by: Josef Andersson <janderssonse@proton.me>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 11:05:20 +09:00
jiz4oh
2c6381499c [neovim] Respect winborder of Neovim 0.11+ (#4389)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 00:53:26 +09:00
junegunn
4df842e78c Deploying to master from @ junegunn/fzf@b81696fb64 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-05-11 00:02:09 +00:00
Ajeet D'Souza
b81696fb64 bash: set keybinding right before printing special character (#4377)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-05-10 15:29:27 +09:00
Junegunn Choi
d226d841a1 0.62.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-05-04 18:31:18 +09:00
Junegunn Choi
c6d83047e5 Allow whitespace as separator in --color option 2025-05-04 15:08:23 +09:00
Junegunn Choi
46dabccdf1 [vim] Update g:fzf_colors example with 'query' 2025-05-04 14:52:22 +09:00
Junegunn Choi
cd9517b679 Add 'alt-bg' color for striped lines (#4370)
Test cases:

1. 'jump' should show alternating background colors even when 'alt-bg' is
not defined as before.

  go run main.go --bind load:jump

Two differences:
  * The alternating lines will not be in bold (was a bug)
  * The marker column will not be rendered with alternating background color

2. Use alternating background color when 'alt-bg' is set

  go run main.go --color bg:238,alt-bg:237
  go run main.go --color bg:238,alt-bg:237 --highlight-line

3. 'selected-bg' should take precedence

  go run main.go --color bg:238,alt-bg:237,selected-bg:232 \
                 --highlight-line --multi --bind 'load:select+up+select+up'

4. Should work with text with ANSI colors

  declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
    bat --plain --language bash --color always |
    go run main.go --read0 --ansi --reverse --multi \
                   --color bg:237,alt-bg:238,current-bg:236 --highlight-line

---

Close #4354
Fix #4372
2025-05-04 14:32:06 +09:00
junegunn
cd6677ba1d Deploying to master from @ junegunn/fzf@9c1a47acf7 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-05-04 00:02:25 +00:00
bitraid
9c1a47acf7 [fish] Support deleting history items with SHIFT-DEL
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Bind to SHIFT-DELETE a command that deletes the selected history items.
It can be overridden by $FZF_CTRL_R_OPTS.
2025-04-28 00:27:51 +09:00
bitraid
0c280a3ce1 [fish] Simplify commandline call in fzf-file-widget 2025-04-28 00:27:51 +09:00
bitraid
53e8b6e705 [fish] Add version check 2025-04-28 00:27:51 +09:00
bitraid
ad33165fa7 [fish] History: Operate only on line at cursor
This allows inserting history entries when constructing multiline
commands.
2025-04-28 00:27:51 +09:00
junegunn
2055db61c8 Deploying to master from @ junegunn/fzf@d2c662e54f 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-27 00:02:22 +00:00
Junegunn Choi
d2c662e54f Reset coordinator delay on 'reload'
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Fix #4364
2025-04-25 21:30:25 +09:00
Junegunn Choi
d24b58ef3f 0.61.3
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-22 20:53:23 +09:00
RafaelDominiquini
06ae9b0f3b Add missing environment variables (#4356)
Co-authored-by: Rafael Baboni Dominiquini <rafaeldominiquini@gmail.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-04-22 20:51:43 +09:00
Junegunn Choi
2a9c1c06a4 Revert "Disable tmux popup when already running inside one (#4351)"
This reverts commit af8fe918d8.

Fix #4360
Fix #4359
2025-04-22 20:20:21 +09:00
Junegunn Choi
90ad1b7f22 0.61.2
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-20 11:37:15 +09:00
Junegunn Choi
f22fbcd1af Fix typo and update CHANGLOG 2025-04-20 11:31:15 +09:00
Junegunn Choi
1d761684c5 Add --tty-default=/dev/tty and --no-tty-default option (#4352)
Fix #4242.

Use --no-tty-default, if you want fzf to perform a TTY look-up instead of defaulting to /dev/tty.
2025-04-20 11:24:50 +09:00
bitraid
e491770f1c [fish] Improve option prefix processing
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
- Support single-letter options without = such as -fFILEPATH
- fish v3.3.0 and newer: Disable option prefix if -- is preceded
2025-04-18 21:06:25 +09:00
bitraid
a41be61506 [fish] Fix whitespace/regex characters in command line
This is a rewrite of __fzf_parse_commandline function, that fixes the
following issues, when CTRL-T/ALT-C is used and current command line
token contains:
- Escaped newlines (\n): This never worked correctly, but after 282884a,
  the string would split, and the script would enter an infinite loop
  while trying to set $dir.
- Escaped bell (\a, \cg), backspace (\b), form feed (\v, \cl), carriage
  return (\r), vertical tab (\v, \ck): walker-root would not set
  correctly for existing directories containing any of those characters.
- Regular expression special characters (^, +, ? etc): $dir would not be
  be stripped from $fzf_query if it contained any of those characters.

The lowest supported fish version is v3.1b. For optimal operation, the
function uses more recent commands when supported by the running
version. Specifically, for versions equal or newer than:
- v3.2.0: Sets variables using PCRE2 capture groups of `string match
  --regex` when needing to preserve any trailing newlines and
  simultaneously omit the extra newline that is appended by `string
  collect -N`.
- v3.5.0: Uses the builtin path command for path normalization, dirname
  extraction and existing directories check.
- v4.0.0: Uses the --tokens-expanded option of commandline, for
  expansion and dealing with unbalanced quotes and incomplete escape
  sequences. It also uses the regex style of string-escape, to prepare
  variable contents for regex operations. This is not used in older
  versions, because they don't escape newlines.
2025-04-18 21:06:25 +09:00
bitraid
1a8f633611 [fish] Fix for file/dir names containing newlines
CTRL-T/ALT-C now works correctly when selecting files or directories
that contain newlines in their names. When external commands defined by
$FZF_CTRL_T_COMMAND/$FZF_ALT_C_COMMAND are used (for example the fd
command with -0 switch), the --read0 option must also be set through
$FZF_CTRL_T_OPTS/$FZF_ALT_C_OPTS.
2025-04-18 21:06:25 +09:00
Pierre Guinoiseau
af8fe918d8 Disable tmux popup when already running inside one (#4351) 2025-04-18 17:35:48 +09:00
istepic
8ef9dfd9a2 Update reference to manpage in README.md (#4348)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-18 08:38:28 +09:00
phanium
66df24040f Fix panic when use header border without pointer/marker (#4345)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-13 20:24:29 +09:00
junegunn
ed4442d9ea Deploying to master from @ junegunn/fzf@0edb5d5ebb 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-13 00:26:08 +00:00
Junegunn Choi
0edb5d5ebb Fix trailing ␊ not rendered with '--read0 --no-multi-line'
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
https://github.com/junegunn/fzf/pull/4334#issue-2966013714

    # Should display foo␊
    echo -en "foo\n" | fzf --read0  --no-multi-line
2025-04-11 20:46:49 +09:00
Junegunn Choi
9ffc2c7ca3 reader: Do not append '/' to '/'
https://github.com/junegunn/fzf/pull/4334#issue-2966013714
2025-04-11 20:38:16 +09:00
Junegunn Choi
93cb3758b5 0.61.1
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-04-06 13:09:59 +09:00
Junegunn Choi
d22e75dcdd Disable bracketed paste mode on exit
Related: #4338
2025-04-06 12:51:36 +09:00
junegunn
a1b2a6fe2c Deploying to master from @ junegunn/fzf@e15cba0c8c 🚀 2025-04-06 00:02:12 +00:00
Junegunn Choi
e15cba0c8c 0.61.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-03-30 19:51:28 +09:00
Junegunn Choi
31fd207ba2 Add 'r' flag (raw) for unquoted output
By default, placeholder expressions are automatically quoted to ensure
they are safely passed as arguments to external programs.

The r flag ({r}, {r1}, etc.) disables this behavior, outputting the
evaluated value without quotes.

For example,

  echo 'foo   bar' | fzf --preview 'echo {} {r}'

The preview command becomes:

  echo 'foo   bar' foo   bar

Since `{r}` expands to unquoted "foo   bar", 'foo' and 'bar' are passed
as separate arguments.

**Use with caution** Unquoted output can lead to broken commands.

  echo "let's go" | fzf --preview 'echo {r}'

Close #4330
2025-03-30 19:49:05 +09:00
Junegunn Choi
ba6d1b8772 Add change-ghost and transform-ghost
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-03-28 23:35:20 +09:00
Junegunn Choi
0dce561ec9 Fix header window not updated on change-header 2025-03-28 23:23:43 +09:00
dependabot[bot]
376142eb0d Bump github.com/charlievieth/fastwalk from 1.0.9 to 1.0.10 (#4307)
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.9 to 1.0.10.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.9...v1.0.10)

---
updated-dependencies:
- dependency-name: github.com/charlievieth/fastwalk
  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>
2025-03-28 23:05:34 +09:00
Junegunn Choi
664ee1f483 Add change-pointer and transform-pointer
Close #4178
2025-03-28 21:28:25 +09:00
Junegunn Choi
dac5b6fde1 Fix info not updated after track-current is disabled due to race condition
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-03-26 16:00:05 +09:00
Junegunn Choi
998c57442b Fix query precedence in an action chain (#4326)
When 'search' and any action that modifies the query are in an action
chain, anything that comes later takes precedence.
2025-03-26 15:47:43 +09:00
Junegunn Choi
4a0ab6c926 Improve query modification prevention in input-less mode
fzf would restore the original query in input-less mode after executing
a chain of actions.

This commit changes the behavior so that the restoration
happens after each action to allow something like
'show-input+change-query(...)+hide-input'.

Fix #4326
2025-03-26 10:34:52 +09:00
Junegunn Choi
f43e82f17f Do not ignore current query when input is hidden
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
* The initial query given by --query should be respected
* The current query should still be respected after `hide-input`
  (or `toggle-input)

Fix #4327
2025-03-25 21:08:06 +09:00
Junegunn Choi
62238620a5 Fix first entry not clickable when input section is hidden
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4325
2025-03-24 22:08:57 +09:00
Junegunn Choi
200745011a Fix cursor position when prompt is truncated
e.g.
    fzf --preview 'cat {}' --prompt "$(seq 100 | xargs)"
    fzf --preview 'cat {}' --prompt "$(seq 100 | xargs)" --input-border
2025-03-24 17:09:44 +09:00
Junegunn Choi
82fd88339b Fix offset-middle not updating the list
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-03-23 11:13:21 +09:00
junegunn
de0f2efbfb Deploying to master from @ junegunn/fzf@29cf28d845 🚀 2025-03-23 00:02:20 +00:00
Junegunn Choi
29cf28d845 Suppress 'change' event during bracketed paste mode
Some checks failed
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
Generate Sponsors README / deploy (push) Has been cancelled
Close #4316
2025-03-22 09:17:18 +09:00
Junegunn Choi
7e4dbb5f3b Prevent start:track-current from being disabled
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
# track-current state can be immediately disabled
  fzf --sync --bind 'start:track-current'
2025-03-20 11:51:20 +09:00
Junegunn Choi
923c3a814d [bash] Fix $FZF_COMPLETION_{DIR,PATH}_OPTS to support non-trivial arguments
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
This used to fail with 'unknown option: World>'

  export FZF_COMPLETION_PATH_OPTS="--prompt 'Hello World> '"
2025-03-17 18:12:26 +09:00
Junegunn Choi
779e3cc5b5 [vim] Use 24-bit colors on gvim even when &termguicolors is off
Close #2563
2025-03-17 17:46:56 +09:00
junegunn
3f3d1ef8f5 Deploying to master from @ junegunn/fzf@f92f9f137a 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-03-16 00:02:19 +00:00
Junegunn Choi
f92f9f137a Fix wrapping of the list section
Some checks failed
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
Generate Sponsors README / deploy (push) Has been cancelled
# The first line of the second chunk would prematurely wrap
  printf '%0500s\n\n%0500s' 0 0 | fzf --wrap --read0
2025-03-16 01:57:20 +09:00
Junegunn Choi
87f7f436e8 Fix ghost text with inline info
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
Fix #4312
2025-03-15 18:42:08 +09:00
Junegunn Choi
4298c0b1eb Add --ghost=TEXT to display a ghost text when the input is empty
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
2025-03-14 16:46:23 +09:00
Gabriel Marin
6c104d771e Change 'interface{}' to 'any' (#4308)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-03-11 14:24:54 +09:00
Junegunn Choi
aefb9a5bc4 Nullify unwanted FZF_DEFAULT_* variables in tmux popup
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
Fix #4298
2025-03-10 18:18:50 +09:00
Junegunn Choi
8868d7cbb8 Add .idea to .gitignore 2025-03-10 18:15:53 +09:00
junegunn
10cbac20f9 Deploying to master from @ junegunn/fzf@26bcd0c90d 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-03-09 00:01:51 +00:00
Junegunn Choi
26bcd0c90d README: Sponsors ❤️
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Test fzf on Linux / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-03-04 18:30:50 +09:00
Junegunn Choi
fbece2bb67 Update README
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
Test fzf on Linux / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
2025-03-04 17:43:02 +09:00
94 changed files with 9754 additions and 2617 deletions

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
root = true
[*.{sh,bash}]
indent_style = space
indent_size = 2
simplify = true
binary_next_line = false
switch_case_indent = true
space_redirects = true
function_next_line = false
# also bash scripts.
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
indent_style = space
indent_size = 2
simplify = true
binary_next_line = false
switch_case_indent = true
space_redirects = true
function_next_line = false

View File

@@ -27,18 +27,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View File

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

View File

@@ -1,5 +1,5 @@
---
name: Test fzf on Linux
name: build
on:
push:
@@ -18,22 +18,22 @@ jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.20"
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
ruby-version: 3.4.6
- name: Install packages
run: sudo apt-get install --yes zsh fish tmux
run: sudo apt-get install --yes zsh fish tmux shfmt
- name: Install Ruby gems
run: bundle install

View File

@@ -15,14 +15,14 @@ jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.20"
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
@@ -30,7 +30,7 @@ jobs:
ruby-version: 3.0.0
- name: Install packages
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
- 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

View File

@@ -3,13 +3,13 @@ name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 0
- cron: 0 15 * * 6
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1

View File

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

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ gopath
fzf
tmp
*.patch
.idea

View File

@@ -14,6 +14,7 @@ builds:
- windows
- freebsd
- openbsd
- android
goarch:
- amd64
- arm
@@ -21,10 +22,11 @@ builds:
- loong64
- ppc64le
- s390x
- riscv64
goarm:
- 5
- 6
- 7
- "5"
- "6"
- "7"
flags:
- -trimpath
ldflags:
@@ -38,6 +40,12 @@ builds:
goarch: arm64
- goos: openbsd
goarch: arm64
- goos: openbsd
goarch: riscv64
- goos: android
goarch: amd64
- goos: android
goarch: arm
# .goreleaser.yaml
notarize:
@@ -77,12 +85,14 @@ notarize:
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
builds:
ids:
- fzf
format: tar.gz
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
files:
- non-existent*
@@ -94,7 +104,7 @@ release:
name_template: '{{ .Version }}'
snapshot:
name_template: "{{ .Version }}-devel"
version_template: "{{ .Version }}-devel"
changelog:
sort: asc

View File

@@ -1,2 +1,3 @@
golang 1.20.13
ruby 3.4.1
golang 1.23
ruby 3.4
shfmt 3.12

View File

@@ -363,7 +363,7 @@ projects, and it will free up memory as you narrow down the results.
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 "start:reload:$RG_PREFIX {q} || true" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites
- Go 1.20 or above
- Go 1.23 or above
### Using Makefile
@@ -41,6 +41,20 @@ make release
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
> ```
Running tests
-------------
```sh
# Run go unit tests
make test
# Run integration tests (requires to be on tmux)
make itest
# Run a single test case
ruby test/runner.rb --name test_something
```
Third-party libraries used
--------------------------

View File

@@ -1,6 +1,589 @@
CHANGELOG
=========
0.68.0
------
- Implemented word wrapping in the list section
- Added `--wrap=word` (or `--wrap-word`) option and `toggle-wrap-word` action for word-level line wrapping in the list section
- Changed default binding of `ctrl-/` and `alt-/` from `toggle-wrap` to `toggle-wrap-word`
```sh
fzf --wrap=word
```
- Implemented word wrapping in the preview window
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
- Added `toggle-preview-wrap-word` action
```sh
fzf --preview 'bat --style=plain --color=always {}' \
--preview-window wrap-word \
--bind space:toggle-preview-wrap-word
```
- Added support for underline style variants in `--color`: `underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
```sh
fzf --color 'fg:underline-curly,current-fg:underline-dashed'
```
- Added support for underline styles (`4:N`) and underline colors (SGR 58/59)
```sh
# In the list section
printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n' | fzf --ansi
# In the preview window
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'"
```
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview window
- Added `alt-gutter` color option (#4602) (@hedgieinsocks)
- Added `$FZF_WRAP` environment variable to child processes (`char` or `word` when wrapping is enabled) (#4672) (@bitraid)
- fish: Improved command history (CTRL-R) (#4672) (@bitraid)
- Enabled syntax highlighting in the list on fish 4.3.3+
- Added syntax-highlighted preview window that auto-shows for long or multi-line commands
- Added `ALT-ENTER` to reformat and insert selected commands
- Improved handling of bulk deletion of selected history entries (`SHIFT-DELETE`)
- Added fish completion support (#4605) (@lalvarezt)
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
- Bug fixes
- Fixed `_fzf_compgen_{path,dir}` to respect `FZF_COMPLETION_{PATH,DIR}_OPTS` (#4592) (@shtse8, @LangLangBart)
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
- Fixed preview process not killed on exit (#4667)
- Fixed coloring of items with zero-width characters (#4620)
- Fixed `track-current` unset after a combined movement action (#4649)
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
- Fixed clearing of the rest of the current line on start (#4652)
- Fixed `x-api-key` header not required for GET requests (#4627)
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
- Do not allow very long queries in `FuzzyMatchV2` (#4608)
0.67.0
------
- Added `--freeze-left=N` option to keep the leftmost N columns always visible.
```sh
# Keep the file name column fixed and always visible
git grep --line-number --color=always -- '' |
fzf --ansi --delimiter : --freeze-left 1
# Can be used with --keep-right
git grep --line-number --color=always -- '' |
fzf --ansi --delimiter : --freeze-left 1 --keep-right
```
- Also added `--freeze-right=N` option to keep the rightmost N columns always visible.
```sh
# Stronger version of --keep-right that always keeps the right-end visible
fd | fzf --freeze-right 1
# Keep the base name always visible
fd | fzf --freeze-right 1 --delimiter /
# Keep both leftmost and rightmost components visible
fd | fzf --freeze-left 1 --freeze-right 1 --delimiter /
```
- Updated `--info=inline` to print the spinner (load indicator).
- Bug fixes
0.66.1
------
- Bug fixes
- Fixed a bug preventing 'ctrl-h' from being bound to an action (#4556)
- Fixed `--no-color` / `NO_COLOR` theme (#4561)
0.66.0
------
### Quick summary
This version introduces many new features centered around the new "raw" mode.
| Type | Class | Name | Description |
| :-- | :-- | :-- | :-- |
| New | Option | `--raw` | Enable raw mode by default |
| New | Option | `--gutter CHAR` | Set the gutter column character |
| New | Option | `--gutter-raw CHAR` | Set the gutter column character in raw mode |
| Enhancement | Option | `--listen SOCKET` | Added support for Unix domain sockets |
| New | Action | `toggle-raw` | Toggle raw mode |
| New | Action | `enable-raw` | Enable raw mode |
| New | Action | `disable-raw` | Disable raw mode |
| New | Action | `up-match` | Move up to the matching item |
| New | Action | `down-match` | Move down to the matching item |
| New | Action | `best` | Move to the matching item with the best score |
| New | Color | `nomatch` | Color for non-matching items in raw mode |
| New | Env Var | `FZF_RAW` | Matching status in raw mode (0, 1, or undefined) |
| New | Env Var | `FZF_DIRECTION` | `up` or `down` depending on the layout |
| New | Env Var | `FZF_SOCK` | Path to the Unix domain socket fzf is listening on |
| Enhancement | Key | `CTRL-N` | `down` -> `down-match` |
| Enhancement | Key | `CTRL-P` | `up` -> `up-match` |
| Enhancement | Shell | `CTRL-R` binding | Toggle raw mode with `ALT-R` |
| Enhancement | Shell | `CTRL-R` binding | Opt-out with an empty `FZF_CTRL_R_COMMAND` |
### 1. Introducing "raw" mode
![](https://github.com/user-attachments/assets/9640ae11-b5f7-43fb-95f1-c29307fc17c2)
This version introduces a new "raw" mode (named so because it shows the list
"unfiltered"). In raw mode, non-matching items stay in their original positions,
but appear dimmed. This allows you to see the surrounding items of a match and
better understand the context of it. You can enable raw mode by default with
`--raw`, but it's often more useful when toggled dynamically with the
`toggle-raw` action.
```sh
tree | fzf --reverse --bind alt-r:toggle-raw
```
While non-matching items are displayed in a dimmed color, they are treated just
like matching items, so you place the cursor on them and perform any action. If
you prefer to navigate only through matching items, use the `down-match` and
`up-match` actions, which are from now on bound to `CTRL-N` and `CTRL-P`
respectively, and also to `ALT-DOWN` and `ALT-UP`.
| Key | Action | With `--history` |
| :-- | :-- | :-- |
| `down` | `down` | |
| `up` | `up` | |
| `ctrl-j` | `down` | |
| `ctrl-k` | `up` | |
| `ctrl-n` | `down-match` | `next-history` |
| `ctrl-p` | `up-match` | `prev-history` |
| `alt-down` | `down-match` | |
| `alt-up` | `up-match` | |
> [!NOTE]
> `CTRL-N` and `CTRL-P` are bound to `next-history` and `prev-history` when
> `--history` option is enabled, so in that case, you'll need to manually bind
> them, or use `ALT-DOWN` and `ALT-UP` instead.
> [!TIP]
> `up-match` and `down-match` are equivalent to `up` and `down` when not in
> raw mode, so you can safely bind them to `up` and `arrow` keys if you prefer.
> ```sh
> fzf --bind up:up-match,down:down-match
> ```
#### Customizing the behavior
In raw mode, the input list is presented in its original order, unfiltered, and
your cursor will not move to the matching item automatically. Here are ways to
customize the behavior.
```sh
# When the result list is updated, move the cursor to the item with the best score
# (assuming sorting is not disabled)
fzf --raw --bind result:best
# Move to the first matching item in the original list
# - $FZF_RAW is set to 0 when raw mode is enabled and the current item is a non-match
# - $FZF_DIRECTION is set to either 'up' or 'down' depending on the layout direction
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
```
#### Customizing the look
##### Gutter
To make the mode visually distinct, the gutter column is rendered in a dashed
line using `` character. But you can customize it with the `--gutter-raw CHAR`
option.
```sh
# Use a thinner gutter instead of the default dashed line
fzf --bind alt-r:toggle-raw --gutter-raw ▎
```
##### Color and style of non-matching items
Non-matching items are displayed in a dimmed color by default, but you can
change it with the `--color nomatch:...` option.
```sh
fzf --raw --color nomatch:red
fzf --raw --color nomatch:red:dim
fzf --raw --color nomatch:red:dim:strikethrough
fzf --raw --color nomatch:red:dim:strikethrough:italic
```
For colored input, dimming alone may not be enough, and you may prefer to remove
colors entirely. For that case, a new special style attribute `strip` has been
added.
```sh
fd --color always | fzf --ansi --raw --color nomatch:dim:strip:strikethrough
```
#### Conditional actions for raw mode
You may want to perform different actions depending on whether the current item
is a match or not. For that, fzf now exports `$FZF_RAW` environment variable.
It's:
- Undefined if raw mode is disabled
- `1` if the current item is a match
- `0` otherwise
```sh
# Do not allow selecting non-matching items
fzf --raw --bind 'enter:transform:[[ ${FZF_RAW-1} = 1 ]] && echo accept || echo bell'
```
#### Leveraging raw mode in shell integration
The `CTRL-R` binding (command history) now lets you toggle raw mode with `ALT-R`.
### 2. Style changes
The screenshot on the right shows the updated gutter style:
![](https://github.com/user-attachments/assets/8ea7b5ef-c99e-4686-905b-22eb078b700a)
This version includes a few minor updates to fzf's classic visual style:
- The gutter column is now narrower, rendered with the left-half block character (``).
- Markers no longer use background colors.
- The `--color base16` theme (alias: `16`) has been updated for better compatibility with both dark and light themes.
### 3. `--listen` now supports Unix domain sockets
If an argument to `--listen` ends with `.sock`, fzf will listen on a Unix
domain socket at the specified path.
```sh
fzf --listen /tmp/fzf.sock --no-tmux
# GET
curl --unix-socket /tmp/fzf.sock http
# POST
curl --unix-socket /tmp/fzf.sock http -d up
```
Note that any existing file at the given path will be removed before creating
the socket, so avoid using an important file path.
### 4. Added options
#### `--gutter CHAR`
The gutter column can now be customized using `--gutter CHAR` and styled with
`--color gutter:...`. Examples:
```sh
# Right-aligned gutter
fzf --gutter '▐'
# Even thinner gutter
fzf --gutter '▎'
# Yellow checker pattern
fzf --gutter '▚' --color gutter:yellow
# Classic style
fzf --gutter ' ' --color gutter:reverse
```
#### `--gutter-raw CHAR`
As noted above, the `--gutter-raw CHAR` option was also added for customizing the gutter column in raw mode.
### 5. Added actions
The following actions were introduced to support working with raw mode:
| Action | Description |
| :-- | :-- |
| `toggle-raw` | Toggle raw mode |
| `enable-raw` | Enable raw mode |
| `disable-raw` | Disable raw mode |
| `up-match` | Move up to the matching item; identical to `up` if raw mode is disabled |
| `down-match` | Move down to the matching item; identical to `down` if raw mode is disabled |
| `best` | Move to the matching item with the best score; identical to `first` if raw mode is disabled |
### 6. Added environment variables
#### `$FZF_DIRECTION`
`$FZF_DIRECTION` is now exported to child processes, indicating the list direction of the current layout:
- `up` for the default layout
- `down` for `reverse` or `reverse-list`
This simplifies writing transform actions involving layout-dependent actions
like `{up,down}-match`, `{up,down}-selected`, and `toggle+{up,down}`.
```sh
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
```
#### `$FZF_SOCK`
When fzf is listening on a Unix domain socket using `--listen`, the path to the
socket is exported as `$FZF_SOCK`, analogous to `$FZF_PORT` for TCP sockets.
#### `$FZF_RAW`
As described above, `$FZF_RAW` is now exported to child processes in raw mode,
indicating whether the current item is a match (`1`) or not (`0`). It is not
defined when not in raw mode.
#### `$FZF_CTRL_R_COMMAND`
You can opt-out `CTRL-R` binding from the shell integration by setting
`FZF_CTRL_R_COMMAND` to an empty string. Setting it to any other value is not
supported and will result in a warning.
```sh
# Disable the CTRL-R binding from the shell integration
FZF_CTRL_R_COMMAND= eval "$(fzf --bash)"
```
### 7. Added key support for `--bind`
Pull request [#3996](https://github.com/junegunn/fzf/pull/3996) added support
for many additional keys for `--bind` option, such as `ctrl-backspace`.
### 8. Breaking changes
#### Hiding the gutter column
In the previous versions, the recommended way to hide the gutter column was to
set `--color gutter:-1`. That's because the gutter column was just a space
character, reversed. But now that it's using a visible character (``), applying
the default color is no longer enough to hide it. Instead, you can set it to
a space character.
```sh
# Hide the gutter column
fzf --gutter ' '
# Classic style
fzf --gutter ' ' --color gutter:reverse
```
#### `--color` option
In the previous versions, some elements had default style attributes applied and
you would have to explicitly unset them with `regular` attribute if you wanted
to reset them. This is no longer needed now, as the default style attributes
are applied only when you do not specify any color or style for that element.
```sh
# No 'dim', just red and italic.
fzf --ghost 'Type to search' --color ghost:red:italic
```
#### Compatibility changes
Starting with this release, fzf is built with Go 1.23. Support for some old OS versions has been dropped.
See https://go.dev/wiki/MinimumRequirements.
0.65.2
------
- Bug fixes and improvements
- Fix incorrect truncation of `--info-command` with `--info=inline-right` (#4479)
- [install] Support old uname in macOS (#4492)
- [bash 3] Fix `CTRL-T` and `ALT-C` to preserve the last yank (#4496)
- Do not unset `FZF_DEFAULT_*` variables when using winpty (#4497) (#4400)
- Fix rendering of items with tabs when using a non-default ellipsis (#4505)
- **This is the final release to support Windows 7.**
- Future versions will be built with the latest Go toolchain, which has dropped support for Windows 7.
0.65.1
------
- Fixed incorrect `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_FOOTER_WORD` when the header or footer contains ANSI escape sequences and tab characters.
- Fixed a bug where you cannot unset the default `--nth` using `change-nth` action.
- Fixed a highlighting bug when using `--color fg:dim,nth:regular` pattern over ANSI-colored items.
0.65.0
------
- Added `click-footer` event that is triggered when the footer section is clicked. When the event is triggered, the following environment variables are set:
- `$FZF_CLICK_FOOTER_COLUMN` - clicked column (1-based)
- `$FZF_CLICK_FOOTER_LINE` - clicked line (1-based)
- `$FZF_CLICK_FOOTER_WORD` - the word under the cursor
```sh
fzf --footer $'[Edit] [View]\n[Copy to clipboard]' \
--with-shell 'bash -c' \
--bind 'click-footer:transform:
[[ $FZF_CLICK_FOOTER_WORD =~ Edit ]] && echo "execute:vim \{}"
[[ $FZF_CLICK_FOOTER_WORD =~ View ]] && echo "execute:view \{}"
(( FZF_CLICK_FOOTER_LINE == 2 )) && (( FZF_CLICK_FOOTER_COLUMN < 20 )) &&
echo "execute-silent(echo -n \{} | pbcopy)+bell"
'
```
- Added `trigger(...)` action that triggers events bound to another key or event.
```sh
# You can click on each key name to trigger the actions bound to that key
fzf --footer 'Ctrl-E: Edit / Ctrl-V: View / Ctrl-Y: Copy to clipboard' \
--with-shell 'bash -c' \
--bind 'ctrl-e:execute:vim {}' \
--bind 'ctrl-v:execute:view {}' \
--bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell' \
--bind 'click-footer:transform:
[[ $FZF_CLICK_FOOTER_WORD =~ Ctrl ]] && echo "trigger(${FZF_CLICK_FOOTER_WORD%:})"
'
```
- You can specify a series of keys and events
```sh
fzf --bind 'a:up,b:trigger(a,a,a)'
```
- Added support for `{*n}` and `{*nf}` placeholder.
- `{*n}` evaluates to the zero-based ordinal index of all matched items.
- `{*nf}` evaluates to the temporary file containing that.
- Bug fixes and improvements
- [neovim] Fixed margin background color when `&winborder` is used (#4453)
- Fixed rendering error when hiding a preview window without border (#4465)
- fix(shell): check for mawk existence before version check (#4468)
- Thanks to @LangLangBart and @akinomyoga
- Fixed `--no-header-lines-border` behavior (08027e7a)
0.64.0
------
- Added `multi` event that is triggered when the multi-selection has changed.
```sh
fzf --multi \
--bind 'ctrl-a:select-all,ctrl-d:deselect-all' \
--bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'
```
- [Halfwidth and fullwidth alphanumeric and punctuation characters](https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)) are now internally normalized to their ASCII equivalents to allow matching with ASCII queries.
```sh
echo | fzf -q abc
```
- Renamed `clear-selection` action to `clear-multi` for consistency.
- `clear-selection` remains supported as an alias for backward compatibility.
- Bug fixes
- Fixed a bug that could cause fzf to abort due to incorrect update ordering.
- Fixed a bug where some multi-selections were lost when using `exclude` or `change-nth`.
0.63.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_
- Added footer. The default border style for footer is `line`, which draws a single separator line.
```sh
fzf --reverse --footer "fzf: friend zone forever"
```
- Options
- `--footer[=STRING]`
- `--footer-border[=STYLE]`
- `--footer-label=LABEL`
- `--footer-label-pos=COL[:bottom]`
- Colors
- `footer`
- `footer-bg`
- `footer-border`
- `footer-label`
- Actions
- `change-footer`
- `transform-footer`
- `bg-transform-footer`
- `change-footer-label`
- `transform-footer-label`
- `bg-transform-footer-label`
- `line` border style is now allowed for all types of border except for `--list-border`.
```sh
fzf --height 50% --style full:line --preview 'cat {}' \
--bind 'focus:bg-transform-header(file {})+bg-transform-footer(wc {})'
```
- Added `{*}` placeholder flag that evaluates to all matched items.
```bash
seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}"
```
- Use this with caution, as it can make fzf sluggish for large lists.
- Added asynchronous transform actions with `bg-` prefix that run asynchronously in the background, along with `bg-cancel` action to cancel currently running `bg-transform` actions.
```sh
# Implement popup that disappears after 1 second
# * Use footer as the popup
# * Use `bell` to ring the terminal bell
# * Use `bg-transform-footer` to clear the footer after 1 second
# * Use `bg-cancel` to cancel currently running background transform actions
fzf --multi --list-border \
--bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
--bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
--bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
# It's okay for the commands to take a little while because they run in the background
GETTER='curl -s http://metaphorpsum.com/sentences/1'
fzf --style full --border --preview : \
--bind "focus:bg-transform-header:$GETTER" \
--bind "focus:+bg-transform-footer:$GETTER" \
--bind "focus:+bg-transform-border-label:$GETTER" \
--bind "focus:+bg-transform-preview-label:$GETTER" \
--bind "focus:+bg-transform-input-label:$GETTER" \
--bind "focus:+bg-transform-list-label:$GETTER" \
--bind "focus:+bg-transform-header-label:$GETTER" \
--bind "focus:+bg-transform-footer-label:$GETTER" \
--bind "focus:+bg-transform-ghost:$GETTER" \
--bind "focus:+bg-transform-prompt:$GETTER"
```
- Added support for full-line background color in the list section
```sh
for i in $(seq 16 255); do
echo -e "\x1b[48;5;${i}m\x1b[0Khello"
done | fzf --ansi
```
- SSH completion enhancements by @akinomyoga
- Bug fixes and improvements
0.62.0
------
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
```sh
# seoul256-light
fzf --style full --color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'
```
- Added `alt-bg` color to create striped lines to visually separate rows
```sh
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi \
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
```
- [fish] Improvements in CTRL-R binding (@bitraid)
- You can trigger CTRL-R in the middle of a command to insert the selected item
- You can delete history items with SHIFT-DEL
- Bug fixes and improvements
- Fixed unnecessary 100ms delay after `reload` (#4364)
- Fixed `selected-bg` not applied to colored items (#4372)
0.61.3
------
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
- More environment variables for child processes (#4356)
0.61.2
------
- Fixed panic when using header border without pointer/marker (@phanen)
- Fixed `--tmux` option when already inside a tmux popup (@peikk0)
- Bug fixes and improvements in CTRL-T binding of fish (#4334) (@bitraid)
- Added `--no-tty-default` option to make fzf search for the current TTY device instead of defaulting to `/dev/tty` (#4242)
0.61.1
------
- Disable bracketed-paste mode on exit. This fixes issue where pasting breaks after running fzf on old bash versions that don't support the mode.
0.61.0
------
- Added `--ghost=TEXT` to display a ghost text when the input is empty
```sh
# Display "Type to search" when the input is empty
fzf --ghost "Type to search"
```
- Added `change-ghost` and `transform-ghost` actions for dynamically changing the ghost text
- Added `change-pointer` and `transform-pointer` actions for dynamically changing the pointer sign
- Added `r` flag for placeholder expression (raw mode) for unquoted output
- Bug fixes and improvements
0.60.3
------
- Bug fixes and improvements

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 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

View File

@@ -1,10 +1,20 @@
GO ?= go
DOCKER ?= docker
GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
$(ROOT_DIR)/bin/fzf-tmux \
$(ROOT_DIR)/install \
$(ROOT_DIR)/uninstall \
$(ROOT_DIR)/shell/common.sh \
$(ROOT_DIR)/shell/update.sh \
$(ROOT_DIR)/shell/completion.bash \
$(ROOT_DIR)/shell/key-bindings.bash
ifdef FZF_VERSION
VERSION := $(FZF_VERSION)
else
@@ -88,9 +98,14 @@ itest:
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
lint: $(SOURCES) test/*.rb test/lib/*.rb
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
shell/update.sh --check ${BASH_SCRIPTS}
fmt: $(SOURCES) $(BASH_SCRIPTS)
gofmt -s -w src
shell/update.sh ${BASH_SCRIPTS}
install: bin/fzf
@@ -178,15 +193,15 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
docker:
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu tmux
$(DOCKER) build -t fzf-ubuntu .
$(DOCKER) run -it fzf-ubuntu tmux
docker-test:
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu
$(DOCKER) build -t fzf-ubuntu .
$(DOCKER) run -it fzf-ubuntu
update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all generate build release test itest bench lint install clean docker docker-test update
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt

View File

@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'query': ['fg', 'Normal'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
@@ -492,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 Junegunn Choi

185
README.md

File diff suppressed because one or more lines are too long

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# Security Reporting
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
## Reporting
To report a security vulnerability, please provide the following information:
1. **PROJECT**
- https://github.com/junegunn/fzf
2. **PUBLIC**
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
- If so, provide relevant links.
3. **DESCRIPTION**
- Provide a detailed description of the security vulnerability.
- Include as much information as possible to help us understand and address the issue.
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
## Confidentiality
We kindly ask you to keep the report confidential until a public announcement is made.
## Notes
- Vulnerabilities will be handled on a best-effort basis.
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
- You will be notified via email simultaneously with the public announcement.
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
Thank you for helping to improve the security of our project!

View File

@@ -49,9 +49,9 @@ if [[ ! $type =~ image/ ]]; then
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
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
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))

View File

@@ -8,7 +8,7 @@ fail() {
}
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
[[ -x $fzf ]] || fail 'fzf executable not found'
args=()
opt=""
@@ -16,8 +16,8 @@ skip=""
swap=""
close=""
term=""
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
[[ -n $LINES ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
[[ -n $COLUMNS ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
@@ -47,7 +47,7 @@ help() {
while [[ $# -gt 0 ]]; do
arg="$1"
shift
[[ -z "$skip" ]] && case "$arg" in
[[ -z $skip ]] && case "$arg" in
-)
term=1
;;
@@ -58,19 +58,19 @@ while [[ $# -gt 0 ]]; do
echo "fzf-tmux (with fzf $("$fzf" --version))"
exit
;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-E" ]] || opt="-E"
elif [[ "$arg" =~ ^.[lr] ]]; then
-p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
if [[ $arg =~ ^-[pwhxy] ]]; then
[[ $opt =~ "-E" ]] || opt="-E"
elif [[ $arg =~ ^.[lr] ]]; then
opt="-h"
if [[ "$arg" =~ ^.l ]]; then
if [[ $arg =~ ^.l ]]; then
opt="$opt -d"
swap="; swap-pane -D ; select-pane -L"
close="; tmux swap-pane -D"
fi
else
opt=""
if [[ "$arg" =~ ^.u ]]; then
if [[ $arg =~ ^.u ]]; then
opt="$opt -d"
swap="; swap-pane -D ; select-pane -U"
close="; tmux swap-pane -D"
@@ -79,7 +79,7 @@ while [[ $# -gt 0 ]]; do
if [[ ${#arg} -gt 2 ]]; then
size="${arg:2}"
else
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
size="$1"
shift
else
@@ -87,37 +87,37 @@ while [[ $# -gt 0 ]]; do
fi
fi
if [[ "$arg" =~ ^-p ]]; then
if [[ -n "$size" ]]; then
if [[ $arg =~ ^-p ]]; then
if [[ -n $size ]]; then
w=${size%%,*}
h=${size##*,}
opt="$opt -w$w -h$h"
fi
elif [[ "$arg" =~ ^-[whxy] ]]; then
elif [[ $arg =~ ^-[whxy] ]]; then
opt="$opt ${arg:0:2}$size"
elif [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))}
if [[ $tmux_32 = 1 ]]; then
if [[ -n "$swap" ]]; then
opt="$opt -l $(( 100 - size ))%"
elif [[ $size =~ %$ ]]; then
size=${size:0:${#size}-1}
if [[ $tmux_32 == 1 ]]; then
if [[ -n $swap ]]; then
opt="$opt -l $((100 - size))%"
else
opt="$opt -l $size%"
fi
else
if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))"
if [[ -n $swap ]]; then
opt="$opt -p $((100 - size))"
else
opt="$opt -p $size"
fi
fi
else
if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then
if [[ -n $swap ]]; then
if [[ $arg =~ ^.l ]]; then
max=$columns
else
max=$lines
fi
size=$(( max - size ))
size=$((max - size))
[[ $size -lt 0 ]] && size=0
opt="$opt -l $size"
else
@@ -135,10 +135,10 @@ while [[ $# -gt 0 ]]; do
args+=("$arg")
;;
esac
[[ -n "$skip" ]] && args+=("$arg")
[[ -n $skip ]] && args+=("$arg")
done
if [[ -z "$TMUX" ]]; then
if [[ -z $TMUX ]]; then
"$fzf" "${args[@]}"
exit $?
fi
@@ -149,7 +149,7 @@ fi
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
# Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-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
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'")
@@ -159,28 +159,27 @@ fi
set -e
# Clean up named pipes on exit
id=$RANDOM
argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/fzf-tmux-XXXXXX")
argsf="$tmpdir/args"
fifo1="$tmpdir/fifo1"
fifo2="$tmpdir/fifo2"
fifo3="$tmpdir/fifo3"
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_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_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() {
\rm -f $argsf $fifo1 $fifo2 $fifo3
\rm -rf "$tmpdir"
# Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
eval "tmux ${tmux_win_opts[*]}"
fi
# Remove temp window if we were zoomed without popup options
if [[ -n "$zoomed_without_popup" ]]; then
if [[ -n $zoomed_without_popup ]]; then
tmux display-message -p "#{window_id}" > /dev/null
tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \
@@ -196,11 +195,11 @@ cleanup() {
trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT
envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then
if [[ $tmux_version = 3.2 ]]; then
envs="export TERM=$(printf %q "$TERM") "
if [[ $opt =~ "-E" ]]; then
if [[ $tmux_version == 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
elif [[ $tmux_32 = 1 ]]; then
elif [[ $tmux_32 == 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
else
@@ -211,8 +210,8 @@ 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")"
[[ -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"
# Build arguments to fzf
@@ -224,9 +223,9 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-E" ]]; then
if [[ $opt =~ "-E" ]]; then
cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then
if [[ -n $term ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
else
mkfifo $fifo1
@@ -239,7 +238,7 @@ if [[ "$opt" =~ "-E" ]]; then
fi
mkfifo -m o+w $fifo3
if [[ -n "$term" ]] || [[ -t 0 ]]; then
if [[ -n $term ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
else
mkfifo $fifo1
@@ -249,6 +248,9 @@ fi
tmux \
split-window -c "$PWD" $opt "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
exit "$(cat $fifo3)"

View File

@@ -503,7 +503,7 @@ LICENSE *fzf-license*
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 Junegunn Choi
==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

12
go.mod
View File

@@ -1,20 +1,20 @@
module github.com/junegunn/fzf
require (
github.com/charlievieth/fastwalk v1.0.9
github.com/gdamore/tcell/v2 v2.8.1
github.com/charlievieth/fastwalk v1.0.14
github.com/gdamore/tcell/v2 v2.9.0
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.30.0
golang.org/x/term v0.29.0
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
)
require (
github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
go 1.20
go 1.23.0

52
go.sum
View File

@@ -1,10 +1,9 @@
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -14,35 +13,20 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -50,38 +34,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

192
install
View File

@@ -2,7 +2,7 @@
set -u
version=0.60.3
version=0.68.0
auto_completion=
key_bindings=
update_config=2
@@ -46,16 +46,16 @@ for opt in "$@"; do
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
;;
--key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;;
--completion) auto_completion=1 ;;
--no-completion) auto_completion=0 ;;
--update-rc) update_config=1 ;;
--no-update-rc) update_config=0 ;;
--bin) ;;
--no-bash) shells=${shells/bash/} ;;
--no-zsh) shells=${shells/zsh/} ;;
--no-fish) shells=${shells/fish/} ;;
--key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;;
--completion) auto_completion=1 ;;
--no-completion) auto_completion=0 ;;
--update-rc) update_config=1 ;;
--no-update-rc) update_config=0 ;;
--bin) ;;
--no-bash) shells=${shells/bash/} ;;
--no-zsh) shells=${shells/zsh/} ;;
--no-fish) shells=${shells/fish/} ;;
*)
echo "unknown option: $opt"
help
@@ -104,7 +104,7 @@ check_binary() {
link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH"
echo ' - Found in $PATH'
echo " - Creating symlink: bin/fzf -> $which_fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
@@ -114,22 +114,22 @@ link_fzf_in_path() {
try_curl() {
command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
}
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
}
download() {
@@ -164,28 +164,30 @@ download() {
}
# Try to download binary executable
archi=$(uname -sm)
archi=$(uname -smo 2> /dev/null || uname -sm)
binary_available=1
binary_error=""
case "$archi" in
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
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 ;;
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ riscv64*) download fzf-$version-linux_riscv64.tar.gz ;;
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
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
cd "$fzf_base"
@@ -214,7 +216,7 @@ if [ -n "$binary_error" ]; then
fi
fi
[[ "$*" =~ "--bin" ]] && exit 0
[[ $* =~ "--bin" ]] && exit 0
for s in $shells; do
if ! command -v "$s" > /dev/null; then
@@ -241,16 +243,16 @@ fi
echo
for shell in $shells; do
[[ "$shell" = fish ]] && continue
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
[[ $shell == fish ]] && continue
src=${prefix_expand}.${shell}
echo -n "Generate $src ... "
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion"
fi
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
if [ $key_bindings -eq 0 ]; then
fzf_key_bindings="# $fzf_key_bindings"
fi
@@ -265,7 +267,7 @@ fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
if [[ "$shell" = zsh ]]; then
if [[ $shell == zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
@@ -285,7 +287,7 @@ EOF
done
# fish
if [[ "$shells" =~ fish ]]; then
if [[ $shells =~ fish ]]; then
echo -n "Update fish_user_paths ... "
fish << EOF
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
@@ -300,15 +302,16 @@ append_line() {
line="$2"
file="$3"
pat="${4:-}"
at_lno="${5:-}"
lines=""
echo "Update $file:"
echo " - $line"
if [ -f "$file" ]; then
if [ $# -lt 4 ]; then
lines=$(\grep -nF "$line" "$file")
else
if [[ -n $pat ]]; then
lines=$(\grep -nF "$pat" "$file")
else
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
fi
fi
@@ -317,17 +320,21 @@ append_line() {
sed 's/^/ Line /' <<< "$lines"
update=0
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
echo " - But they all seem to be commented"
ask " - Continue modifying $file?"
ask " - Continue modifying $file?"
update=$?
fi
fi
set -e
if [ "$update" -eq 1 ]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
if [[ -z $at_lno ]]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
else
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
fi
echo " + Added"
else
echo " ~ Skipped"
@@ -355,43 +362,84 @@ if [ $update_config -eq 2 ]; then
fi
echo
for shell in $shells; do
[[ "$shell" = fish ]] && continue
[[ $shell == fish ]] && continue
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
done
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
if [[ $shells =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then
mkdir -p "${fish_dir}/functions"
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf --fish | source' \
'end'
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
mkdir -p "${fish_dir}/functions"
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf --fish | source' \
'end'
elif [[ $key_bindings -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
" $fzf_key_bindings" \
'end'
elif [[ $auto_completion -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
" $fzf_completion" \
'end'
fi
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
else
lno_func=0
fi
else
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'"
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -z $lno_func ]]; then
echo -e "function fish_user_key_bindings\nend" >> "$bind_file"
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
fi
lno_keys=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $lno_keys ]]; then
echo " ** Found 'fzf_key_bindings' in line #$lno_keys"
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
echo " ** You have to replace the line to 'fzf --fish | source'"
elif [[ $key_bindings -eq 1 ]]; then
echo " ** You have to replace the line to '$fzf_key_bindings'"
else
echo " ** You have to remove the line"
fi
echo
else
echo " - Clear"
echo
append_line $update_config "fzf --fish | source" "$bind_file"
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " fzf --fish | source" "$bind_file" "" "$lno_func"
else
sed -i.~fzf_bak '/fzf --fish \| source/d' "$bind_file" && rm "$bind_file.~fzf_bak"
if [[ $key_bindings -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " $fzf_key_bindings" "$bind_file" "" "$lno_func"
elif [[ $auto_completion -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " $fzf_completion" "$bind_file" "" "$lno_func"
fi
fi
fi
fi
fi
if [ $update_config -eq 1 ]; then
echo 'Finished. Restart your shell or reload config file.'
if [[ "$shells" =~ bash ]]; then
if [[ $shells =~ bash ]]; then
echo -n ' source ~/.bashrc # bash'
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
[[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
echo
fi
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
echo
echo 'Use uninstall script to remove fzf.'
echo

View File

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

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.60"
var version = "0.68"
var revision = "devel"
//go:embed shell/key-bindings.bash
@@ -29,6 +29,9 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish
var fishKeyBindings []byte
//go:embed shell/completion.fish
var fishCompletion []byte
//go:embed man/man1/fzf.1
var manPage []byte
@@ -65,7 +68,7 @@ func main() {
}
if options.Fish {
printScript("key-bindings.fish", fishKeyBindings)
fmt.Println("fzf_key_bindings")
printScript("completion.fish", fishCompletion)
return
}
if options.Help {

View File

@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 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
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf\-tmux 1 "Mar 2025" "fzf 0.60.3" "fzf\-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "Feb 2026" "fzf 0.68.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf\-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 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
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Mar 2025" "fzf 0.60.3" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Feb 2026" "fzf 0.68.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -228,6 +228,13 @@ e.g. \fB# Avoid rendering both fzf instances at the same time
(sleep 1; seq 1000000; sleep 1) |
fzf \-\-sync \-\-query 5 \-\-listen \-\-bind start:up,load:up,result:up,focus:change\-header:Ready\fR
.RE
.TP
.B "\-\-no\-tty\-default"
Make fzf search for the current TTY device via standard error instead of
defaulting to \fB/dev/tty\fR. This option avoids issues when launching
emacsclient from within fzf. Alternatively, you can change the default TTY
device by setting \fB--tty-default=DEVICE_NAME\fR.
.SS GLOBAL STYLE
.TP
.BI "\-\-style=" "PRESET"
@@ -235,15 +242,15 @@ Apply a style preset [default|minimal|full[:BORDER_STYLE]]
.TP
.BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
Color configuration. The name of the base color scheme is followed by custom
color mappings.
color mappings. Each entry is separated by a comma and/or whitespaces.
.RS
.B BASE SCHEME:
(default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
(default: \fBdark\fR on 256-color terminal, otherwise \fBbase16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
\fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBdark \fRColor scheme for dark terminal
\fBlight \fRColor scheme for light terminal
\fBbase16 \fRColor scheme using base 16 colors (alias: \fB16\fR)
\fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR)
.B COLOR NAMES:
@@ -255,15 +262,19 @@ color mappings.
\fBlist\-bg \fRList section background
\fBselected\-bg \fRSelected line background
\fBpreview\-bg \fRPreview window background
\fBinput\-bg \fRInput window background (\fB\-\-input\-border\fR)
\fBheader\-bg \fRHeader window background (\fB\-\-header\-border\fR)
\fBinput\-bg \fRInput window background
\fBheader\-bg \fRHeader window background
\fBfooter\-bg \fRFooter window background
\fBhl \fRHighlighted substrings
\fBselected\-hl \fRHighlighted substrings in the selected line
\fBcurrent\-fg (fg+) \fRText (current line)
\fBcurrent\-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
\fBalt\-bg \fRAlternate background color to create striped lines
\fBalt\-gutter \fRAlternate gutter color to create the striped pattern
\fBquery (input\-fg) \fRQuery string
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
\fBinfo \fRInfo line (match counters)
\fBborder \fRBorder around the window (\fB\-\-border\fR and \fB\-\-preview\fR)
@@ -275,17 +286,21 @@ color mappings.
\fBpreview\-scrollbar \fRScrollbar
\fBinput\-border \fRBorder around the input window (\fB\-\-input\-border\fR)
\fBheader\-border \fRBorder around the header window (\fB\-\-header\-border\fR)
\fBfooter\-border \fRBorder around the footer window (\fB\-\-footer\-border\fR)
\fBlabel \fRBorder label (\fB\-\-border\-label\fR, \fB\-\-list\-label\fR, \fB\-\-input\-label\fR, and \fB\-\-preview\-label\fR)
\fBlist\-label \fRBorder label of the list section (\fB\-\-list\-label\fR)
\fBpreview\-label \fRBorder label of the preview window (\fB\-\-preview\-label\fR)
\fBinput\-label \fRBorder label of the input window (\fB\-\-input\-label\fR)
\fBheader\-label \fRBorder label of the header window (\fB\-\-header\-label\fR)
\fBfooter\-label \fRBorder label of the footer window (\fB\-\-footer\-label\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti\-select marker
\fBspinner \fRStreaming input indicator
\fBheader (header\-fg) \fRHeader
\fBfooter (footer\-fg) \fRFooter
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
\fBnomatch \fRNon-matching items in raw mode (default: \fBdim\fR)
.B ANSI COLORS:
\fB\-1 \fRDefault terminal foreground/background color
@@ -311,9 +326,14 @@ color mappings.
\fB#rrggbb \fR24-bit colors
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
\fBregular \fRClears previously set attributes; should precede the other ones
\fBregular \fRClear previously set attributes; should precede the other ones
\fBstrip \fRRemove colors
\fBbold\fR
\fBunderline\fR
\fBunderline-double\fR
\fBunderline-curly\fR
\fBunderline-dotted\fR
\fBunderline-dashed\fR
\fBreverse\fR
\fBdim\fR
\fBitalic\fR
@@ -330,7 +350,19 @@ color mappings.
# Seoul256 theme with 24-bit colors
fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
\-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'
# Seoul256 light theme with 24-bit colors, each entry separated by whitespaces
fzf \-\-style full \-\-color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'\fR
.RE
.TP
.B "\-\-no\-color"
@@ -482,6 +514,8 @@ Draw border around the finder
.br
.BR vertical " Vertical lines on each side of the finder"
.br
.BR line " Single line border (position automatically determined)"
.br
.BR top " (up)"
.br
.BR bottom " (down)"
@@ -497,6 +531,9 @@ If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly
rendered, set \fB\-\-no\-unicode\fR.
\fBline\fR style draws a single separator line at the top when \fB\-\-height\fR
is used.
.TP
.BI "\-\-border\-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the
@@ -556,8 +593,11 @@ Highlight the whole current line
.B "\-\-cycle"
Enable cyclic scroll
.TP
.B "\-\-wrap"
Enable line wrap
.BI "\-\-wrap" "[=MODE]"
Enable line wrap. \fIMODE\fR can be \fBchar\fR (default) or \fBword\fR.
\fBword\fR mode wraps lines at word boundaries (spaces and tabs) instead of
at arbitrary character positions. \fB\-\-wrap\-word\fR is a synonym for
\fB\-\-wrap=word\fR.
.TP
.BI "\-\-wrap\-sign" "=INDICATOR"
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
@@ -566,6 +606,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
.B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR
.TP
.B "\-\-raw"
Enable raw mode where non-matching items are also displayed in a dimmed color.
.TP
.B "\-\-track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is
@@ -594,9 +637,16 @@ Render empty lines between each item
The given string will be repeated to draw a horizontal line on each gap
(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
.TP
.BI "\-\-freeze\-left=" "N"
Number of fields to freeze on the left.
.TP
.BI "\-\-freeze\-right=" "N"
Number of fields to freeze on the right.
.TP
.B "\-\-keep\-right"
Keep the right end of the line visible when it's too long. Effective only when
the query string is empty.
the query string is empty. Use \fB\-\-freeze\-right=1\fR instead if you want
the last field to be always visible even with a non-empty query.
.TP
.BI "\-\-scroll\-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to
@@ -613,6 +663,12 @@ on the center of the screen.
.BI "\-\-jump\-labels=" "CHARS"
Label characters for \fBjump\fR mode.
.TP
.BI "\-\-gutter=" "CHAR"
Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given)
.TP
.BI "\-\-gutter\-raw=" "CHAR"
Character used for the gutter column in raw mode (default: '▖' unless \fB\-\-no\-unicode\fR is given)
.TP
.BI "\-\-pointer=" "STR"
Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR)
.TP
@@ -640,7 +696,8 @@ Do not display scrollbar. A synonym for \fB\-\-scrollbar=''\fB
.TP
.BI "\-\-list\-border" [=STYLE]
Draw border around the list section
Draw border around the list section. \fBline\fR style is not supported for
this border.
.TP
.BI "\-\-list\-label" [=LABEL]
@@ -691,7 +748,7 @@ ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES.
e.g.
\fB# Prepend the current cursor position in yellow
fzf \-\-info\-command='echo \-e "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
fzf \-\-info\-command='printf "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
.TP
.B "\-\-no\-info"
@@ -709,6 +766,10 @@ ANSI color codes are supported.
Do not display horizontal separator on the info line. A synonym for
\fB\-\-separator=''\fB
.TP
.BI "\-\-ghost=" "TEXT"
Ghost text to display when the input is empty
.TP
.B "\-\-filepath\-word"
Make word-wise movements and actions respect path separators. The following
@@ -723,7 +784,8 @@ actions are affected:
\fBkill\-word\fR
.TP
.BI "\-\-input\-border" [=STYLE]
Draw border around the input section
Draw border around the input section. \fBline\fR style draws a single separator
line between the input section and the list section.
.TP
.BI "\-\-input\-label" [=LABEL]
@@ -757,26 +819,37 @@ fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
the preview command can determine the position of the preview window.
A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection
space-separated list of the selected items (or the current item if no selection
was made) individually quoted.
e.g.
\fBfzf \-\-multi \-\-preview='head \-10 {+}'
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
Similarly, a placeholder expression starting with \fB*\fR flag will be replaced
to the space-separated list of all matched items individually quoted.
Each expression expands to a quoted string, so that it's safe to pass it as an
argument to an external command. So you should not manually add quotes around
the curly braces. But if you don't want this behavior, you can put
\fBr\fR flag (raw) in the expression (e.g. \fB{r}\fR, \fB{r1}\fR, etc).
Use it with caution as unquoted output can lead to broken commands.
When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may
pass a large number of items and the length of the evaluated string may
exceed \fBARG_MAX\fR.
e.g.
\fB# Press CTRL\-A to select 100K items and see the sum of all the numbers.
\fB# See the sum of all the matched numbers
# This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf \-\-multi \-\-bind ctrl\-a:select\-all \\
\-\-preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
\fB# Use {+f} to get the selected items as a line-separated list
seq 100 | fzf \-\-multi \-\-bind 'enter:become:cat {+f}'\fR
Also,
@@ -817,8 +890,7 @@ e.g.
.TP
.BI "\-\-preview\-border" [=STYLE]
Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other
styles, \fBline\fR style is also supported for preview border, which draws
Short for \fB\-\-preview\-window=border\-STYLE\fR. \fBline\fR style draws
a single separator line between the preview window and the rest of the
interface.
@@ -847,6 +919,11 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.B * border\-bottom
.br
.TP
.BI "\-\-preview\-wrap\-sign" =INDICATOR
Indicator for wrapped lines in the preview window. If not set, the value of
\fB\-\-wrap\-sign\fR is used.
.TP
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
Position of the border label on the border line of the preview window. Specify
@@ -857,7 +934,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
border line.
.TP
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,wrap\-word][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.RS
.B POSITION: (default: right)
@@ -875,7 +952,8 @@ default until \fBtoggle\-preview\fR action is triggered.
execute the command in the background.
* Long lines are truncated by default. Line wrap can be enabled with
\fBwrap\fR flag.
\fBwrap\fR flag. \fBwrap\-word\fR flag enables word-level wrapping, which
breaks lines at word boundaries instead of mid-word.
* Preview window will automatically scroll to the bottom when \fBfollow\fR
flag is set, similarly to how \fBtail \-f\fR works.
@@ -970,10 +1048,12 @@ The first N lines of the input are treated as the sticky header. When
lines that follow.
.TP
.B "\-\-header\-first"
Print header before the prompt line
Print header before the prompt line. When both normal header and header lines
(\fB\-\-header\-lines\fR) are present, this applies only to the normal header.
.TP
.BI "\-\-header\-border" [=STYLE]
Draw border around the header section
Draw border around the header section. \fBline\fR style draws a single
separator line between the header window and the list section.
.TP
.BI "\-\-header\-label" [=LABEL]
@@ -987,7 +1067,30 @@ Position of the header label
.BI "\-\-header\-lines\-border" [=STYLE]
Display header from \fB--header\-lines\fR with a separate border. Pass
\fBnone\fR to still separate the header lines but without a border. To combine
two headers, use \fB\-\-no\-header\-lines\-border\fR.
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
a single separator line between the header lines and the list section.
.SS FOOTER
.TP
.BI "\-\-footer=" "STR"
The given string will be printed as the sticky footer. The lines are displayed
in the given order from top to bottom regardless of \fB\-\-layout\fR option, and
are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even when
\fB\-\-ansi\fR is not set.
.TP
.BI "\-\-footer\-border" [=STYLE]
Draw border around the footer section. \fBline\fR style draws a single
separator line between the footer and the list section.
.TP
.BI "\-\-footer\-label" [=LABEL]
Label to print on the footer border
.TP
.BI "\-\-footer\-label\-pos" [=N[:top|bottom]]
Position of the footer label
.SS SCRIPTING
.TP
@@ -1057,19 +1160,25 @@ On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
set.
.TP
.B "\-\-listen[=[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method.
.B "\-\-listen[=SOCKET_PATH|[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address or Unix socket. It allows
external processes to send actions to perform via POST method and query the
program state via GET method. For the argument to be recognized as a socket
path, it must have \fB.sock\fR extension.
- If the port number is omitted or given as 0, fzf will automatically choose
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
a port and export it as \fBFZF_PORT\fR environment variable to the child processes.
- If a Unix socket path is given, fzf will create a Unix domain socket at the
given path. The existing file will be removed. The path to the socket file
is exported as \fBFZF_SOCK\fR environment variable.
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header.
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
- \fBFZF_API_KEY\fR is required for a non-localhost listen address.
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR.
e.g.
\fB# Start HTTP server on port 6266
@@ -1078,11 +1187,6 @@ e.g.
# Send action to the server
curl \-XPOST localhost:6266 \-d 'reload(seq 100)+change\-prompt(hundred> )'
# Get program state in JSON format (experimental)
# * Make sure NOT to access this endpoint from execute/transform actions
# as it will result in a timeout
curl localhost:6266
# Start HTTP server on port 6266 with remote connections allowed
# * Listening on non-localhost address requires using an API key
export FZF_API_KEY="$(head \-c 32 /dev/urandom | base64)"
@@ -1093,8 +1197,38 @@ e.g.
# Choose port automatically and export it as $FZF_PORT to the child process
fzf \-\-listen \-\-bind 'start:execute\-silent:echo $FZF_PORT > /tmp/fzf\-port'
# Get program state in JSON format (experimental)
# - GET Parameters:
# - limit: number of items to return (default: 100)
# - offset: number of items to skip (default: 0)
curl localhost:6266
# Automatically select items with .txt extension
fzf \-\-multi \-\-sync \-\-listen \-\-bind 'load:transform:
pos=1
curl \-s localhost:$FZF_PORT?limit=1000 | jq \-r .matches[].text | while read \-r text; do
if [[ $text =~ \\.txt$ ]]; then
echo \-n "+pos($pos)+select"
fi
pos=$((pos + 1))
done
echo +first
'
\fR
Here is an example script that uses a Unix socket instead of a TCP port.
\fB
fzf --listen=/tmp/fzf.sock
# GET
curl --unix-socket /tmp/fzf.sock http
# POST
curl --unix-socket /tmp/fzf.sock http -d up
\fR
.SS DIRECTORY TRAVERSAL
.TP
.B "\-\-walker=[file][,dir][,follow][,hidden]"
@@ -1246,6 +1380,8 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
.br
.BR FZF_DIRECTION " Direction of the list (\fBup\fR or \fBdown\fR)"
.br
.BR FZF_TOTAL_COUNT " Total number of items"
.br
.BR FZF_MATCH_COUNT " Number of matched items"
@@ -1254,6 +1390,8 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_WRAP " The line wrapping mode (char, word) when enabled"
.br
.BR FZF_QUERY " Current query string"
.br
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
@@ -1262,16 +1400,28 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_PROMPT " Prompt string"
.br
.BR FZF_GHOST " Ghost string"
.br
.BR FZF_POINTER " Pointer string"
.br
.BR FZF_PREVIEW_LABEL " Preview label string"
.br
.BR FZF_BORDER_LABEL " Border label string"
.br
.BR FZF_LIST_LABEL " List label string"
.br
.BR FZF_INPUT_LABEL " Input label string"
.br
.BR FZF_HEADER_LABEL " Header label string"
.br
.BR FZF_ACTION " The name of the last action performed"
.br
.BR FZF_KEY " The name of the last key pressed"
.br
.BR FZF_PORT " Port number when \-\-listen option is used"
.br
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
.br
.BR FZF_PREVIEW_TOP " Top position of the preview window"
.br
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
@@ -1279,6 +1429,8 @@ fzf exports the following environment variables to its child processes.
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
.br
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
.br
.BR FZF_RAW " Only in raw mode. 1 if the current item matches, 0 otherwise"
.SH EXTENDED SEARCH MODE
@@ -1362,7 +1514,7 @@ e.g.
.br
\fIctrl\-/\fR (\fIctrl\-_\fR)
.br
\fIctrl\-alt\-[a\-z]\fR
\fIctrl\-alt\-[a\-z]\fR (\fIctrl\-alt\-h\fR is \fIctrl\-alt\-backspace\fR on non-Windows)
.br
\fIalt\-[*]\fR (Any case-sensitive single character is allowed)
.br
@@ -1382,12 +1534,22 @@ e.g.
.br
\fIalt\-right\fR
.br
\fIalt\-home\fR
.br
\fIalt\-end\fR
.br
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
.br
\fIalt\-delete\fR
.br
\fIalt\-page\-up\fR
.br
\fIalt\-page\-down\fR
.br
\fIalt\-enter\fR
.br
\fIalt\-space\fR
.br
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
.br
\fItab\fR
.br
\fIshift\-tab\fR (\fIbtab\fR)
@@ -1414,6 +1576,26 @@ e.g.
.br
\fIpage\-down\fR (\fIpgdn\fR)
.br
\fIctrl\-up\fR
.br
\fIctrl\-down\fR
.br
\fIctrl\-left\fR
.br
\fIctrl\-right\fR
.br
\fIctrl\-home\fR
.br
\fIctrl\-end\fR
.br
\fIctrl\-backspace\fR (\fIctrl\-bspace\fR \fIctrl\-bs\fR)
.br
\fIctrl\-delete\fR
.br
\fIctrl\-page\-up\fR
.br
\fIctrl\-page\-down\fR
.br
\fIshift\-up\fR
.br
\fIshift\-down\fR
@@ -1422,8 +1604,16 @@ e.g.
.br
\fIshift\-right\fR
.br
\fIshift\-home\fR
.br
\fIshift\-end\fR
.br
\fIshift\-delete\fR
.br
\fIshift\-page\-up\fR
.br
\fIshift\-page\-down\fR
.br
\fIalt\-shift\-up\fR
.br
\fIalt\-shift\-down\fR
@@ -1432,6 +1622,72 @@ e.g.
.br
\fIalt\-shift\-right\fR
.br
\fIalt\-shift\-home\fR
.br
\fIalt\-shift\-end\fR
.br
\fIalt\-shift\-delete\fR
.br
\fIalt\-shift\-page\-up\fR
.br
\fIalt\-shift\-page\-down\fR
.br
\fIctrl\-alt\-up\fR
.br
\fIctrl\-alt\-down\fR
.br
\fIctrl\-alt\-left\fR
.br
\fIctrl\-alt\-right\fR
.br
\fIctrl\-alt\-home\fR
.br
\fIctrl\-alt\-end\fR
.br
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR) (\fIctrl\-alt\-h\fR (non-Windows))
.br
\fIctrl\-alt\-delete\fR
.br
\fIctrl\-alt\-page\-up\fR
.br
\fIctrl\-alt\-page\-down\fR
.br
\fIctrl\-shift\-up\fR
.br
\fIctrl\-shift\-down\fR
.br
\fIctrl\-shift\-left\fR
.br
\fIctrl\-shift\-right\fR
.br
\fIctrl\-shift\-home\fR
.br
\fIctrl\-shift\-end\fR
.br
\fIctrl\-shift\-delete\fR
.br
\fIctrl\-shift\-page\-up\fR
.br
\fIctrl\-shift\-page\-down\fR
.br
\fIctrl\-alt\-shift\-up\fR
.br
\fIctrl\-alt\-shift\-down\fR
.br
\fIctrl\-alt\-shift\-left\fR
.br
\fIctrl\-alt\-shift\-right\fR
.br
\fIctrl\-alt\-shift\-home\fR
.br
\fIctrl\-alt\-shift\-end\fR
.br
\fIctrl\-alt\-shift\-delete\fR
.br
\fIctrl\-alt\-shift\-page\-up\fR
.br
\fIctrl\-alt\-shift\-page\-down\fR
.br
\fIleft\-click\fR
.br
\fIright\-click\fR
@@ -1456,6 +1712,8 @@ e.g.
.br
or any single character
Note that some terminal emulators may not support \fIctrl-*\fR bindings.
.SS AVAILABLE EVENTS:
\fIstart\fR
.RS
@@ -1518,6 +1776,10 @@ e.g.
# Beware not to introduce an infinite loop
seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR
.RE
\fImulti\fR
.RS
Triggered when the multi\-selection has changed.
.RE
\fIone\fR
.RS
@@ -1587,6 +1849,14 @@ e.g.
)'\fR
.RE
\fIclick\-footer\fR
.RS
Triggered when a mouse click occurs within the footer. Sets
\fBFZF_CLICK_FOOTER_LINE\fR and \fBFZF_CLICK_FOOTER_COLUMN\fR environment
variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR
if clicked on a word.
.RE
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
@@ -1596,15 +1866,20 @@ A key or an event can be bound to one or more of the following actions.
\fBaccept\-non\-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBaccept\-or\-print\-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
\fBbackward\-char\fR \fIctrl\-b left\fR
\fBbackward\-delete\-char\fR \fIctrl\-h bspace\fR
\fBbackward\-delete\-char\fR \fIctrl\-h ctrl\-bspace bspace\fR
\fBbackward\-delete\-char/eof\fR (same as \fBbackward\-delete\-char\fR except aborts fzf if query is empty)
\fBbackward\-kill\-subword\fR
\fBbackward\-kill\-word\fR \fIalt\-bs\fR
\fBbackward\-subword\fR
\fBbackward\-word\fR \fIalt\-b shift\-left\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
\fBbell\fR (ring the terminal bell)
\fBbest\fR (move to the best match; same as \fBfirst\fR if raw mode is disabled)
\fBbg\-cancel\fR (cancel background transform processes)
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
\fBchange\-ghost(...)\fR (change ghost text to the given string)
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
@@ -1612,21 +1887,26 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-multi\fR (enable multi-select mode with no limit)
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option)
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
\fBchange\-preview\-window(...)\fR (change \fB\-\-preview\-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange\-prompt(...)\fR (change prompt to the given string)
\fBchange\-query(...)\fR (change query string to the given string)
\fBclear\-screen\fR \fIctrl\-l\fR
\fBclear\-selection\fR (clear multi\-selection)
\fBclear\-multi\fR (clear multi\-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear\-query\fR (clear query string)
\fBdelete\-char\fR \fIdel\fR
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect\-all\fR (deselect all matches)
\fBdeselect\-all\fR (deselect all matches; to also clear non-matching selections, use \fBclear\-multi\fR)
\fBdisable\-raw\fR (disable raw mode)
\fBdisable\-search\fR (disable search functionality)
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
\fBdown\fR \fIctrl\-j down\fR
\fBdown\-match\fR \fIctrl\-n\fR \fIalt\-down\fR (move to the match below the cursor)
\fBdown\-selected\fR (move to the selected item below the cursor)
\fBenable\-raw\fR (enable raw mode)
\fBenable\-search\fR (enable search functionality)
\fBend\-of\-line\fR \fIctrl\-e end\fR
\fBexclude\fR (exclude the current item from the result)
@@ -1635,14 +1915,16 @@ A key or an event can be bound to one or more of the following actions.
\fBexecute\-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward\-char\fR \fIctrl\-f right\fR
\fBforward\-subword\fR
\fBforward\-word\fR \fIalt\-f shift\-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBkill\-line\fR
\fBkill\-subword\fR
\fBkill\-word\fR \fIalt\-d\fR
\fBlast\fR (move to the last match; same as \fBpos(\-1)\fR)
\fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR)
\fBnext\-selected\fR (move to the next selected item)
\fBnext\-selected\fR (synonym to \fBdown\-selected\fR)
\fBpage\-down\fR \fIpgdn\fR
\fBpage\-up\fR \fIpgup\fR
\fBhalf\-page\-down\fR
@@ -1655,7 +1937,7 @@ A key or an event can be bound to one or more of the following actions.
\fBoffset\-middle\fR (place the current item is in the middle of the screen)
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR)
\fBprev\-selected\fR (move to the previous selected item)
\fBprev\-selected\fR (synonym to \fBup\-selected\fR)
\fBpreview(...)\fR (see below for the details)
\fBpreview\-down\fR \fIshift\-down\fR
\fBpreview\-up\fR \fIshift\-up\fR
@@ -1690,32 +1972,43 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle\-multi\-line\fR
\fBtoggle\-preview\fR
\fBtoggle\-preview\-wrap\fR
\fBtoggle\-preview\-wrap\-word\fR
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
\fBtoggle\-search\fR (toggle search functionality)
\fBtoggle\-sort\fR
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
\fBtoggle\-track\-current\fR (toggle tracking of the current item)
\fBtoggle\-wrap\fR \fIctrl\-/\fR \fIalt\-/\fR
\fBtoggle\-wrap\fR
\fBtoggle\-wrap\-word\fR \fIctrl\-/\fR \fIalt\-/\fR
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command)
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
\fBtransform\-header(...)\fR (transform header using an external command)
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
\fBtransform\-nth(...)\fR (transform nth using an external command)
\fBtransform\-pointer(...)\fR (transform pointer using an external command)
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
\fBtransform\-query(...)\fR (transform query string using an external command)
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
\fBtrigger(...)\fR (trigger actions bound to a comma-separated list of keys and events)
\fBunbind(...)\fR (unbind bindings)
\fBunix\-line\-discard\fR \fIctrl\-u\fR
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
\fBup\fR \fIctrl\-k ctrl\-p up\fR
\fBup\fR \fIctrl\-k up\fR
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
\fBup\-selected\fR (move to the selected item above the cursor)
\fByank\fR \fIctrl\-y\fR
Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR
variant that runs the command in the background.
.SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator.
@@ -1833,13 +2126,33 @@ payload of HTTP POST request to the \fB\-\-listen\fR server.
e.g.
\fB# Disallow selecting an empty line
echo \-e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
printf "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\
\-\-bind 'enter:transform:[[ \-n {} ]] &&
echo accept ||
echo "change\-header:Invalid selection"'
\fR
A common mistake when writing a \fBtransform\fR action is not escaping
placeholder expressions when passing them back to fzf. In the following
example, if you don't escape \fB{}\fR, fzf will immediately replace it with the
single-quoted string of the current item. This causes single quotes to appear
in the header and footer, and the script will break if any item contains
double-quote characters.
\fBfzf \-\-bind 'focus:transform:[[ $FZF_ACTION =~ up ]] &&
echo "change\-header()+transform\-footer:echo \\{}" ||
echo "change\-footer()+transform\-header:echo \\{}"'\fR
.SS TRANSFORM IN THE BACKGROUND
Transform actions are synchronous, meaning fzf becomes unresponsive while the
command runs. To avoid this, each \fBtransform*\fR action has a corresponding
\fBbg\-transform*\fR variant that runs in the background. Unless you need to
chain multiple transform actions where later ones depend on earlier results,
prefer using the \fBbg\fR variant. To cancel currently running background
transform processes, use \fBbg\-cancel\fR action.
.SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2024 Junegunn Choi
" Copyright (c) 2013-2026 Junegunn Choi
"
" MIT License
"
@@ -358,7 +358,7 @@ endfunction
function! s:get_color(attr, ...)
" 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 gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000
@@ -553,8 +553,15 @@ try
let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --no-tmux --height='.height
endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
" Add 1-column horizontal margin
let optstr = join(['--margin 0,1', optstr])
else
" 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])
endif
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term
@@ -1020,8 +1027,23 @@ if has('nvim')
let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let win = nvim_open_win(buf, v:true, opts)
silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
call setwinvar(win, '&colorcolumn', '')
" Colors
try
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
let rules = get(g:, 'fzf_colors', {})
if has_key(rules, 'bg')
let color = call('s:get_color', rules.bg)
if len(color)
let ns = nvim_create_namespace('fzf_popup')
let hl = nvim_set_hl(ns, 'Normal',
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
call nvim_win_set_hl_ns(win, ns)
endif
endif
catch
endtry
return buf
endfunction
else

141
shell/common.fish Normal file
View File

@@ -0,0 +1,141 @@
function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
# Get tokens - use version-appropriate flags
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
# Filter out leading environment variable assignments
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
# Skip command prefixes so callers see the actual command name,
# e.g. "builtin cd" → "cd", "env VAR=1 command cd" → "cd"
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
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 fzf_query ''
set -l prefix ''
set -l dir '.'
# Set variables containing the major and minor fish version numbers, using
# a method compatible with all supported fish versions.
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.4.1
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
# fish v3.1b1 - v3.1.2
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end

40
shell/common.sh Normal file
View File

@@ -0,0 +1,40 @@
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
# This function performs `exec awk "$@"` safely by working around awk
# compatibility issues.
#
# To reduce an extra fork, this function performs "exec" so is expected to be
# run as the last command in a subshell.
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
# with an ancient implementation of 1977 awk in the original UNIX. It
# lacks many features of POSIX awk, so it is essentially useless in the
# modern point of view. To use a standard-conforming version in Solaris,
# one needs to explicitly use /usr/xpg4/bin/awk.
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
# choose the faster mawk if: it's installed && build date >= 20230322 &&
# version >= 1.3.4
local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi
fi
# Note: macOS awk has a quirk that it stops processing at all when it sees
# any data not following UTF-8 in the input stream when the current LC_CTYPE
# specifies the UTF-8 encoding. To work around this quirk, one needs to
# specify LC_ALL=C to change the current encoding to the plain one.
LC_ALL=C exec "$__fzf_awk" "$@"
}

View File

@@ -31,21 +31,40 @@ if [[ $- =~ i ]]; then
###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
#----BEGIN shfmt
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__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%} --min-height 20+ --bind=ctrl-z:ignore $1"
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
if [[ "$(type -t _fzf_comprun 2>&1)" == function ]]; then
_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
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
else
@@ -57,13 +76,13 @@ __fzf_comprun() {
__fzf_orig_completion() {
local l comp f cmd
while read -r l; do
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
comp="${BASH_REMATCH[1]}"
f="${BASH_REMATCH[2]}"
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}"
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 "
fi
fi
@@ -99,12 +118,13 @@ _fzf_opts_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
prev="${COMP_WORDS[COMP_CWORD - 1]}"
opts="
+c --no-color
+i --no-ignore-case
+s --no-sort
+x --no-extended
--accept-nth
--ansi
--bash
--bind
@@ -118,56 +138,88 @@ _fzf_opts_completion() {
--expect
--filepath-word
--fish
--footer
--footer-border
--footer-label
--footer-label-pos
--freeze-left
--freeze-right
--gap
--gap-line
--ghost
--gutter
--gutter-raw
--header
--header-border
--header-first
--header-label
--header-label-pos
--header-lines
--header-lines-border
--height
--highlight-line
--history
--history-size
--hscroll-off
--info
--info-command
--input-border
--input-label
--input-label-pos
--jump-labels
--keep-right
--layout
--listen
--listen-unsafe
--list-border
--list-label
--list-label-pos
--literal
--man
--margin
--marker
--marker-multi-line
--min-height
--no-bold
--no-clear
--no-hscroll
--no-mouse
--no-input
--no-multi-line
--no-scrollbar
--no-separator
--no-unicode
--padding
--pointer
--preview
--preview-border
--preview-label
--preview-label-pos
--preview-window
--print-query
--print0
--prompt
--raw
--read0
--reverse
--scheme
--scroll-off
--scrollbar
--separator
--smart-case
--style
--sync
--tabstop
--tac
--tail
--tiebreak
--tmux
--track
--version
--walker
--walker-root
--walker-skip
--with-nth
--with-shell
--wrap
--wrap-sign
--preview-wrap-sign
--zsh
-0 --exit-0
-1 --select-1
@@ -182,32 +234,41 @@ _fzf_opts_completion() {
--"
case "${prev}" in
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
;;
--tiebreak)
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
return 0
;;
--color)
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
return 0
;;
--layout)
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 "
--scheme)
COMPREPLY=($(compgen -W "default path history" -- "$cur"))
return 0
;;
--tiebreak)
COMPREPLY=($(compgen -W "length chunk pathname begin end index" -- "$cur"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "dark light base16 16 bw no" -- "$cur"))
return 0
;;
--layout)
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
return 0
;;
--info)
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
return 0
;;
--wrap)
COMPREPLY=($(compgen -W "char word" -- "$cur"))
return 0
;;
--style)
COMPREPLY=($(compgen -W "default minimal full" -- "$cur"))
return 0
;;
--preview-window)
COMPREPLY=($(compgen -W "
default
hidden
nohidden
wrap
wrap-word
nowrap
cycle
nocycle
@@ -216,6 +277,7 @@ _fzf_opts_completion() {
left
right
rounded border border-rounded
border-line
sharp border-sharp
border-bold
border-block
@@ -229,21 +291,23 @@ _fzf_opts_completion() {
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
;;
nofollow
info
noinfo" -- "$cur"))
return 0
;;
--border | --list-border | --header-border | --header-lines-border | --footer-border | --input-border | --preview-border)
COMPREPLY=($(compgen -W "line rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
return 0
;;
--border-label-pos | --preview-label-pos | --list-label-pos | --header-label-pos | --footer-label-pos | --input-label-pos)
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
return 0
;;
esac
if [[ "$cur" =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
if [[ $cur =~ ^-|\+ ]]; then
COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
return 0
fi
@@ -257,7 +321,7 @@ _fzf_handle_dynamic_completion() {
orig_cmd="$1"
if __fzf_orig_completion_get_orig_func "$cmd"; then
"$REPLY" "$@"
elif [[ -n "${_fzf_completion_loader-}" ]]; then
elif [[ -n ${_fzf_completion_loader-} ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
$_fzf_completion_loader "$@"
ret=$?
@@ -271,7 +335,7 @@ _fzf_handle_dynamic_completion() {
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
orig_complete=$REPLY
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
else
eval "$orig_complete"
@@ -291,48 +355,53 @@ __fzf_generic_path_completion() {
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" 2> /dev/null || return
dir=
[[ $base = *"/"* ]] && dir="$base"
[[ $base == *"/"* ]] && dir="$base"
while true; do
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
if [[ -z $dir ]] || [[ -d $dir ]]; then
leftover=${base/#"$dir"/}
leftover=${leftover/#\//}
[[ -z $dir ]] && dir='.'
[[ $dir != "/" ]] && dir="${dir/%\//}"
matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if [[ $1 =~ dir ]]; then
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
else
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
fi
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
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
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
)
matches=${matches% }
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n "$matches" ]]; then
COMPREPLY=( "$matches" )
[[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n $matches ]]; then
COMPREPLY=("$matches")
else
COMPREPLY=( "$cur" )
COMPREPLY=("$cur")
fi
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n'
return 0
fi
dir=$(command dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/
[[ $dir =~ /$ ]] || dir="$dir"/
done
else
shift
@@ -348,15 +417,15 @@ _fzf_complete() {
args=("$@")
sep=
for i in "${!args[@]}"; do
if [[ "${args[$i]}" = -- ]]; then
if [[ ${args[$i]} == -- ]]; then
sep=$i
break
fi
done
if [[ -n "$sep" ]]; then
if [[ -n $sep ]]; then
str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}")
args=("${args[@]:0:sep}")
else
str_arg=$1
args=()
@@ -365,25 +434,27 @@ _fzf_complete() {
fi
local cur selected trigger cmd post
post="$(caller 0 | command awk '{print $2}')_post"
post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post='command cat'
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cmd="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}}
selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' '
)
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then
if [[ -n $selected ]]; then
COMPREPLY=("$selected")
else
COMPREPLY=("$cur")
fi
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n'
return 0
else
@@ -436,14 +507,14 @@ _fzf_proc_completion() {
'
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
--bind "click-header:transform:$transformer" -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin
)
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin
)
}
_fzf_proc_completion_post() {
command awk '{print $2}'
__fzf_exec_awk '{print $2}'
}
# To use custom hostname lists, override __fzf_list_hosts.
@@ -460,10 +531,54 @@ _fzf_proc_completion_post() {
# }
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
command sort -u \
<(
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
# properly, we need to adjust the related shell options. We need to
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
# avoid matching unwanted files. We need to unset "failglob" to avoid
# outputting the error messages to the terminal when no matching is
# found. We need to set "nullglob" to avoid attempting to read the
# literal filename '~/.ssh/config.d/*' when no matching is found.
set +f
GLOBIGNORE=
shopt -u dotglob nocaseglob failglob
shopt -s nullglob
__fzf_exec_awk '
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
sub(/#.*/, "")
for (i = 1; i <= NF; i++)
if ($i !~ /[*?%]/)
print $i
}
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
) \
<(
__fzf_exec_awk -F ',' '
match($0, /^[][a-zA-Z0-9.,:-]+/) {
$0 = substr($0, 1, RLENGTH)
gsub(/[][]|:[^,]*/, "")
for (i = 1; i <= NF; i++)
print $i
}
' ~/.ssh/known_hosts 2> /dev/null
) \
<(
__fzf_exec_awk '
{
sub(/#.*/, "")
for (i = 2; i <= NF; i++)
if ($i != "0.0.0.0")
print $i
}
' /etc/hosts 2> /dev/null
)
}
fi
@@ -478,13 +593,13 @@ _fzf_host_completion() {
# > 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)
-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}')
[[ $2 =~ '@' ]] && user="${2%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
;;
esac
}
@@ -572,7 +687,7 @@ __fzf_defc() {
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
eval "$REPLY"
else
complete -F "$func" $opts "$cmd"
eval "complete -F \"$func\" $opts \"$cmd\""
fi
}
@@ -614,12 +729,13 @@ _fzf_setup_completion() {
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
for cmd in "$@"; do
case "$kind" in
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
esac
done
}
#----END shfmt
fi

241
shell/completion.fish Normal file
View File

@@ -0,0 +1,241 @@
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ completion.fish
#
# - $FZF_COMPLETION_OPTS (default: empty)
function fzf_completion_setup
#----BEGIN INCLUDE common.fish
# NOTE: Do not directly edit this section, which is copied from "common.fish".
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
# the changes. See code comments in "common.fish" for the implementation details.
function __fzf_defaults
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
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 fzf_query ''
set -l prefix ''
set -l dir '.'
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
if test "$fish_major" -ge 4
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
if test "$fish_major" -ge 4
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Use complete builtin for specific commands
function __fzf_complete_native
set -l -- token (commandline -t)
set -l -- completions (eval complete -C \"$argv[1]\")
test -n "$completions"; or begin commandline -f repaint; return; end
# Calculate tabstop based on longest completion item (sample first 500 for performance)
set -l -- tabstop 20
set -l -- sample_size (math "min(500, "(count $completions)")")
for c in $completions[1..$sample_size]
set -l -- len (string length -V -- (string split -- \t $c))
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
and set -- tabstop $len[1]
end
# limit to 120 to prevent long lines
set -- tabstop (math "min($tabstop + 4, 120)")
set -l result
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
and begin
set -l -- tail ' '
# Append / to bare ~username results (fish omits it unlike other shells)
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
# Don't add trailing space if single result is a directory
test (count $result) -eq 1
and string match -q -- '*/' "$result"; and set -- tail ''
set -l -- result (string escape -n -- $result)
string match -q -- '~*' "$token"
and set result (string replace -r -- '^\\\\~' '~' $result)
string match -q -- '$*' "$token"
and set result (string replace -r -- '^\\\\\$' '\$' $result)
commandline -rt -- (string join ' ' -- $result)$tail
end
commandline -f repaint
end
function _fzf_complete
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
set -l -- fzf_query (commandline -t | string escape)
set -l result
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
and if functions -q $post_func
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
else
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
end
commandline -f repaint
end
# Kill completion (process selection)
function _fzf_complete_kill
set -l -- fzf_query (commandline -t | string escape)
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
set -lx FZF_DEFAULT_OPTS_FILE
if type -q ps
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
'or command ps --everyone --full --windows 2>/dev/null; end'
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
and commandline -rt -- (string join ' ' -- $result)" "
else
__fzf_complete_native "kill " --multi --query=$fzf_query
end
commandline -f repaint
end
# Main completion function
function fzf-completion
set -l -- tokens (__fzf_cmd_tokens)
set -l -- current_token (commandline -t)
set -l -- cmd_name $tokens[1]
# Route to appropriate completion function
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
_fzf_complete_$cmd_name $tokens
else
set -l -- fzf_opt --query=$current_token --multi
__fzf_complete_native "$tokens $current_token" $fzf_opt
end
end
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
if test (string match -r -- '^\d+' $version) -ge 4
bind shift-tab fzf-completion
bind -M insert shift-tab fzf-completion
else
bind -k btab fzf-completion
bind -M insert -k btab fzf-completion
end
end
# Run setup
fzf_completion_setup

View File

@@ -96,14 +96,35 @@ if [[ -o interactive ]]; then
###########################################################
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__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 -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo -E "${FZF_DEFAULT_OPTS-} $2"
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@"
@@ -153,15 +174,18 @@ __fzf_generic_path_completion() {
export FZF_DEFAULT_OPTS
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if [[ $compgen =~ dir ]]; then
rest=${FZF_COMPLETION_DIR_OPTS-}
else
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" ${(Q)${(Z+n+)rest}}
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_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read -r item; do
@@ -242,11 +266,50 @@ _fzf_complete() {
# 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
command sort -u \
<(
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
# properly, we need to adjust the related shell options. We need to
# unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
# when no matching is found.
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
__fzf_exec_awk '
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
sub(/#.*/, "")
for (i = 1; i <= NF; i++)
if ($i !~ /[*?%]/)
print $i
}
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
) \
<(
__fzf_exec_awk -F ',' '
match($0, /^[][a-zA-Z0-9.,:-]+/) {
$0 = substr($0, 1, RLENGTH)
gsub(/[][]|:[^,]*/, "")
for (i = 1; i <= NF; i++)
print $i
}
' ~/.ssh/known_hosts 2> /dev/null
) \
<(
__fzf_exec_awk '
{
sub(/#.*/, "")
for (i = 2; i <= NF; i++)
if ($i != "0.0.0.0")
print $i
}
' /etc/hosts 2> /dev/null
)
}
fi
@@ -266,7 +329,7 @@ _fzf_complete_ssh() {
*)
local user
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
;;
esac
}
@@ -324,7 +387,7 @@ _fzf_complete_kill() {
}
_fzf_complete_kill_post() {
awk '{print $2}'
__fzf_exec_awk '{print $2}'
}
fzf-completion() {

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
@@ -17,40 +18,62 @@ if [[ $- =~ i ]]; then
# Key bindings
# ------------
#----BEGIN shfmt
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__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%} --min-height 20+ --bind=ctrl-z:ignore $1"
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__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) "$@" |
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
printf '%q ' "$item" # escape special chars
done
}
__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"
}
fzf-file-widget() {
local selected="$(__fzf_select__ "$@")"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
READLINE_POINT=$((READLINE_POINT + ${#selected}))
}
__fzf_cd__() {
local dir
dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
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)"
}
@@ -62,11 +85,11 @@ if command -v perl > /dev/null; then
set +o pipefail
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z "$READLINE_POINT" ]]; then
if [[ -z $READLINE_POINT ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
@@ -74,14 +97,8 @@ if command -v perl > /dev/null; then
}
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
local output script
[[ $(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 }
@@ -89,13 +106,13 @@ else # awk - fallback for POSIX systems
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 --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
if [[ -z $READLINE_POINT ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
@@ -104,43 +121,54 @@ else # awk - fallback for POSIX systems
fi
# Required to refresh the prompt after fzf
bind -m emacs-standard '"\er": redraw-current-line'
bind -m emacs-standard '"\C-\e(": redraw-current-line'
bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode'
if (( BASH_VERSINFO[0] < 4 )); then
if ((BASH_VERSINFO[0] < 4)); then
# CTRL-T - Paste the selected file path into the command line
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\C-\e(\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\C-\e("'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
fi
else
# CTRL-T - Paste the selected file path into the command line
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
bind -m vi-command -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget'
fi
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard -x '"\C-r": __fzf_history__'
bind -m vi-command -x '"\C-r": __fzf_history__'
bind -m vi-insert -x '"\C-r": __fzf_history__'
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
bind -m emacs-standard -x '"\C-r": __fzf_history__'
bind -m vi-command -x '"\C-r": __fzf_history__'
bind -m vi-insert -x '"\C-r": __fzf_history__'
fi
fi
# ALT-C - cd into the selected directory
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\C-\e(\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
#----END shfmt
fi

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
@@ -14,15 +15,27 @@
# Key bindings
# ------------
# For compatibility with fish versions down to 3.1.2, the script does not use:
# - The -f/--function switch of command: set
# - The process substitution syntax: $(cmd)
# - Ranges that omit start/end indexes: $var[$start..] $var[..$end] $var[..]
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
# command substitution syntax $(cmd) should never be used, even behind a version
# check, otherwise the source command will fail on fish versions older than 3.4.0.
function fzf_key_bindings
# Check fish version
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
echo "This script requires fish version 3.1b1 or newer." >&2
return 1
else if not type -q fzf
echo "fzf was not found in path." >&2
return 1
end
#----BEGIN INCLUDE common.fish
# NOTE: Do not directly edit this section, which is copied from "common.fish".
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
# the changes. See code comments in "common.fish" for the implementation details.
function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
@@ -41,51 +54,107 @@ function fzf_key_bindings
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
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 fzf_query ''
set -l prefix ''
set -l dir '.'
set -l query
set -l commandline (commandline -t | string unescape -n)
# Strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# Enable home directory expansion of leading ~/
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Escape special characters, except for the $ sign of valid variable names,
# so that the original string with expanded variables is returned after eval.
set commandline (string escape -n -- $commandline)
set commandline (string replace -r -a -- '\\\\\$(?=[\w])' '\$' $commandline)
if test "$fish_major" -ge 4
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
# eval is used to do shell expansion on paths
eval set commandline $commandline
# Combine multiple consecutive slashes into one.
set commandline (string replace -r -a -- '/+' '/' $commandline)
if test -n "$commandline"
# Strip trailing slash, unless $dir is root dir (/)
set dir (string replace -r -- '(?<!^)/$' '' $commandline)
# Set $dir to the longest existing filepath
while not test -d "$dir"
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
set dir (dirname -- $dir)
if test -n "$fzf_query"
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if test "$dir" = '.'; and test (string sub -l 2 -- $commandline) != './'
# If $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline
else
# Also remove trailing slash after dir, to "split" input properly
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
if test "$fish_major" -ge 4
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
@@ -95,52 +164,77 @@ function fzf_key_bindings
set -l prefix $commandline[3]
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" \
"$FZF_CTRL_T_OPTS --multi")
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
"--multi $FZF_CTRL_T_OPTS --print0")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE
if set -l result (eval (__fzfcmd) --query=$fzf_query)
# Remove last token from commandline.
commandline -t ''
for i in $result
commandline -it -- $prefix(string escape -- $i)' '
end
end
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
commandline -f repaint
end
function fzf-history-widget -d "Show command history"
set -l fzf_query (commandline | string escape)
set -l -- command_line (commandline)
set -l -- current_line (commandline -L)
set -l -- total_lines (count $command_line)
set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
"--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
# Add dynamic preview options if preview command isn't already set by user
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
# Convert the highlighted timestamp using the date command if available
set -l -- date_cmd '{1}'
if type -q date
if date -d @0 '+%s' 2>/dev/null | string match -q 0
# GNU date
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
# BSD date
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
end
end
# Prepend the options to allow user customizations
set -p -- FZF_DEFAULT_OPTS \
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
end
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
set FZF_DEFAULT_COMMAND \
'set -l h (builtin history -z --reverse | string split0);' \
'for i in (seq (count $h) -1 1);' \
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end'
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
# Enable syntax highlighting colors on fish v4.3.3 and newer
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
set -a -- FZF_DEFAULT_OPTS '--ansi'
set -a -- FZF_DEFAULT_COMMAND '--color=always'
end
# Merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
commandline -- (string replace -a -- \n\t \n $result[1])
test (count $result) -gt 1; and for i in $result[2..-1]
commandline -i -- (string replace -a -- \n\t \n \n$i)
if test "$total_lines" -eq 1
commandline -- $result
else
set -l a (math $current_line - 1)
set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] $result
commandline -a -- '' $command_line[$b..-1]
end
end
@@ -154,13 +248,13 @@ function fzf_key_bindings
set -l prefix $commandline[3]
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" \
"$FZF_ALT_C_OPTS --no-multi")
"--reverse --walker=dir,follow,hidden --scheme=path" \
"$FZF_ALT_C_OPTS --no-multi --print0")
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
if set -l result (eval (__fzfcmd) --query=$fzf_query)
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
cd -- $result
commandline -rt -- $prefix
end
@@ -168,8 +262,13 @@ function fzf_key_bindings
commandline -f repaint
end
bind \cr fzf-history-widget
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
if test -n "$FZF_CTRL_R_COMMAND"
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
end
bind \cr fzf-history-widget
bind -M insert \cr fzf-history-widget
end
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
@@ -182,3 +281,6 @@ function fzf_key_bindings
end
end
# Run setup
fzf_key_bindings

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
@@ -38,14 +39,35 @@ fi
{
if [[ -o interactive ]]; then
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__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 -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo -E "${FZF_DEFAULT_OPTS-} $2"
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
# CTRL-T - Paste the selected file path(s) into the command line
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
@@ -106,25 +128,52 @@ fi
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
local selected extracted_with_perl=0
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
# Ensure the module is loaded if not already, and the required features, such
# as the associative 'history' array, which maps event numbers to full history
# lines, are set. Also, make sure Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
extracted_with_perl=1
else
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi
local ret=$?
local -a cmds
# Avoid leaking auto assigned values when using backreferences '(#b)'
local -a mbegin mend match
if [ -n "$selected" ]; then
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
zle vi-fetch-history -n $MATCH
# Heuristic to check if the selected value is from history or a custom query
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
# Split at newlines
for line in ${(ps:\n:)selected}; do
if (( extracted_with_perl )); then
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
fi
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
# Avoid $history array: lags behind 'fc' on foreign commands (*)
# https://zsh.org/mla/users/2024/msg00692.html
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
zle .push-line
zle vi-fetch-history -n ${match[1]}
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
BUFFER=""
zle .get-line
fi
done
if (( ${#cmds[@]} )); then
# Join by newline after stripping trailing newlines from each command
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
CURSOR=${#BUFFER}
fi
else # selected is a custom query, not from history
LBUFFER="$selected"
fi
@@ -132,10 +181,15 @@ fzf-history-widget() {
zle reset-prompt
return $ret
}
zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
fi
fi
} always {

70
shell/update.sh Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# This script applies the contents of "common.sh" to the other files.
set -e
dir=${0%"${0##*/}"}
update() {
{
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
cat << EOF
# NOTE: Do not directly edit this section, which is copied from "$1".
# To modify it, one can edit "$1" and run "./update.sh" to apply
# the changes. See code comments in "$1" for the implementation details.
EOF
echo
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
sed -n '/^#----END INCLUDE/,$p' "$2"
} > "$2.part"
mv -f "$2.part" "$2"
}
update "common.sh" "$dir/completion.bash"
update "common.sh" "$dir/completion.zsh"
update "common.sh" "$dir/key-bindings.bash"
update "common.sh" "$dir/key-bindings.zsh"
update "common.fish" "$dir/completion.fish"
update "common.fish" "$dir/key-bindings.fish"
# Check if --check is in ARGV
check=0
rest=()
for arg in "$@"; do
case $arg in
--check) check=1 ;;
*) rest+=("$arg") ;;
esac
done
fmt() {
if ! grep -q "^#----BEGIN shfmt" "$1"; then
if [[ $check == 1 ]]; then
shfmt -d "$1"
return $?
else
shfmt -w "$1"
fi
else
{
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
} > "$1.part"
if [[ $check == 1 ]]; then
diff -q "$1" "$1.part"
ret=$?
rm -f "$1.part"
return $ret
fi
mv -f "$1.part" "$1"
fi
}
for file in "${rest[@]}"; do
fmt "$file" || exit $?
done

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi
Copyright (c) 2013-2026 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

View File

@@ -12,139 +12,179 @@ func _() {
_ = 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[actChangeListLabel-17]
_ = x[actChangeInputLabel-18]
_ = x[actChangeHeader-19]
_ = x[actChangeHeaderLabel-20]
_ = x[actChangeMulti-21]
_ = x[actChangePreviewLabel-22]
_ = x[actChangePrompt-23]
_ = x[actChangeQuery-24]
_ = x[actChangeNth-25]
_ = x[actClearScreen-26]
_ = x[actClearQuery-27]
_ = x[actClearSelection-28]
_ = x[actClose-29]
_ = x[actDeleteChar-30]
_ = x[actDeleteCharEof-31]
_ = x[actEndOfLine-32]
_ = x[actFatal-33]
_ = x[actForwardChar-34]
_ = x[actForwardWord-35]
_ = x[actKillLine-36]
_ = x[actKillWord-37]
_ = x[actUnixLineDiscard-38]
_ = x[actUnixWordRubout-39]
_ = x[actYank-40]
_ = x[actBackwardKillWord-41]
_ = x[actSelectAll-42]
_ = x[actDeselectAll-43]
_ = x[actToggle-44]
_ = x[actToggleSearch-45]
_ = x[actToggleAll-46]
_ = x[actToggleDown-47]
_ = x[actToggleUp-48]
_ = x[actToggleIn-49]
_ = x[actToggleOut-50]
_ = x[actToggleTrack-51]
_ = x[actToggleTrackCurrent-52]
_ = x[actToggleHeader-53]
_ = x[actToggleWrap-54]
_ = x[actToggleMultiLine-55]
_ = x[actToggleHscroll-56]
_ = x[actTrackCurrent-57]
_ = x[actToggleInput-58]
_ = x[actHideInput-59]
_ = x[actShowInput-60]
_ = x[actUntrackCurrent-61]
_ = x[actDown-62]
_ = x[actUp-63]
_ = x[actPageUp-64]
_ = x[actPageDown-65]
_ = x[actPosition-66]
_ = x[actHalfPageUp-67]
_ = x[actHalfPageDown-68]
_ = x[actOffsetUp-69]
_ = x[actOffsetDown-70]
_ = x[actOffsetMiddle-71]
_ = x[actJump-72]
_ = x[actJumpAccept-73]
_ = x[actPrintQuery-74]
_ = x[actRefreshPreview-75]
_ = x[actReplaceQuery-76]
_ = x[actToggleSort-77]
_ = x[actShowPreview-78]
_ = x[actHidePreview-79]
_ = x[actTogglePreview-80]
_ = x[actTogglePreviewWrap-81]
_ = x[actTransform-82]
_ = x[actTransformBorderLabel-83]
_ = x[actTransformListLabel-84]
_ = x[actTransformInputLabel-85]
_ = x[actTransformHeader-86]
_ = x[actTransformHeaderLabel-87]
_ = x[actTransformNth-88]
_ = x[actTransformPreviewLabel-89]
_ = x[actTransformPrompt-90]
_ = x[actTransformQuery-91]
_ = x[actTransformSearch-92]
_ = x[actSearch-93]
_ = x[actPreview-94]
_ = x[actChangePreview-95]
_ = x[actChangePreviewWindow-96]
_ = x[actPreviewTop-97]
_ = x[actPreviewBottom-98]
_ = x[actPreviewUp-99]
_ = x[actPreviewDown-100]
_ = x[actPreviewPageUp-101]
_ = x[actPreviewPageDown-102]
_ = x[actPreviewHalfPageUp-103]
_ = x[actPreviewHalfPageDown-104]
_ = x[actPrevHistory-105]
_ = x[actPrevSelected-106]
_ = x[actPrint-107]
_ = x[actPut-108]
_ = x[actNextHistory-109]
_ = x[actNextSelected-110]
_ = x[actExecute-111]
_ = x[actExecuteSilent-112]
_ = x[actExecuteMulti-113]
_ = x[actSigStop-114]
_ = x[actFirst-115]
_ = x[actLast-116]
_ = x[actReload-117]
_ = x[actReloadSync-118]
_ = x[actDisableSearch-119]
_ = x[actEnableSearch-120]
_ = x[actSelect-121]
_ = x[actDeselect-122]
_ = x[actUnbind-123]
_ = x[actRebind-124]
_ = x[actToggleBind-125]
_ = x[actBecome-126]
_ = x[actShowHeader-127]
_ = x[actHideHeader-128]
_ = x[actBell-129]
_ = x[actExclude-130]
_ = x[actExcludeMulti-131]
_ = x[actBracketedPasteBegin-4]
_ = x[actBracketedPasteEnd-5]
_ = x[actChar-6]
_ = x[actMouse-7]
_ = x[actBeginningOfLine-8]
_ = x[actAbort-9]
_ = x[actAccept-10]
_ = x[actAcceptNonEmpty-11]
_ = x[actAcceptOrPrintQuery-12]
_ = x[actBackwardChar-13]
_ = x[actBackwardDeleteChar-14]
_ = x[actBackwardDeleteCharEof-15]
_ = x[actBackwardWord-16]
_ = x[actBackwardSubWord-17]
_ = x[actCancel-18]
_ = x[actChangeBorderLabel-19]
_ = x[actChangeGhost-20]
_ = x[actChangeHeader-21]
_ = x[actChangeFooter-22]
_ = x[actChangeHeaderLabel-23]
_ = x[actChangeFooterLabel-24]
_ = x[actChangeInputLabel-25]
_ = x[actChangeListLabel-26]
_ = x[actChangeMulti-27]
_ = x[actChangeNth-28]
_ = x[actChangePointer-29]
_ = x[actChangePreview-30]
_ = x[actChangePreviewLabel-31]
_ = x[actChangePreviewWindow-32]
_ = x[actChangePrompt-33]
_ = x[actChangeQuery-34]
_ = x[actClearScreen-35]
_ = x[actClearQuery-36]
_ = x[actClearSelection-37]
_ = x[actClose-38]
_ = x[actDeleteChar-39]
_ = x[actDeleteCharEof-40]
_ = x[actEndOfLine-41]
_ = x[actFatal-42]
_ = x[actForwardChar-43]
_ = x[actForwardWord-44]
_ = x[actForwardSubWord-45]
_ = x[actKillLine-46]
_ = x[actKillWord-47]
_ = x[actKillSubWord-48]
_ = x[actUnixLineDiscard-49]
_ = x[actUnixWordRubout-50]
_ = x[actYank-51]
_ = x[actBackwardKillWord-52]
_ = x[actBackwardKillSubWord-53]
_ = x[actSelectAll-54]
_ = x[actDeselectAll-55]
_ = x[actToggle-56]
_ = x[actToggleSearch-57]
_ = x[actToggleAll-58]
_ = x[actToggleDown-59]
_ = x[actToggleUp-60]
_ = x[actToggleIn-61]
_ = x[actToggleOut-62]
_ = x[actToggleTrack-63]
_ = x[actToggleTrackCurrent-64]
_ = x[actToggleHeader-65]
_ = x[actToggleWrap-66]
_ = x[actToggleWrapWord-67]
_ = x[actToggleMultiLine-68]
_ = x[actToggleHscroll-69]
_ = x[actToggleRaw-70]
_ = x[actEnableRaw-71]
_ = x[actDisableRaw-72]
_ = x[actTrackCurrent-73]
_ = x[actToggleInput-74]
_ = x[actHideInput-75]
_ = x[actShowInput-76]
_ = x[actUntrackCurrent-77]
_ = x[actDown-78]
_ = x[actDownMatch-79]
_ = x[actUp-80]
_ = x[actUpMatch-81]
_ = x[actPageUp-82]
_ = x[actPageDown-83]
_ = x[actPosition-84]
_ = x[actHalfPageUp-85]
_ = x[actHalfPageDown-86]
_ = x[actOffsetUp-87]
_ = x[actOffsetDown-88]
_ = x[actOffsetMiddle-89]
_ = x[actJump-90]
_ = x[actJumpAccept-91]
_ = x[actPrintQuery-92]
_ = x[actRefreshPreview-93]
_ = x[actReplaceQuery-94]
_ = x[actToggleSort-95]
_ = x[actShowPreview-96]
_ = x[actHidePreview-97]
_ = x[actTogglePreview-98]
_ = x[actTogglePreviewWrap-99]
_ = x[actTogglePreviewWrapWord-100]
_ = x[actTransform-101]
_ = x[actTransformBorderLabel-102]
_ = x[actTransformGhost-103]
_ = x[actTransformHeader-104]
_ = x[actTransformFooter-105]
_ = x[actTransformHeaderLabel-106]
_ = x[actTransformFooterLabel-107]
_ = x[actTransformInputLabel-108]
_ = x[actTransformListLabel-109]
_ = x[actTransformNth-110]
_ = x[actTransformPointer-111]
_ = x[actTransformPreviewLabel-112]
_ = x[actTransformPrompt-113]
_ = x[actTransformQuery-114]
_ = x[actTransformSearch-115]
_ = x[actTrigger-116]
_ = x[actBgTransform-117]
_ = x[actBgTransformBorderLabel-118]
_ = x[actBgTransformGhost-119]
_ = x[actBgTransformHeader-120]
_ = x[actBgTransformFooter-121]
_ = x[actBgTransformHeaderLabel-122]
_ = x[actBgTransformFooterLabel-123]
_ = x[actBgTransformInputLabel-124]
_ = x[actBgTransformListLabel-125]
_ = x[actBgTransformNth-126]
_ = x[actBgTransformPointer-127]
_ = x[actBgTransformPreviewLabel-128]
_ = x[actBgTransformPrompt-129]
_ = x[actBgTransformQuery-130]
_ = x[actBgTransformSearch-131]
_ = x[actBgCancel-132]
_ = x[actSearch-133]
_ = x[actPreview-134]
_ = x[actPreviewTop-135]
_ = x[actPreviewBottom-136]
_ = x[actPreviewUp-137]
_ = x[actPreviewDown-138]
_ = x[actPreviewPageUp-139]
_ = x[actPreviewPageDown-140]
_ = x[actPreviewHalfPageUp-141]
_ = x[actPreviewHalfPageDown-142]
_ = x[actPrevHistory-143]
_ = x[actPrevSelected-144]
_ = x[actPrint-145]
_ = x[actPut-146]
_ = x[actNextHistory-147]
_ = x[actNextSelected-148]
_ = x[actExecute-149]
_ = x[actExecuteSilent-150]
_ = x[actExecuteMulti-151]
_ = x[actSigStop-152]
_ = x[actBest-153]
_ = x[actFirst-154]
_ = x[actLast-155]
_ = x[actReload-156]
_ = x[actReloadSync-157]
_ = x[actDisableSearch-158]
_ = x[actEnableSearch-159]
_ = x[actSelect-160]
_ = x[actDeselect-161]
_ = x[actUnbind-162]
_ = x[actRebind-163]
_ = x[actToggleBind-164]
_ = x[actBecome-165]
_ = x[actShowHeader-166]
_ = x[actHideHeader-167]
_ = x[actBell-168]
_ = x[actExclude-169]
_ = x[actExcludeMulti-170]
_ = x[actAsync-171]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798, 1808, 1823}
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 992, 1010, 1026, 1038, 1050, 1063, 1078, 1092, 1104, 1116, 1133, 1140, 1152, 1157, 1167, 1176, 1187, 1198, 1211, 1226, 1237, 1250, 1265, 1272, 1285, 1298, 1315, 1330, 1343, 1357, 1371, 1387, 1407, 1431, 1443, 1466, 1483, 1501, 1519, 1542, 1565, 1587, 1608, 1623, 1642, 1666, 1684, 1701, 1719, 1729, 1743, 1768, 1787, 1807, 1827, 1852, 1877, 1901, 1924, 1941, 1962, 1988, 2008, 2027, 2047, 2058, 2067, 2077, 2090, 2106, 2118, 2132, 2148, 2166, 2186, 2208, 2222, 2237, 2245, 2251, 2265, 2280, 2290, 2306, 2321, 2331, 2338, 2346, 2353, 2362, 2375, 2391, 2406, 2415, 2426, 2435, 2444, 2457, 2466, 2479, 2492, 2499, 2509, 2524, 2532}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
}
func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 {
if r < 0x00C0 || r > 0xFF61 {
return r
}
@@ -365,7 +365,7 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int
firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ {
for pidx := range pattern {
b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 {
@@ -445,7 +445,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
if slab != nil && N*M > cap(slab.I16) {
// Also, we should not allow a very long pattern to avoid 16-bit integer
// overflow in the score matrix. 1000 is a safe limit.
if slab != nil && N*M > cap(slab.I16) || M > 1000 {
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
}
@@ -501,7 +503,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if pidx < M {
F[pidx] = int32(off)
pidx++
pchar = pattern[util.Min(pidx, M-1)]
pchar = pattern[min(pidx, M-1)]
}
lastIdx = off
}
@@ -519,9 +521,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false
} else {
if inGap {
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
H0[off] = max(prevH0+scoreGapExtension, 0)
} else {
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
H0[off] = max(prevH0+scoreGapStart, 0)
}
C0[off] = 0
inGap = true
@@ -587,7 +589,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if b >= bonusBoundary && b > fb {
consecutive = 1
} else {
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
b = max(b, bonusConsecutive, fb)
}
}
if s1+b < s2 {
@@ -600,7 +602,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
Csub[off] = consecutive
inGap = s1 < s2
score := util.Max16(util.Max16(s1, s2), 0)
score := max(s1, s2, 0)
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, col
}
@@ -684,7 +686,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
if bonus >= bonusBoundary && bonus > firstBonus {
firstBonus = bonus
}
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
bonus = max(bonus, firstBonus, bonusConsecutive)
}
if pidx == 0 {
score += int(bonus * bonusFirstCharMultiplier)
@@ -726,7 +728,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
lenRunes := text.Length()
lenPattern := len(pattern)
for index := 0; index < lenRunes; index++ {
for index := range lenRunes {
char := text.Get(indexAt(index, lenRunes, forward))
// This is considerably faster than blindly applying strings.ToLower to the
// whole string
@@ -827,7 +829,7 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
// For simplicity, only look at the bonus at the first character position
pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
for index := 0; index < lenRunes; index++ {
index_ := indexAt(index, lenRunes, forward)
char := text.Get(index_)
@@ -849,7 +851,16 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
bonus = bonusAt(text, index_)
}
if boundaryCheck {
ok = bonus >= bonusBoundary
if forward && pidx_ == 0 {
bbonus = bonus
} else if !forward && pidx_ == lenPattern-1 {
if index_ < lenRunes-1 {
bbonus = bonusAt(text, index_+1)
} else {
bbonus = bonusBoundaryWhite
}
}
ok = bbonus >= bonusBoundary
if ok && pidx_ == 0 {
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
}

View File

@@ -86,7 +86,7 @@ func TestFuzzyMatch(t *testing.T) {
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
max(bonusCamel123, int(bonusBoundaryWhite)))
// Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,

View File

@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
'ử': 'u',
'ữ': 'u',
'ự': 'u',
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
0xFF01: '!', // Fullwidth exclamation
0xFF02: '"', // Fullwidth quotation mark
0xFF03: '#', // Fullwidth number sign
0xFF04: '$', // Fullwidth dollar sign
0xFF05: '%', // Fullwidth percent
0xFF06: '&', // Fullwidth ampersand
0xFF07: '\'', // Fullwidth apostrophe
0xFF08: '(', // Fullwidth left parenthesis
0xFF09: ')', // Fullwidth right parenthesis
0xFF0A: '*', // Fullwidth asterisk
0xFF0B: '+', // Fullwidth plus
0xFF0C: ',', // Fullwidth comma
0xFF0D: '-', // Fullwidth hyphen-minus
0xFF0E: '.', // Fullwidth period
0xFF0F: '/', // Fullwidth slash
0xFF10: '0',
0xFF11: '1',
0xFF12: '2',
0xFF13: '3',
0xFF14: '4',
0xFF15: '5',
0xFF16: '6',
0xFF17: '7',
0xFF18: '8',
0xFF19: '9',
0xFF1A: ':', // Fullwidth colon
0xFF1B: ';', // Fullwidth semicolon
0xFF1C: '<', // Fullwidth less-than
0xFF1D: '=', // Fullwidth equal
0xFF1E: '>', // Fullwidth greater-than
0xFF1F: '?', // Fullwidth question mark
0xFF20: '@', // Fullwidth at sign
0xFF21: 'A',
0xFF22: 'B',
0xFF23: 'C',
0xFF24: 'D',
0xFF25: 'E',
0xFF26: 'F',
0xFF27: 'G',
0xFF28: 'H',
0xFF29: 'I',
0xFF2A: 'J',
0xFF2B: 'K',
0xFF2C: 'L',
0xFF2D: 'M',
0xFF2E: 'N',
0xFF2F: 'O',
0xFF30: 'P',
0xFF31: 'Q',
0xFF32: 'R',
0xFF33: 'S',
0xFF34: 'T',
0xFF35: 'U',
0xFF36: 'V',
0xFF37: 'W',
0xFF38: 'X',
0xFF39: 'Y',
0xFF3A: 'Z',
0xFF3B: '[', // Fullwidth left bracket
0xFF3C: '\\', // Fullwidth backslash
0xFF3D: ']', // Fullwidth right bracket
0xFF3E: '^', // Fullwidth circumflex
0xFF3F: '_', // Fullwidth underscore
0xFF40: '`', // Fullwidth grave accent
0xFF41: 'a',
0xFF42: 'b',
0xFF43: 'c',
0xFF44: 'd',
0xFF45: 'e',
0xFF46: 'f',
0xFF47: 'g',
0xFF48: 'h',
0xFF49: 'i',
0xFF4A: 'j',
0xFF4B: 'k',
0xFF4C: 'l',
0xFF4D: 'm',
0xFF4E: 'n',
0xFF4F: 'o',
0xFF50: 'p',
0xFF51: 'q',
0xFF52: 'r',
0xFF53: 's',
0xFF54: 't',
0xFF55: 'u',
0xFF56: 'v',
0xFF57: 'w',
0xFF58: 'x',
0xFF59: 'y',
0xFF5A: 'z',
0xFF5B: '{', // Fullwidth left brace
0xFF5C: '|', // Fullwidth vertical bar
0xFF5D: '}', // Fullwidth right brace
0xFF5E: '~', // Fullwidth tilde
0xFF61: '.', // Halfwidth ideographic full stop
}
// NormalizeRunes normalizes latin script letters
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes))
copy(ret, runes)
for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 {
if r < 0x00C0 || r > 0xFF61 {
continue
}
n := normalized[r]

View File

@@ -22,20 +22,21 @@ type url struct {
type ansiState struct {
fg tui.Color
bg tui.Color
ul tui.Color
attr tui.Attr
lbg tui.Color
url *url
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
return s.fg != -1 || s.bg != -1 || s.ul != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
return s.fg == t.fg && s.bg == t.bg && s.ul == t.ul && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
}
func (s *ansiState) ToString() string {
@@ -54,7 +55,18 @@ func (s *ansiState) ToString() string {
ret += "3;"
}
if s.attr&tui.Underline > 0 {
ret += "4;"
switch s.attr.UnderlineStyle() {
case tui.UlStyleDouble:
ret += "4:2;"
case tui.UlStyleCurly:
ret += "4:3;"
case tui.UlStyleDotted:
ret += "4:4;"
case tui.UlStyleDashed:
ret += "4:5;"
default:
ret += "4;"
}
}
if s.attr&tui.Blink > 0 {
ret += "5;"
@@ -66,6 +78,9 @@ func (s *ansiState) ToString() string {
ret += "9;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
if s.ul != -1 {
ret += toAnsiStringUl(s.ul)
}
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
if s.url != nil {
@@ -74,6 +89,20 @@ func (s *ansiState) ToString() string {
return ret
}
func toAnsiStringUl(color tui.Color) string {
col := int(color)
if col < 0 {
return ""
}
if col >= (1 << 24) {
r := strconv.Itoa((col >> 16) & 0xff)
g := strconv.Itoa((col >> 8) & 0xff)
b := strconv.Itoa(col & 0xff)
return "58;2;" + r + ";" + g + ";" + b + ";"
}
return "58;5;" + strconv.Itoa(col) + ";"
}
func toAnsiString(color tui.Color, offset int) string {
col := int(color)
ret := ""
@@ -156,13 +185,13 @@ func isCtrlSeqStart(c uint8) bool {
// 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)"
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
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':
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
// 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.
@@ -174,6 +203,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
Loop:
for ; i < len(s); i++ {
switch s[i] {
case '\n':
// match: `\n`
return i, i + 1
case '\x08':
// backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' {
@@ -265,13 +297,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(prev)
}
newState := interpretCode(str[start:idx], state)
if !newState.equals(state) {
code := str[start:idx]
newState := interpretCode(code, state)
if code == "\n" || !newState.equals(state) {
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if code == "\n" {
output.WriteRune('\n')
runeCount++
// Full-background marker
if newState.lbg >= 0 {
marker := newState
marker.attr |= tui.FullBg
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
marker,
})
// Reset the full-line background color
newState.lbg = -1
}
}
if newState.colored() {
// Append new offset
if pstate == nil {
@@ -318,15 +367,19 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
return trimmed, nil, state
}
func parseAnsiCode(s string) (int, string) {
func parseAnsiCode(s string) (int, byte, string) {
var remaining string
var i int
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
var sep byte
// Find the first separator (either ; or :)
i := -1
for j := 0; j < len(s); j++ {
if s[j] == ';' || s[j] == ':' {
i = j
break
}
}
if i >= 0 {
sep = s[i]
remaining = s[i+1:]
s = s[:i]
}
@@ -338,25 +391,32 @@ func parseAnsiCode(s string) (int, string) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, remaining
return -1, sep, remaining
}
code = code*10 + int(ch)
}
return code, remaining
return code, sep, remaining
}
return -1, remaining
return -1, sep, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
if ansiCode == "\n" {
if prevState != nil {
return *prevState
}
return ansiState{-1, -1, -1, 0, -1, nil}
}
var state ansiState
if prevState == nil {
state = ansiState{-1, -1, 0, -1, nil}
state = ansiState{-1, -1, -1, 0, -1, nil}
} else {
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
state = ansiState{prevState.fg, prevState.bg, prevState.ul, prevState.attr, prevState.lbg, prevState.url}
}
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
state.lbg = prevState.bg
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
stLen := 2
@@ -375,10 +435,15 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
return state
}
if len(ansiCode) <= 3 {
reset := func() {
state.fg = -1
state.bg = -1
state.ul = -1
state.attr = 0
}
if len(ansiCode) <= 3 {
reset()
return state
}
ansiCode = ansiCode[2 : len(ansiCode)-1]
@@ -389,7 +454,8 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
count := 0
for len(ansiCode) != 0 {
var num int
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
var sep byte
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
count++
switch state256 {
case 0:
@@ -400,10 +466,15 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 48:
ptr = &state.bg
state256++
case 58:
ptr = &state.ul
state256++
case 39:
state.fg = -1
case 49:
state.bg = -1
case 59:
state.ul = -1
case 1:
state.attr = state.attr | tui.Bold
case 2:
@@ -411,7 +482,30 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 3:
state.attr = state.attr | tui.Italic
case 4:
state.attr = state.attr | tui.Underline
if sep == ':' {
// SGR 4:N — underline style sub-parameter
var subNum int
subNum, _, ansiCode = parseAnsiCode(ansiCode)
state.attr = state.attr &^ tui.UnderlineStyleMask
switch subNum {
case 0:
state.attr = state.attr &^ tui.Underline
case 1:
state.attr = state.attr | tui.Underline
case 2:
state.attr = state.attr | tui.Underline | tui.UlStyleDouble
case 3:
state.attr = state.attr | tui.Underline | tui.UlStyleCurly
case 4:
state.attr = state.attr | tui.Underline | tui.UlStyleDotted
case 5:
state.attr = state.attr | tui.Underline | tui.UlStyleDashed
default:
state.attr = state.attr | tui.Underline
}
} else {
state.attr = state.attr | tui.Underline
}
case 5:
state.attr = state.attr | tui.Blink
case 7:
@@ -425,6 +519,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.attr = state.attr &^ tui.Italic
case 24: // tput rmul
state.attr = state.attr &^ tui.Underline
state.attr = state.attr &^ tui.UnderlineStyleMask
case 25:
state.attr = state.attr &^ tui.Blink
case 27:
@@ -432,9 +527,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 29:
state.attr = state.attr &^ tui.StrikeThrough
case 0:
state.fg = -1
state.bg = -1
state.attr = 0
reset()
state256 = 0
default:
if num >= 30 && num <= 37 {
@@ -474,9 +567,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
// Empty sequence: reset
if count == 0 {
state.fg = -1
state.bg = -1
state.attr = 0
reset()
}
if state256 > 0 {

View File

@@ -22,7 +22,7 @@ import (
// (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)")
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
func testParserReference(t testing.TB, str string) {
t.Helper()
@@ -41,7 +41,7 @@ func testParserReference(t testing.TB, str string) {
equal := len(got) == len(exp)
if equal {
for i := 0; i < len(got); i++ {
for i := range got {
if got[i] != exp[i] {
equal = false
break
@@ -167,9 +167,9 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
randomString := func(rr *rand.Rand) string {
numChars := rand.Intn(50)
codePoints := make([]rune, numChars)
for i := 0; i < len(codePoints); i++ {
for i := range codePoints {
var r rune
for n := 0; n < 1000; n++ {
for range 1000 {
r = rune(rr.Intn(utf8.MaxRune))
// Allow 10% of runes to be invalid
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
@@ -182,7 +182,7 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
}
rr := rand.New(rand.NewSource(1))
for i := 0; i < 100_000; i++ {
for range 100_000 {
testParserReference(t, randomString(rr))
}
}
@@ -369,10 +369,10 @@ func TestAnsiCodeStringConversion(t *testing.T) {
}
}
assert("\x1b[m", nil, "")
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[m", &ansiState{attr: tui.Blink, ul: -1, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m")
@@ -380,36 +380,142 @@ func TestAnsiCodeStringConversion(t *testing.T) {
assert("\x1b[92m", nil, "\x1b[92;49m")
assert("\x1b[102m", nil, "\x1b[39;102m")
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[31m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, ul: -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[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;7m",
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1, ul: -1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
// Underline styles
assert("\x1b[4:3m", nil, "\x1b[4:3;39;49m")
assert("\x1b[4:2m", nil, "\x1b[4:2;39;49m")
assert("\x1b[4:4m", nil, "\x1b[4:4;39;49m")
assert("\x1b[4:5m", nil, "\x1b[4:5;39;49m")
assert("\x1b[4:1m", nil, "\x1b[4;39;49m")
// Underline color (256-color)
assert("\x1b[4;58;5;100m", nil, "\x1b[4;39;49;58;5;100m")
// Underline color (24-bit)
assert("\x1b[4;58;2;255;0;128m", nil, "\x1b[4;39;49;58;2;255;0;128m")
// Curly underline + underline color
assert("\x1b[4:3;58;2;255;0;0m", nil, "\x1b[4:3;39;49;58;2;255;0;0m")
// SGR 59 resets underline color
assert("\x1b[59m", &ansiState{fg: 1, bg: -1, ul: 100, lbg: -1}, "\x1b[31;49m")
}
func TestParseAnsiCode(t *testing.T) {
tests := []struct {
In, Exp string
N int
In string
Exp string
N int
Sep byte
}{
{"123", "", 123},
{"1a", "", -1},
{"1a;12", "12", -1},
{"12;a", "a", 12},
{"-2", "", -1},
{"123", "", 123, 0},
{"1a", "", -1, 0},
{"1a;12", "12", -1, ';'},
{"12;a", "a", 12, ';'},
{"-2", "", -1, 0},
// Colon sub-parameters: earliest separator wins (@shtse8)
{"4:3", "3", 4, ':'},
{"4:3;31", "3;31", 4, ':'},
{"38:2:255:0:0", "2:255:0:0", 38, ':'},
{"58:5:200", "5:200", 58, ':'},
// Semicolon before colon
{"4;38:2:0:0:0", "38:2:0:0:0", 4, ';'},
}
for _, x := range tests {
n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
n, sep, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp || sep != x.Sep {
t.Fatalf("%q: got: (%d %q %q) want: (%d %q %q)", x.In, n, s, string(sep), x.N, x.Exp, string(x.Sep))
}
}
}
// Test cases adapted from @shtse8 (PR #4678)
func TestInterpretCodeUnderlineStyles(t *testing.T) {
// 4:0 = no underline
state := interpretCode("\x1b[4:0m", nil)
if state.attr&tui.Underline != 0 {
t.Error("4:0 should not set underline")
}
// 4:1 = single underline
state = interpretCode("\x1b[4:1m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:1 should set underline")
}
// 4:3 = curly underline
state = interpretCode("\x1b[4:3m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:3 should set underline")
}
if state.attr.UnderlineStyle() != tui.UlStyleCurly {
t.Error("4:3 should set curly underline style")
}
// 4:3 should NOT set italic (3 is a sub-param, not SGR 3)
if state.attr&tui.Italic != 0 {
t.Error("4:3 should not set italic")
}
// 4:2;31 = double underline + red fg
state = interpretCode("\x1b[4:2;31m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:2;31 should set underline")
}
if state.fg != 1 {
t.Errorf("4:2;31 should set fg to red (1), got %d", state.fg)
}
if state.attr&tui.Dim != 0 {
t.Error("4:2;31 should not set dim")
}
// Plain 4 still works
state = interpretCode("\x1b[4m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4 should set underline")
}
// 4;2 (semicolon) = underline + dim
state = interpretCode("\x1b[4;2m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4;2 should set underline")
}
if state.attr&tui.Dim == 0 {
t.Error("4;2 should set dim")
}
}
// Test cases adapted from @shtse8 (PR #4678)
func TestInterpretCodeUnderlineColor(t *testing.T) {
// 58:2:R:G:B should not affect fg or bg
state := interpretCode("\x1b[58:2:255:0:0m", nil)
if state.fg != -1 || state.bg != -1 {
t.Errorf("58:2:R:G:B should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
}
// 58:5:200 should not affect fg or bg
state = interpretCode("\x1b[58:5:200m", nil)
if state.fg != -1 || state.bg != -1 {
t.Errorf("58:5:N should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
}
// 58:2:R:G:B combined with 38:2:R:G:B should only set fg
state = interpretCode("\x1b[58:2:255:0:0;38:2:0:255:0m", nil)
expectedFg := tui.Color(1<<24 | 0<<16 | 255<<8 | 0)
if state.fg != expectedFg {
t.Errorf("expected fg=%d, got %d", expectedFg, state.fg)
}
if state.bg != -1 {
t.Errorf("bg should be -1, got %d", state.bg)
}
}
// 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/" +

View File

@@ -41,6 +41,13 @@ func (c *Chunk) IsFull() bool {
return c.count == chunkSize
}
func (c *Chunk) lastIndex(minValue int32) int32 {
if c.count == 0 {
return minValue
}
return c.items[c.count-1].Index() + 1 // Exclusive
}
func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1]
}

View File

@@ -51,8 +51,8 @@ func TestChunkList(t *testing.T) {
}
// Add more data
for i := 0; i < chunkSize*2; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i)))
for i := range chunkSize * 2 {
cl.Push(fmt.Appendf(nil, "item %d", i))
}
// Previous snapshot should remain the same
@@ -85,8 +85,8 @@ func TestChunkListTail(t *testing.T) {
return true
})
total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i)))
for i := range total {
cl.Push(fmt.Appendf(nil, "item %d", i))
}
snapshot, count, changed := cl.Snapshot(0)

View File

@@ -29,6 +29,10 @@ const (
maxPatternLength = 1000
maxMulti = math.MaxInt32
// Background processes
maxBgProcesses = 30
maxBgProcessesPerAction = 3
// Matcher
numPartitionsMultiplier = 8
maxPartitions = 32

View File

@@ -2,10 +2,12 @@
package fzf
import (
"maps"
"os"
"sync"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -36,10 +38,22 @@ func (r revision) compatible(other revision) bool {
return r.major == other.major
}
func buildItemTransformer(opts *Options) func(*Item) string {
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
return func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
return func(item *Item) string {
return item.AsString(opts.Ansi)
}
}
// Run starts fzf
func Run(opts *Options) (int, error) {
if opts.Filter == nil {
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
if opts.useTmux() {
return runTmux(os.Args, opts)
}
@@ -74,20 +88,24 @@ func Run(opts *Options) (int, error) {
var lineAnsiState, prevLineAnsiState *ansiState
if opts.Ansi {
if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars(stringBytes(trimmed)), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(stringBytes(trimmed)), nil
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
// Full line background is found. Add a special marker.
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
marker := (*offsets)[len(*offsets)-1]
marker.offset[0] = marker.offset[1]
marker.color.bg = newState.lbg
marker.color.attr = marker.color.attr | tui.FullBg
newOffsets := append(*offsets, marker)
offsets = &newOffsets
// Reset the full-line background color
lineAnsiState.lbg = -1
}
return util.ToChars(stringBytes(trimmed)), offsets
}
}
@@ -112,7 +130,7 @@ func Run(opts *Options) (int, error) {
nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
if opts.Ansi && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
ansiStateDup := *prevLineAnsiState
@@ -135,7 +153,17 @@ func Run(opts *Options) (int, error) {
return false
}
item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
// We should not trim trailing whitespaces with background colors
var maxColorOffset int32
if item.colors != nil {
for _, ansi := range *item.colors {
if ansi.color.bg >= 0 {
maxColorOffset = max(maxColorOffset, ansi.offset[1])
}
}
}
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
item.text.Index = itemIndex
item.origText = &data
itemIndex++
@@ -210,10 +238,7 @@ func Run(opts *Options) (int, error) {
}
patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock()
denylistCopy := make(map[int32]struct{})
for k, v := range denylist {
denylistCopy[k] = v
}
denylistCopy := maps.Clone(denylist)
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
@@ -230,6 +255,8 @@ func Run(opts *Options) (int, error) {
pattern := patternBuilder([]rune(*opts.Filter))
matcher.sort = pattern.sortable
transformer := buildItemTransformer(opts)
found := false
if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size)
@@ -240,7 +267,7 @@ func Run(opts *Options) (int, error) {
if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
opts.Printer(transformer(&item))
found = true
}
mutex.Unlock()
@@ -254,11 +281,11 @@ func Run(opts *Options) (int, error) {
// NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{
result := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
for i := 0; i < result.merger.Length(); i++ {
opts.Printer(transformer(result.merger.Get(i).item))
found = true
}
}
@@ -295,6 +322,7 @@ func Run(opts *Options) (int, error) {
// Event coordination
reading := true
ticks := 0
startTick := 0
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
@@ -305,7 +333,7 @@ func Run(opts *Options) (int, error) {
if total >= maxFit || final {
deferred = false
heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
terminal.startChan <- fitpad{min(total, maxFit), padHeight}
}
} else if deferred {
deferred = false
@@ -321,6 +349,7 @@ func Run(opts *Options) (int, error) {
clearDenylist()
}
reading = true
startTick = ticks
chunkList.Clear()
itemIndex = 0
inputRevision.bumpMajor()
@@ -464,12 +493,13 @@ func Run(opts *Options) (int, error) {
case EvtSearchFin:
switch val := value.(type) {
case *Merger:
case MatchResult:
merger := val.merger
if deferred {
count := val.Length()
count := merger.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
determine(val.final)
} else if val.final {
determine(merger.final)
} else if merger.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery {
opts.Printer(opts.Query)
@@ -477,17 +507,9 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 {
opts.Printer("")
}
transformer := func(item *Item) string {
return item.AsString(opts.Ansi)
}
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
transformer = func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
for i := 0; i < count; i++ {
opts.Printer(transformer(val.Get(i).item))
transformer := buildItemTransformer(opts)
for i := range count {
opts.Printer(transformer(merger.Get(i).item))
}
if count == 0 {
exitCode = ExitNoMatch
@@ -495,7 +517,7 @@ func Run(opts *Options) (int, error) {
stop = true
return
}
determine(val.final)
determine(merger.final)
}
}
terminal.UpdateList(val)
@@ -508,8 +530,8 @@ func Run(opts *Options) (int, error) {
break
}
if delay && reading {
dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep,
dur := util.Constrain(
time.Duration(ticks-startTick)*coordinatorDelayStep,
0, coordinatorDelayMax)
time.Sleep(dur)
}

View File

@@ -38,7 +38,7 @@ func TestHistory(t *testing.T) {
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
for i := 0; i < maxHistory; i++ {
for i := range maxHistory {
if h.lines[i] != "foobar" {
t.Error("Expected: foobar, actual: " + h.lines[i])
}

View File

@@ -19,6 +19,20 @@ type MatchRequest struct {
revision revision
}
type MatchResult struct {
merger *Merger
passMerger *Merger
cancelled bool
}
func (mr MatchResult) cacheable() bool {
return mr.merger != nil && mr.merger.cacheable()
}
func (mr MatchResult) final() bool {
return mr.merger != nil && mr.merger.final
}
// Matcher is responsible for performing search
type Matcher struct {
cache *ChunkCache
@@ -29,7 +43,7 @@ type Matcher struct {
reqBox *util.EventBox
partitions int
slab []*util.Slab
mergerCache map[string]*Merger
mergerCache map[string]MatchResult
revision revision
}
@@ -41,7 +55,7 @@ const (
// NewMatcher returns a new Matcher
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
@@ -51,7 +65,7 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(),
partitions: partitions,
slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger),
mergerCache: make(map[string]MatchResult),
revision: revision}
}
@@ -85,43 +99,42 @@ func (m *Matcher) Loop() {
cacheCleared := false
if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger)
m.mergerCache = make(map[string]MatchResult)
if !request.revision.compatible(m.revision) {
m.cache.Clear()
}
m.revision = request.revision
cacheCleared = true
}
// Restart search
patternString := request.pattern.AsString()
var merger *Merger
cancelled := false
var result MatchResult
count := CountItems(request.chunks)
if !cacheCleared {
if count == prevCount {
// Look up mergerCache
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
merger = cached
if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
result = cached
}
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
m.mergerCache = make(map[string]MatchResult)
}
}
if merger == nil {
merger, cancelled = m.scan(request)
if result.merger == nil {
result = m.scan(request)
}
if !cancelled {
if merger.cacheable() {
m.mergerCache[patternString] = merger
if !result.cancelled {
if result.cacheable() {
m.mergerCache[patternString] = result
}
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger)
result.merger.final = request.final
m.eventBox.Set(EvtSearchFin, result)
}
}
}
@@ -152,19 +165,22 @@ type partialResult struct {
matches []Result
}
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
func (m *Matcher) scan(request MatchRequest) MatchResult {
startedAt := time.Now()
numChunks := len(request.chunks)
if numChunks == 0 {
return EmptyMerger(request.revision), false
m := EmptyMerger(request.revision)
return MatchResult{m, m, false}
}
pattern := request.pattern
passMerger := PassMerger(&request.chunks, m.tac, request.revision)
if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac, request.revision), false
return MatchResult{passMerger, passMerger, false}
}
minIndex := request.chunks[0].items[0].Index()
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks)
@@ -223,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
if m.reqBox.Peek(reqReset) {
return nil, wait()
return MatchResult{nil, nil, wait()}
}
if time.Since(startedAt) > progressMinDuration {
@@ -236,7 +252,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
return MatchResult{merger, passMerger, false}
}
// Reset is called to interrupt/signal the ongoing search

View File

@@ -4,7 +4,7 @@ import "fmt"
// EmptyMerger is a Merger with no data
func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
}
// Merger holds a set of locally sorted lists of items and provides the view of
@@ -22,14 +22,16 @@ type Merger struct {
pass bool
revision revision
minIndex int32
maxIndex int32
}
// PassMerger returns a new Merger that simply returns the items in the
// original order
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32
var minIndex, maxIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
}
mg := Merger{
pattern: nil,
@@ -38,7 +40,8 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
count: 0,
pass: true,
revision: revision,
minIndex: minIndex}
minIndex: minIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks {
mg.count += chunk.count
@@ -47,7 +50,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
}
// NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
mg := Merger{
pattern: pattern,
lists: lists,
@@ -59,7 +62,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
final: false,
count: 0,
revision: revision,
minIndex: minIndex}
minIndex: minIndex,
maxIndex: maxIndex}
for _, list := range mg.lists {
mg.count += len(list)
@@ -137,6 +141,15 @@ func (mg *Merger) Get(idx int) Result {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
}
func (mg *Merger) ToMap() map[int32]Result {
ret := make(map[int32]Result, mg.count)
for i := 0; i < mg.count; i++ {
result := mg.Get(i)
ret[result.Index()] = result
}
return ret
}
func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax
}

View File

@@ -34,11 +34,11 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4
lists := make([][]Result, numLists)
cnt := 0
for i := 0; i < numLists; i++ {
for i := range numLists {
numResults := rand.Int() % 20
cnt += numResults
lists[i] = make([]Result, numResults)
for j := 0; j < numResults; j++ {
for j := range numResults {
item := randResult()
lists[i][j] = item
}
@@ -58,9 +58,9 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items)
// Not sorted: same order
mg := NewMerger(nil, lists, false, false, revision{}, 0)
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ {
for i := range cnt {
assert(t, items[i] == mg.Get(i), "Invalid Get")
}
}
@@ -70,17 +70,17 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items)
// Sorted sorted order
mg := NewMerger(nil, lists, true, false, revision{}, 0)
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ {
for i := range cnt {
if items[i] != mg.Get(i) {
t.Error("Not sorted", items[i], mg.Get(i))
}
}
// Inverse order
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i))

File diff suppressed because it is too large Load Diff

View File

@@ -142,7 +142,7 @@ func TestIrrelevantNth(t *testing.T) {
}
func TestParseKeys(t *testing.T) {
pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) {
if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s)
@@ -168,7 +168,7 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
@@ -182,7 +182,7 @@ func TestParseKeys(t *testing.T) {
check(tui.Left, "left")
check(tui.Right, "right")
pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 {
t.Error(11)
}
@@ -211,40 +211,40 @@ func TestParseKeysWithComma(t *testing.T) {
}
}
pairs, _ := parseKeyChords(",", "")
pairs, _, _ := parseKeyChords(",", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords(",,a,b", "")
pairs, _, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords("a,b,,", "")
pairs, _, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords("a,,,b", "")
pairs, _, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords("a,,,b,c", "")
pairs, _, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords(",,,", "")
pairs, _, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs, _ = parseKeyChords(",ALT-,,", "")
pairs, _, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,")
}
@@ -300,8 +300,12 @@ func TestBind(t *testing.T) {
}
func TestColorSpec(t *testing.T) {
var base *tui.ColorTheme
theme := tui.Dark256
dark, _ := parseTheme(theme, "dark")
base, dark, _ := parseTheme(theme, "dark")
if *dark != *base {
t.Errorf("incorrect base theme returned")
}
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
@@ -309,7 +313,10 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
light, _ := parseTheme(theme, "dark,light")
base, light, _ := parseTheme(theme, "dark,light")
if *light != *base {
t.Errorf("incorrect base theme returned")
}
if *light == *theme {
t.Errorf("should not be equivalent")
}
@@ -320,7 +327,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
customized, _ := parseTheme(theme, "fg:231,bg:232")
_, customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized")
}
@@ -333,7 +340,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
}
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
_, customized, _ = parseTheme(theme, "fg:231,dark bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized")
}
@@ -350,8 +357,8 @@ func TestDefaultCtrlNP(t *testing.T) {
t.Error()
}
}
check([]string{}, tui.CtrlN, actDown)
check([]string{}, tui.CtrlP, actUp)
check([]string{}, tui.CtrlN, actDownMatch)
check([]string{}, tui.CtrlP, actUpMatch)
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
@@ -441,6 +448,64 @@ func TestPreviewOpts(t *testing.T) {
opts.Preview.size.size == 70) {
t.Error(opts.Preview)
}
// wrap-word tests
opts = optsFor("--preview-window=wrap-word")
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
opts = optsFor("--preview-window=wrap-word,nowrap")
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
opts = optsFor("--preview-window=wrap-word,wrap")
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
}
func TestPreviewWrapSign(t *testing.T) {
// Default: no preview wrap sign override
opts := optsFor()
if opts.PreviewWrapSign != nil {
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
}
// --preview-wrap-sign sets PreviewWrapSign
opts = optsFor("--preview-wrap-sign", ">> ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign is independent of --wrap-sign
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
if opts.WrapSign == nil || *opts.WrapSign != "| " {
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign without --wrap-sign
opts = optsFor("--preview-wrap-sign", "→ ")
if opts.WrapSign != nil {
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
}
// Last --preview-wrap-sign wins
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
}
// Empty string is allowed
opts = optsFor("--preview-wrap-sign", "")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
}
}
func TestAdditiveExpect(t *testing.T) {
@@ -462,7 +527,7 @@ func TestValidateSign(t *testing.T) {
}
for _, testCase := range testCases {
err := validateSign(testCase.inputSign, "")
err := validateSign(testCase.inputSign, "", 2)
if testCase.isValid && err != nil {
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
}

View File

@@ -90,11 +90,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
}
}
// To ensure that the options are processed by a POSIX-compliant shell,
// we need to write the command to a temporary file and execute it with sh.
// Write the command to a temporary file and run it with sh to ensure POSIX compliance.
var exports []string
needBash := false
if withExports {
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
for _, pairStr := range os.Environ() {
pair := strings.SplitN(pairStr, "=", 2)
@@ -144,7 +145,7 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
env = elems[1:]
}
executor := util.NewExecutor(opts.WithShell)
ttyin, err := tui.TtyIn()
ttyin, err := tui.TtyIn(opts.TtyDefault)
if err != nil {
return ExitError, err
}

View File

@@ -7,6 +7,7 @@ import (
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -177,8 +178,8 @@ func (r *Reader) feed(src io.Reader) {
var err error
for {
n := 0
scope := slab[:util.Min(len(slab), readerBufferSize)]
for i := 0; i < 100; i++ {
scope := slab[:min(len(slab), readerBufferSize)]
for range 100 {
n, err = src.Read(scope)
if n > 0 || err != nil {
break
@@ -285,7 +286,7 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
if strings.HasPrefix(ignore, sep) {
ignoresSuffix = append(ignoresSuffix, ignore)
} else {
// 'foo/bar' should match match
// 'foo/bar' should match
// * 'foo/bar'
// * 'baz/foo/bar'
// * but NOT 'bazfoo/bar'
@@ -302,28 +303,30 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
}
path = trimPath(path)
if path != "." {
isDir := de.IsDir()
if isDir || opts.follow && isSymlinkToDir(path, de) {
isDirSymlink := isSymlinkToDir(path, de)
if isDirSymlink && !opts.follow {
return filepath.SkipDir
}
isDir := de.IsDir() || isDirSymlink
if isDir {
base := filepath.Base(path)
if !opts.hidden && base[0] == '.' && base != ".." {
return filepath.SkipDir
}
for _, ignore := range ignoresBase {
if ignore == base {
return filepath.SkipDir
}
if slices.Contains(ignoresBase, base) {
return filepath.SkipDir
}
for _, ignore := range ignoresFull {
if ignore == path {
return filepath.SkipDir
}
if slices.Contains(ignoresFull, path) {
return filepath.SkipDir
}
for _, ignore := range ignoresSuffix {
if strings.HasSuffix(path, ignore) {
return filepath.SkipDir
}
}
path += sep
if path != sep {
path += sep
}
}
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))

View File

@@ -19,6 +19,10 @@ type colorOffset struct {
url *url
}
func (co colorOffset) IsFullBgMarker(at int32) bool {
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
}
type Result struct {
item *Item
points [4]uint16
@@ -38,9 +42,9 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
for _, offset := range offsets {
b, e := int(offset[0]), int(offset[1])
if b < e {
minBegin = util.Min(b, minBegin)
minEnd = util.Min(e, minEnd)
maxEnd = util.Max(e, maxEnd)
minBegin = min(b, minBegin)
minEnd = min(e, minEnd)
maxEnd = max(e, maxEnd)
validOffsetFound = true
}
}
@@ -87,7 +91,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
for idx := range numChars {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || !unicode.IsSpace(r) {
@@ -119,14 +123,14 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset {
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
itemColors := result.item.Colors()
// No ANSI codes
if len(itemColors) == 0 && len(nthOffsets) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
offsets := make([]colorOffset, len(matchOffsets))
for i, off := range matchOffsets {
offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
}
return offsets
}
@@ -149,12 +153,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color bool
match bool
nth bool
fbg tui.Color
}
cols := make([]cellInfo, maxCol)
cols := make([]cellInfo, maxCol+1)
for idx := range cols {
cols[idx].fbg = -1
}
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = cellInfo{colorIndex, true, false, false}
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
cols[ansi.offset[0]].fbg = ansi.color.lbg
} else {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
}
}
}
@@ -176,29 +188,35 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
var curr cellInfo = cellInfo{0, false, false, false}
curr := cellInfo{0, false, false, false, -1}
start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
if !theme.Colored {
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
}
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
if base.ShouldStripColors() {
return base
}
fg := ansi.color.fg
bg := ansi.color.bg
if fg == -1 {
if current {
fg = theme.Current.Color
} else {
fg = theme.Fg.Color
}
fg = colBase.Fg()
}
if bg == -1 {
if current {
bg = theme.DarkBg.Color
} else {
bg = theme.Bg.Color
}
bg = colBase.Bg()
}
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
}
var colors []colorOffset
add := func(idx int) {
if curr.fbg >= 0 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(start)},
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
match: false,
url: nil})
}
if (curr.color || curr.nth || curr.match) && idx > start {
if curr.match {
var color tui.ColorPair
@@ -208,7 +226,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color = colBase.Merge(colMatch)
}
var url *url
if curr.color && theme.Colored {
if curr.color {
ansi := itemColors[curr.index]
url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch)
@@ -223,7 +241,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
if color.Fg().IsDefault() && origColor.HasBg() {
color = origColor
if curr.nth {
color = color.WithAttr(attrNth)
color = color.WithAttr(attrNth &^ tui.AttrRegular)
}
} else {
color = origColor.MergeNonDefault(color)
@@ -233,19 +251,27 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else if curr.color {
ansi := itemColors[curr.index]
color := ansiToColorPair(ansi, colBase)
base := colBase
if curr.nth {
color = color.WithAttr(attrNth)
base = base.WithAttr(attrNth)
}
if hidden {
base = base.WithFg(theme.Nomatch)
}
color := ansiToColorPair(ansi, base)
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: color,
match: false,
url: ansi.color.url})
} else {
color := colBase.WithAttr(attrNth)
if hidden {
color = color.WithFg(theme.Nomatch)
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: colBase.WithAttr(attrNth),
color: color,
match: false,
url: nil})
}

View File

@@ -124,14 +124,14 @@ func TestColorOffset(t *testing.T) {
item := Result{
item: &Item{
colors: &[]ansiOffset{
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true)
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c {
@@ -158,7 +158,7 @@ func TestColorOffset(t *testing.T) {
nthOffsets := []Offset{{37, 39}, {42, 45}}
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true)
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
@@ -176,7 +176,7 @@ func TestColorOffset(t *testing.T) {
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
expected := tui.Bold | attr
if attr == tui.AttrRegular {
expected = tui.AttrRegular
expected = tui.Bold
}
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))

View File

@@ -46,15 +46,20 @@ type httpServer struct {
type listenAddress struct {
host string
port int
sock string
}
func (addr listenAddress) IsLocal() bool {
return addr.host == "localhost" || addr.host == "127.0.0.1"
return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
}
var defaultListenAddr = listenAddress{"localhost", 0}
var defaultListenAddr = listenAddress{"localhost", 0, ""}
func parseListenAddress(address string) (listenAddress, error) {
if strings.HasSuffix(address, ".sock") {
return listenAddress{"", 0, address}, nil
}
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
@@ -70,7 +75,7 @@ func parseListenAddress(address string) (listenAddress, error) {
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
return listenAddress{parts[0], port}, nil
return listenAddress{parts[0], port, ""}, nil
}
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
@@ -80,21 +85,40 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
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 listener net.Listener
var err error
if len(address.sock) > 0 {
if _, err := os.Stat(address.sock); err == nil {
// Check if the socket is already in use
if conn, err := net.Dial("unix", address.sock); err == nil {
conn.Close()
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
}
os.Remove(address.sock)
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
listener, err = net.Listen("unix", address.sock)
if err != nil {
return nil, port, err
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
}
os.Chmod(address.sock, 0600)
} else {
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
}
}
}
@@ -159,23 +183,22 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
})
section := 0
var getMatch []string
Loop:
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0:
getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
response := server.getHandler(parseGetParams(getMatch[1]))
if len(response) > 0 {
return good(response)
}
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
} else if !strings.HasPrefix(text, "POST / HTTP") {
case 0: // Request line
getMatch = getRegex.FindStringSubmatch(text)
if len(getMatch) == 0 && !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1:
if text == crlf {
case 1: // Request headers
if text == crlf { // End of headers
if len(getMatch) > 0 {
break Loop
}
if contentLength == 0 {
return bad("content-length header missing")
}
@@ -195,7 +218,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
apiKey = strings.TrimSpace(pair[1])
}
}
case 2:
case 2: // Request body
body += text
}
}
@@ -204,6 +227,14 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
return unauthorized("invalid api key")
}
if len(getMatch) > 0 {
response := server.getHandler(parseGetParams(getMatch[1]))
if len(response) > 0 {
return good(response)
}
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
if len(body) < contentLength {
return bad("incomplete request")
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ import (
"github.com/junegunn/fzf/src/util"
)
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
@@ -30,11 +30,11 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
items1 := [3][]*Item{{item1}, {item1}, nil}
items2 := [3][]*Item{
{newItem("foo'bar \x1b[31mbaz\x1b[m")},
{newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
delim := "'"
var regex *regexp.Regexp
@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {r}, strip ansi
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {r..}, strip ansi
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {}, with multiple items
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
@@ -137,11 +145,11 @@ func TestReplacePlaceholder(t *testing.T) {
checkFormat("echo {{.O}} {{.O}}")
// No match
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter
@@ -158,17 +166,18 @@ func TestReplacePlaceholder(t *testing.T) {
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
see: TestParsePlaceholder
*/
items3 := []*Item{
items3 := [3][]*Item{
// single line
newItem("1a 1b 1c 1d 1e 1f"),
{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"),
{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")},
nil,
}
stripAnsi := false
forcePlus := false
@@ -549,14 +558,14 @@ func newItem(str string) *Item {
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))
// Functions tested in this file require array of items (allItems).
// This is helper function.
func newItems(str ...string) [3][]*Item {
result := make([]*Item, len(str))
for i, s := range str {
result[i] = newItem(s)
}
return result
return [3][]*Item{result, nil, nil}
}
// (for logging purposes)
@@ -565,7 +574,7 @@ func (item *Item) String() string {
}
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string {
func templateToString(format string, data any) string {
bb := &bytes.Buffer{}
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
@@ -580,7 +589,7 @@ func templateToString(format string, data interface{}) string {
type give struct {
template string
query string
allItems []*Item
allItems [3][]*Item
}
type want struct {
/*
@@ -618,25 +627,25 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases
for idx, test := range tests {
gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query,
test.give.allItems)
test.template, stripAnsi, delimiter, printsep, forcePlus,
test.query,
test.allItems)
switch {
case test.want.output != "":
if gotOutput != test.want.output {
case test.output != "":
if gotOutput != test.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)
test.template, test.query, test.allItems,
gotOutput, test.output)
}
case test.want.match != "":
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
case test.match != "":
wantMatch := strings.ReplaceAll(test.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)
test.template, test.query, test.allItems,
gotOutput, test.match)
}
default:
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
@@ -690,3 +699,72 @@ func readFile(path string) ([]byte, error) {
}
}
}
func TestWordWrapAnsiLine(t *testing.T) {
term := &Terminal{}
// Simple wrapping
result := term.wordWrapAnsiLine("hello world", 7, 2)
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
t.Errorf("Simple: %q", result)
}
// No wrapping needed
result = term.wordWrapAnsiLine("hello", 10, 2)
if len(result) != 1 || result[0] != "hello" {
t.Errorf("No wrap: %q", result)
}
// ANSI codes preserved across split
result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
t.Errorf("ANSI: %q", result)
}
// Long word (no space) — no break, let character wrapping handle it
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
if len(result) != 1 || result[0] != "abcdefghij" {
t.Errorf("Long word: %q", result)
}
// Multiple words with continuation wrapSignWidth
result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
// max=5 for first line, max=3 for continuations (5-2)
// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
t.Errorf("Multiple words: %q", result)
}
// Empty string
result = term.wordWrapAnsiLine("", 10, 2)
if len(result) != 1 || result[0] != "" {
t.Errorf("Empty: %q", result)
}
// OSC 8 hyperlink preserved
result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
if len(result) != 2 {
t.Errorf("Hyperlink split count: %d, %q", len(result), result)
}
// Tab handling: tab expands to tabstop-aligned width
term.tabstop = 8
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
result = term.wordWrapAnsiLine("\thi there", 15, 2)
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
t.Errorf("Tab: %q", result)
}
// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
// maxWidth=13: fits without wrapping
result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
if len(result) != 1 || result[0] != "hello\tworld" {
t.Errorf("Tab no wrap: %q", result)
}
// maxWidth=12: 13 > 12, wraps at tab
result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
t.Errorf("Tab wrap: %q", result)
}
}

View File

@@ -11,10 +11,14 @@ func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"}
if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined {
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
// We append --border option at the end, because `--style=full:STYLE`
// may have changed the default border style.
rest = append(rest, "--border")
if tui.DefaultBorderShape == tui.BorderRounded {
rest = append(rest, "--border=rounded")
} else {
rest = append(rest, "--border=sharp")
}
}
if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1")

View File

@@ -206,8 +206,9 @@ func Tokenize(text string, delimiter Delimiter) []Token {
if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(text, -1)
begin := 0
for _, loc := range locs {
tokens = append(tokens, text[begin:loc[1]])
tokens = make([]string, len(locs))
for i, loc := range locs {
tokens[i] = text[begin:loc[1]]
begin = loc[1]
}
if begin < len(text) {
@@ -233,6 +234,23 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
return strings.TrimRightFunc(str, unicode.IsSpace)
}
func GetLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil {
if strings.HasSuffix(str, *delimiter.str) {
return *delimiter.str
}
} else if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 {
lastLoc := locs[len(locs)-1]
if lastLoc[1] == len(str) {
return str[lastLoc[0]:]
}
}
}
return ""
}
// JoinTokens concatenates the tokens into a single string
func JoinTokens(tokens []Token) string {
var output bytes.Buffer
@@ -284,7 +302,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
end += numTokens + 1
}
}
minIdx = util.Max(0, begin-1)
minIdx = max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
parts = append(parts, tokens[idx-1].text)

View File

@@ -2,29 +2,7 @@
package tui
type Attr int32
func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
return a | b
}
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
Bold = Attr(1)
Dim = Attr(1 << 1)
Italic = Attr(1 << 2)
@@ -35,6 +13,12 @@ const (
StrikeThrough = Attr(1 << 7)
)
func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape = BorderRounded
func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
@@ -50,11 +34,11 @@ func (r *FullscreenRenderer) ShowCursor() {}
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) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} }
func (r *FullscreenRenderer) CancelGetChar() {}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}

View File

@@ -37,82 +37,137 @@ func _() {
_ = 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]
_ = x[ClickHeader-99]
_ = x[CtrlBackSlash-29]
_ = x[CtrlRightBracket-30]
_ = x[CtrlCaret-31]
_ = x[CtrlSlash-32]
_ = x[ShiftTab-33]
_ = x[Backspace-34]
_ = x[Delete-35]
_ = x[PageUp-36]
_ = x[PageDown-37]
_ = x[Up-38]
_ = x[Down-39]
_ = x[Left-40]
_ = x[Right-41]
_ = x[Home-42]
_ = x[End-43]
_ = x[Insert-44]
_ = x[ShiftUp-45]
_ = x[ShiftDown-46]
_ = x[ShiftLeft-47]
_ = x[ShiftRight-48]
_ = x[ShiftDelete-49]
_ = x[ShiftHome-50]
_ = x[ShiftEnd-51]
_ = x[ShiftPageUp-52]
_ = x[ShiftPageDown-53]
_ = x[F1-54]
_ = x[F2-55]
_ = x[F3-56]
_ = x[F4-57]
_ = x[F5-58]
_ = x[F6-59]
_ = x[F7-60]
_ = x[F8-61]
_ = x[F9-62]
_ = x[F10-63]
_ = x[F11-64]
_ = x[F12-65]
_ = x[AltBackspace-66]
_ = x[AltUp-67]
_ = x[AltDown-68]
_ = x[AltLeft-69]
_ = x[AltRight-70]
_ = x[AltDelete-71]
_ = x[AltHome-72]
_ = x[AltEnd-73]
_ = x[AltPageUp-74]
_ = x[AltPageDown-75]
_ = x[AltShiftUp-76]
_ = x[AltShiftDown-77]
_ = x[AltShiftLeft-78]
_ = x[AltShiftRight-79]
_ = x[AltShiftDelete-80]
_ = x[AltShiftHome-81]
_ = x[AltShiftEnd-82]
_ = x[AltShiftPageUp-83]
_ = x[AltShiftPageDown-84]
_ = x[CtrlUp-85]
_ = x[CtrlDown-86]
_ = x[CtrlLeft-87]
_ = x[CtrlRight-88]
_ = x[CtrlHome-89]
_ = x[CtrlEnd-90]
_ = x[CtrlBackspace-91]
_ = x[CtrlDelete-92]
_ = x[CtrlPageUp-93]
_ = x[CtrlPageDown-94]
_ = x[Alt-95]
_ = x[CtrlAlt-96]
_ = x[CtrlAltUp-97]
_ = x[CtrlAltDown-98]
_ = x[CtrlAltLeft-99]
_ = x[CtrlAltRight-100]
_ = x[CtrlAltHome-101]
_ = x[CtrlAltEnd-102]
_ = x[CtrlAltBackspace-103]
_ = x[CtrlAltDelete-104]
_ = x[CtrlAltPageUp-105]
_ = x[CtrlAltPageDown-106]
_ = x[CtrlShiftUp-107]
_ = x[CtrlShiftDown-108]
_ = x[CtrlShiftLeft-109]
_ = x[CtrlShiftRight-110]
_ = x[CtrlShiftHome-111]
_ = x[CtrlShiftEnd-112]
_ = x[CtrlShiftDelete-113]
_ = x[CtrlShiftPageUp-114]
_ = x[CtrlShiftPageDown-115]
_ = x[CtrlAltShiftUp-116]
_ = x[CtrlAltShiftDown-117]
_ = x[CtrlAltShiftLeft-118]
_ = x[CtrlAltShiftRight-119]
_ = x[CtrlAltShiftHome-120]
_ = x[CtrlAltShiftEnd-121]
_ = x[CtrlAltShiftDelete-122]
_ = x[CtrlAltShiftPageUp-123]
_ = x[CtrlAltShiftPageDown-124]
_ = x[Invalid-125]
_ = x[Fatal-126]
_ = x[BracketedPasteBegin-127]
_ = x[BracketedPasteEnd-128]
_ = x[Mouse-129]
_ = x[DoubleClick-130]
_ = x[LeftClick-131]
_ = x[RightClick-132]
_ = x[SLeftClick-133]
_ = x[SRightClick-134]
_ = x[ScrollUp-135]
_ = x[ScrollDown-136]
_ = x[SScrollUp-137]
_ = x[SScrollDown-138]
_ = x[PreviewScrollUp-139]
_ = x[PreviewScrollDown-140]
_ = x[Resize-141]
_ = x[Change-142]
_ = x[BackwardEOF-143]
_ = x[Start-144]
_ = x[Load-145]
_ = x[Focus-146]
_ = x[One-147]
_ = x[Zero-148]
_ = x[Result-149]
_ = x[Jump-150]
_ = x[JumpCancel-151]
_ = x[ClickHeader-152]
_ = x[ClickFooter-153]
_ = x[Multi-154]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
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, 648}
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, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -13,7 +13,6 @@ import (
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
)
@@ -26,9 +25,10 @@ const (
escPollInterval = 5
offsetPollTries = 10
maxInputBuffer = 1024 * 1024
maxSelectTries = 100
)
const consoleDevice string = "/dev/tty"
const DefaultTtyDevice string = "/dev/tty"
var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
@@ -49,6 +49,18 @@ const DIM string = "\x1b[2m"
const CR string = DIM + "␍"
const LF string = DIM + "␊"
type getCharResult int
const (
getCharSuccess getCharResult = iota
getCharError
getCharCancelled
)
func (r getCharResult) ok() bool {
return r == getCharSuccess
}
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str)
runes := []rune{}
@@ -104,6 +116,7 @@ type LightRenderer struct {
clicks [][2]int
ttyin *os.File
ttyout *os.File
cancel func()
buffer []byte
origState *term.State
width int
@@ -118,9 +131,9 @@ type LightRenderer struct {
x int
maxHeightFunc func(int) int
showCursor bool
mutex sync.Mutex
// Windows only
mutex sync.Mutex
ttyinChannel chan byte
inHandle uintptr
outHandle uintptr
@@ -146,8 +159,8 @@ type LightWindow struct {
wrapSignWidth int
}
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
out, err := openTtyOut()
func NewLightRenderer(ttyDefault string, ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
out, err := openTtyOut(ttyDefault)
if err != nil {
out = os.Stderr
}
@@ -193,11 +206,6 @@ func (r *LightRenderer) Init() error {
if r.fullscreen {
r.smcup()
} else {
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
y, x := r.findOffset()
r.mouse = r.mouse && y >= 0
// When --no-clear is used for repetitive relaunching, there is a small
@@ -208,12 +216,17 @@ func (r *LightRenderer) Init() error {
r.upOneLine = true
r.makeSpace()
}
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
for i := 1; i < r.MaxY(); i++ {
r.makeSpace()
}
}
r.enableMouse()
r.enableModes()
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
@@ -262,16 +275,18 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
func (r *LightRenderer) getBytes() ([]byte, error) {
bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
return r.getBytesInternal(cancellable, r.buffer, false)
}
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) {
c, result := r.getch(cancellable, nonblock)
if result == getCharCancelled {
return buffer, getCharCancelled, nil
}
if !nonblock && !result.ok() {
r.Close()
return nil, errors.New("failed to read " + consoleDevice)
return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice)
}
retries := 0
@@ -282,8 +297,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
pc := c
for {
c, ok = r.getch(true)
if !ok {
c, result = r.getch(false, true)
if !result.ok() {
if retries > 0 {
retries--
time.Sleep(escPollInterval * time.Millisecond)
@@ -302,20 +317,24 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
return nil, getCharError, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
return buffer, nil
return buffer, getCharSuccess, nil
}
func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) GetChar(cancellable bool) Event {
var err error
var result getCharResult
if len(r.buffer) == 0 {
r.buffer, err = r.getBytes()
r.buffer, result, err = r.getBytes(cancellable)
if err != nil {
return Event{Fatal, 0, nil}
}
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
}
if len(r.buffer) == 0 {
return Event{Fatal, 0, nil}
@@ -335,6 +354,8 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlQ, 0, nil}
case 127:
return Event{Backspace, 0, nil}
case 8:
return Event{CtrlBackspace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case 28:
@@ -349,9 +370,14 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
if r.buffer, err = r.getBytes(); err != nil {
r.buffer, result, err = r.getBytes(true)
if err != nil {
return Event{Fatal, 0, nil}
}
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
ev = r.escSequence(&sz)
}
return ev
@@ -369,6 +395,21 @@ func (r *LightRenderer) GetChar() Event {
return Event{Rune, char, nil}
}
func (r *LightRenderer) CancelGetChar() {
r.mutex.Lock()
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
r.mutex.Unlock()
}
func (r *LightRenderer) setCancel(f func()) {
r.mutex.Lock()
r.cancel = f
r.mutex.Unlock()
}
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{Esc, 0, nil}
@@ -381,6 +422,9 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
*sz = 2
if r.buffer[1] == 8 {
return Event{CtrlAltBackspace, 0, nil}
}
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
}
@@ -462,32 +506,150 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
// Bracketed paste mode: \e[200~ ... \e[201~
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
// Immediately discard the sequence from the buffer and reread input
r.buffer = r.buffer[6:]
*sz = 0
return r.GetChar()
*sz = 6
if r.buffer[4] == '0' {
return Event{BracketedPasteBegin, 0, nil}
}
return Event{BracketedPasteEnd, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case '3':
if r.buffer[3] == '~' {
return Event{Delete, 0, nil}
}
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftDelete, 0, nil}
case '1':
return Event{AltDelete, 0, nil}
case '2':
return Event{AltShiftDelete, 0, nil}
case '3':
return Event{CtrlAltDelete, 0, nil}
case '4':
return Event{CtrlAltShiftDelete, 0, nil}
case '5':
return Event{CtrlAltDelete, 0, nil}
case '6':
return Event{CtrlAltShiftDelete, 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}
case '3':
return Event{AltDelete, 0, nil}
case '4':
return Event{AltShiftDelete, 0, nil}
case '5':
return Event{CtrlDelete, 0, nil}
case '6':
return Event{CtrlShiftDelete, 0, nil}
case '7':
return Event{CtrlAltDelete, 0, nil}
case '8':
return Event{CtrlAltShiftDelete, 0, nil}
case '9':
return Event{AltDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4':
return Event{End, 0, nil}
case '5':
return Event{PageUp, 0, nil}
if r.buffer[3] == '~' {
return Event{PageUp, 0, nil}
}
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftPageUp, 0, nil}
case '1':
return Event{AltPageUp, 0, nil}
case '2':
return Event{AltShiftPageUp, 0, nil}
case '3':
return Event{CtrlAltPageUp, 0, nil}
case '4':
return Event{CtrlAltShiftPageUp, 0, nil}
case '5':
return Event{CtrlAltPageUp, 0, nil}
case '6':
return Event{CtrlAltShiftPageUp, 0, nil}
}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '2':
return Event{ShiftPageUp, 0, nil}
case '3':
return Event{AltPageUp, 0, nil}
case '4':
return Event{AltShiftPageUp, 0, nil}
case '5':
return Event{CtrlPageUp, 0, nil}
case '6':
return Event{CtrlShiftPageUp, 0, nil}
case '7':
return Event{CtrlAltPageUp, 0, nil}
case '8':
return Event{CtrlAltShiftPageUp, 0, nil}
case '9':
return Event{AltPageUp, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '6':
return Event{PageDown, 0, nil}
if r.buffer[3] == '~' {
return Event{PageDown, 0, nil}
}
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftPageDown, 0, nil}
case '1':
return Event{AltPageDown, 0, nil}
case '2':
return Event{AltShiftPageDown, 0, nil}
case '3':
return Event{CtrlAltPageDown, 0, nil}
case '4':
return Event{CtrlAltShiftPageDown, 0, nil}
case '5':
return Event{CtrlAltPageDown, 0, nil}
case '6':
return Event{CtrlAltShiftPageDown, 0, nil}
}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '2':
return Event{ShiftPageDown, 0, nil}
case '3':
return Event{AltPageDown, 0, nil}
case '4':
return Event{AltShiftPageDown, 0, nil}
case '5':
return Event{CtrlPageDown, 0, nil}
case '6':
return Event{CtrlShiftPageDown, 0, nil}
case '7':
return Event{CtrlAltPageDown, 0, nil}
case '8':
return Event{CtrlAltShiftPageDown, 0, nil}
case '9':
return Event{AltPageDown, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
@@ -525,63 +687,172 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
*sz = 6
switch r.buffer[4] {
case '1', '2', '3', '4', '5':
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
// 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'
ctrl := bytes.IndexByte([]byte{'5', '6', '7', '8'}, r.buffer[4]) >= 0
alt := bytes.IndexByte([]byte{'3', '4', '7', '8'}, r.buffer[4]) >= 0
shift := bytes.IndexByte([]byte{'2', '4', '6', '8'}, r.buffer[4]) >= 0
char := r.buffer[5]
altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
}
*sz = 7
char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if r.buffer[4] == '9' {
ctrl = false
alt = true
shift = false
if len(r.buffer) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
char = r.buffer[5]
} else if r.buffer[4] == '1' && bytes.IndexByte([]byte{'0', '1', '2', '3', '4', '5', '6'}, r.buffer[5]) >= 0 {
ctrl = bytes.IndexByte([]byte{'3', '4', '5', '6'}, r.buffer[5]) >= 0
alt = true
shift = bytes.IndexByte([]byte{'0', '2', '4', '6'}, r.buffer[5]) >= 0
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
}
*sz = 7
char = r.buffer[6]
}
ctrlShift := ctrl && shift
ctrlAlt := ctrl && alt
altShift := alt && shift
ctrlAltShift := ctrl && alt && shift
switch char {
case 'A':
if alt {
return Event{AltUp, 0, nil}
if ctrlAltShift {
return Event{CtrlAltShiftUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftUp, 0, nil}
}
if altShift {
return Event{AltShiftUp, 0, nil}
}
return Event{ShiftUp, 0, nil}
case 'B':
if ctrl {
return Event{CtrlUp, 0, nil}
}
if alt {
return Event{AltDown, 0, nil}
return Event{AltUp, 0, nil}
}
if shift {
return Event{ShiftUp, 0, nil}
}
case 'B':
if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDown, 0, nil}
}
if altShift {
return Event{AltShiftDown, 0, nil}
}
return Event{ShiftDown, 0, nil}
case 'C':
if ctrl {
return Event{CtrlDown, 0, nil}
}
if alt {
return Event{AltRight, 0, nil}
return Event{AltDown, 0, nil}
}
if shift {
return Event{ShiftDown, 0, nil}
}
case 'C':
if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltRight, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftRight, 0, nil}
}
if altShift {
return Event{AltShiftRight, 0, nil}
}
return Event{ShiftRight, 0, nil}
case 'D':
if ctrl {
return Event{CtrlRight, 0, nil}
}
if shift {
return Event{ShiftRight, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
return Event{AltRight, 0, nil}
}
case 'D':
if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltLeft, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftLeft, 0, nil}
}
if altShift {
return Event{AltShiftLeft, 0, nil}
}
return Event{ShiftLeft, 0, nil}
if ctrl {
return Event{CtrlLeft, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
}
if shift {
return Event{ShiftLeft, 0, nil}
}
case 'H':
if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltHome, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftHome, 0, nil}
}
if altShift {
return Event{AltShiftHome, 0, nil}
}
if ctrl {
return Event{CtrlHome, 0, nil}
}
if alt {
return Event{AltHome, 0, nil}
}
if shift {
return Event{ShiftHome, 0, nil}
}
case 'F':
if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltEnd, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftEnd, 0, nil}
}
if altShift {
return Event{AltShiftEnd, 0, nil}
}
if ctrl {
return Event{CtrlEnd, 0, nil}
}
if alt {
return Event{AltEnd, 0, nil}
}
if shift {
return Event{ShiftEnd, 0, nil}
}
}
} // r.buffer[4]
} // r.buffer[3]
@@ -681,7 +952,7 @@ func (r *LightRenderer) rmcup() {
}
func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.disableModes()
r.restoreTerminal()
if clear {
if r.fullscreen {
@@ -694,12 +965,13 @@ func (r *LightRenderer) Pause(clear bool) {
}
}
func (r *LightRenderer) enableMouse() {
func (r *LightRenderer) enableModes() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
r.csi("?2004h") // Enable bracketed paste mode
}
func (r *LightRenderer) disableMouse() {
@@ -710,6 +982,11 @@ func (r *LightRenderer) disableMouse() {
}
}
func (r *LightRenderer) disableModes() {
r.disableMouse()
r.csi("?2004l")
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal()
if clear {
@@ -718,7 +995,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else {
r.rmcup()
}
r.enableMouse()
r.enableModes()
r.flush()
} else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z):
@@ -773,7 +1050,7 @@ func (r *LightRenderer) Close() {
if !r.showCursor {
r.csi("?25h")
}
r.disableMouse()
r.disableModes()
r.flush()
r.restoreTerminal()
r.closePlatform()
@@ -795,8 +1072,8 @@ func (r *LightRenderer) MaxY() int {
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = util.Max(0, width)
height = util.Max(0, height)
width = max(0, width)
height = max(0, height)
w := &LightWindow{
renderer: r,
colored: r.theme.Colored,
@@ -822,11 +1099,14 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind
case WindowHeader:
w.fg = r.theme.Header.Color
w.bg = r.theme.HeaderBg.Color
case WindowFooter:
w.fg = r.theme.Footer.Color
w.bg = r.theme.FooterBg.Color
case WindowPreview:
w.fg = r.theme.PreviewFg.Color
w.bg = r.theme.PreviewBg.Color
}
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone {
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone && w.height > 0 {
// fzf --color bg:blue --border --padding 1,2
w.Erase()
}
@@ -882,6 +1162,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
@@ -907,6 +1189,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
@@ -934,6 +1218,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
@@ -1036,7 +1322,18 @@ func attrCodes(attr Attr) []string {
codes = append(codes, "3")
}
if (attr & Underline) > 0 {
codes = append(codes, "4")
switch attr.UnderlineStyle() {
case UlStyleDouble:
codes = append(codes, "4:2")
case UlStyleCurly:
codes = append(codes, "4:3")
case UlStyleDotted:
codes = append(codes, "4:4")
case UlStyleDashed:
codes = append(codes, "4:5")
default:
codes = append(codes, "4")
}
}
if (attr & Blink) > 0 {
codes = append(codes, "5")
@@ -1074,8 +1371,27 @@ func colorCodes(fg Color, bg Color) []string {
return codes
}
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
func ulColorCode(c Color) string {
if c == colDefault {
return ""
}
if c.is24() {
r := (c >> 16) & 0xff
g := (c >> 8) & 0xff
b := (c) & 0xff
return fmt.Sprintf("58;2;%d;%d;%d", r, g, b)
}
if c >= 0 && c < 256 {
return fmt.Sprintf("58;5;%d", c)
}
return ""
}
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
if ulCode := ulColorCode(ul); ulCode != "" {
codes = append(codes, ulCode)
}
code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0, code
}
@@ -1089,65 +1405,28 @@ func cleanse(str string) string {
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
w.stderrInternal(cleanse(text), false, code)
w.csi("0m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, attr)
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
if hasColors {
defer w.csi("0m")
}
w.stderrInternal(cleanse(text), false, code)
}
type wrappedLine struct {
text string
displayWidth int
}
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
lines := []wrappedLine{}
width := 0
line := ""
gr := uniseg.NewGraphemes(input)
max := initialMax
for gr.Next() {
rs := gr.Runes()
str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else {
w = uniseg.StringWidth(str)
}
width += w
if prefixLength+width <= max {
line += str
} else {
lines = append(lines, wrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = w
max = initialMax - wrapSignWidth
}
}
lines = append(lines, wrappedLine{string(line), width})
return lines
}
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n")
for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
for j, wl := range lines {
w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth
if w.posx < w.width {
w.stderrInternal(wl.Text, false, resetCode)
w.posx += wl.DisplayWidth
}
// Wrap line
if j < len(lines)-1 || i < len(allLines)-1 {
@@ -1185,7 +1464,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
func (w *LightWindow) setBg() string {
if w.bg != colDefault {
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
return code
}
// Should clear dim attribute after ␍ in the preview window
@@ -1207,7 +1486,7 @@ func (w *LightWindow) Fill(text string) FillReturn {
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, ul Color, attr Attr, text string) FillReturn {
w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
@@ -1215,7 +1494,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault {
bg = w.bg
}
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
defer w.csi("0m")
return w.fill(text, resetCode)
}

352
src/tui/light_test.go Normal file
View File

@@ -0,0 +1,352 @@
package tui
import (
"fmt"
"os"
"testing"
"unicode"
)
func TestLightRenderer(t *testing.T) {
tty_file, _ := os.Open("")
renderer, _ := NewLightRenderer(
"", tty_file, &ColorTheme{}, true, false, 0, false, true,
func(h int) int { return h })
light_renderer := renderer.(*LightRenderer)
go func() {
for {
light_renderer.mutex.Lock()
ready := light_renderer.cancel != nil
light_renderer.mutex.Unlock()
if ready {
light_renderer.CancelGetChar()
break
}
}
}()
event := light_renderer.GetChar(true)
if event.Type != Invalid {
t.Error("Not cancelled")
}
assertCharSequence := func(sequence string, name string) {
bytes := []byte(sequence)
light_renderer.buffer = bytes
event := light_renderer.GetChar(true)
if event.KeyName() != name {
t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s",
string(bytes), bytes,
event.KeyName(), event.Type.String(), name)
}
}
assertEscSequence := func(sequence string, name string) {
bytes := []byte(sequence)
light_renderer.buffer = bytes
sz := 1
event := light_renderer.escSequence(&sz)
if fmt.Sprintf("!%s", event.Type.String()) == name {
// this is fine
} else if event.KeyName() != name {
t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s",
string(bytes), bytes,
event.KeyName(), event.Type.String(), name)
}
}
// invalid
assertEscSequence("\x1b[<", "!Invalid")
assertEscSequence("\x1b[1;1R", "!Invalid")
assertEscSequence("\x1b[", "!Invalid")
assertEscSequence("\x1b[1", "!Invalid")
assertEscSequence("\x1b[3;3~1", "!Invalid")
assertEscSequence("\x1b[13", "!Invalid")
assertEscSequence("\x1b[1;3", "!Invalid")
assertEscSequence("\x1b[1;10", "!Invalid")
assertEscSequence("\x1b[220~", "!Invalid")
assertEscSequence("\x1b[5;30~", "!Invalid")
assertEscSequence("\x1b[6;30~", "!Invalid")
// general
for r := 'a'; r < 'z'; r++ {
lower_r := fmt.Sprintf("%c", r)
upper_r := fmt.Sprintf("%c", unicode.ToUpper(r))
assertCharSequence(lower_r, lower_r)
assertCharSequence(upper_r, upper_r)
}
assertCharSequence("\x01", "ctrl-a")
assertCharSequence("\x02", "ctrl-b")
assertCharSequence("\x03", "ctrl-c")
assertCharSequence("\x04", "ctrl-d")
assertCharSequence("\x05", "ctrl-e")
assertCharSequence("\x06", "ctrl-f")
assertCharSequence("\x07", "ctrl-g")
// ctrl-h is the same as ctrl-backspace
// ctrl-i is the same as tab
assertCharSequence("\n", "ctrl-j")
assertCharSequence("\x0b", "ctrl-k")
assertCharSequence("\x0c", "ctrl-l")
assertCharSequence("\r", "enter") // enter
assertCharSequence("\x0e", "ctrl-n")
assertCharSequence("\x0f", "ctrl-o")
assertCharSequence("\x10", "ctrl-p")
assertCharSequence("\x11", "ctrl-q")
assertCharSequence("\x12", "ctrl-r")
assertCharSequence("\x13", "ctrl-s")
assertCharSequence("\x14", "ctrl-t")
assertCharSequence("\x15", "ctrl-u")
assertCharSequence("\x16", "ctrl-v")
assertCharSequence("\x17", "ctrl-w")
assertCharSequence("\x18", "ctrl-x")
assertCharSequence("\x19", "ctrl-y")
assertCharSequence("\x1a", "ctrl-z")
assertCharSequence("\x00", "ctrl-space")
assertCharSequence("\x1c", "ctrl-\\")
assertCharSequence("\x1d", "ctrl-]")
assertCharSequence("\x1e", "ctrl-^")
assertCharSequence("\x1f", "ctrl-/")
assertEscSequence("\x1ba", "alt-a")
assertEscSequence("\x1bb", "alt-b")
assertEscSequence("\x1bc", "alt-c")
assertEscSequence("\x1bd", "alt-d")
assertEscSequence("\x1be", "alt-e")
assertEscSequence("\x1bf", "alt-f")
assertEscSequence("\x1bg", "alt-g")
assertEscSequence("\x1bh", "alt-h")
assertEscSequence("\x1bi", "alt-i")
assertEscSequence("\x1bj", "alt-j")
assertEscSequence("\x1bk", "alt-k")
assertEscSequence("\x1bl", "alt-l")
assertEscSequence("\x1bm", "alt-m")
assertEscSequence("\x1bn", "alt-n")
assertEscSequence("\x1bo", "alt-o")
assertEscSequence("\x1bp", "alt-p")
assertEscSequence("\x1bq", "alt-q")
assertEscSequence("\x1br", "alt-r")
assertEscSequence("\x1bs", "alt-s")
assertEscSequence("\x1bt", "alt-t")
assertEscSequence("\x1bu", "alt-u")
assertEscSequence("\x1bv", "alt-v")
assertEscSequence("\x1bw", "alt-w")
assertEscSequence("\x1bx", "alt-x")
assertEscSequence("\x1by", "alt-y")
assertEscSequence("\x1bz", "alt-z")
assertEscSequence("\x1bOP", "f1")
assertEscSequence("\x1bOQ", "f2")
assertEscSequence("\x1bOR", "f3")
assertEscSequence("\x1bOS", "f4")
assertEscSequence("\x1b[15~", "f5")
assertEscSequence("\x1b[17~", "f6")
assertEscSequence("\x1b[18~", "f7")
assertEscSequence("\x1b[19~", "f8")
assertEscSequence("\x1b[20~", "f9")
assertEscSequence("\x1b[21~", "f10")
assertEscSequence("\x1b[23~", "f11")
assertEscSequence("\x1b[24~", "f12")
assertEscSequence("\x1b", "esc")
assertCharSequence("\t", "tab")
assertEscSequence("\x1b[Z", "shift-tab")
assertCharSequence("\x7f", "backspace")
assertEscSequence("\x1b\x7f", "alt-backspace")
assertCharSequence("\b", "ctrl-backspace")
assertEscSequence("\x1b\b", "ctrl-alt-backspace")
assertEscSequence("\x1b[A", "up")
assertEscSequence("\x1b[B", "down")
assertEscSequence("\x1b[C", "right")
assertEscSequence("\x1b[D", "left")
assertEscSequence("\x1b[H", "home")
assertEscSequence("\x1b[F", "end")
assertEscSequence("\x1b[2~", "insert")
assertEscSequence("\x1b[3~", "delete")
assertEscSequence("\x1b[5~", "page-up")
assertEscSequence("\x1b[6~", "page-down")
assertEscSequence("\x1b[7~", "home")
assertEscSequence("\x1b[8~", "end")
assertEscSequence("\x1b[1;2A", "shift-up")
assertEscSequence("\x1b[1;2B", "shift-down")
assertEscSequence("\x1b[1;2C", "shift-right")
assertEscSequence("\x1b[1;2D", "shift-left")
assertEscSequence("\x1b[1;2H", "shift-home")
assertEscSequence("\x1b[1;2F", "shift-end")
assertEscSequence("\x1b[3;2~", "shift-delete")
assertEscSequence("\x1b[5;2~", "shift-page-up")
assertEscSequence("\x1b[6;2~", "shift-page-down")
assertEscSequence("\x1b\x1b", "esc")
assertEscSequence("\x1b\x1b[A", "alt-up")
assertEscSequence("\x1b\x1b[B", "alt-down")
assertEscSequence("\x1b\x1b[C", "alt-right")
assertEscSequence("\x1b\x1b[D", "alt-left")
assertEscSequence("\x1b[1;3A", "alt-up")
assertEscSequence("\x1b[1;3B", "alt-down")
assertEscSequence("\x1b[1;3C", "alt-right")
assertEscSequence("\x1b[1;3D", "alt-left")
assertEscSequence("\x1b[1;3H", "alt-home")
assertEscSequence("\x1b[1;3F", "alt-end")
assertEscSequence("\x1b[3;3~", "alt-delete")
assertEscSequence("\x1b[5;3~", "alt-page-up")
assertEscSequence("\x1b[6;3~", "alt-page-down")
assertEscSequence("\x1b[1;4A", "alt-shift-up")
assertEscSequence("\x1b[1;4B", "alt-shift-down")
assertEscSequence("\x1b[1;4C", "alt-shift-right")
assertEscSequence("\x1b[1;4D", "alt-shift-left")
assertEscSequence("\x1b[1;4H", "alt-shift-home")
assertEscSequence("\x1b[1;4F", "alt-shift-end")
assertEscSequence("\x1b[3;4~", "alt-shift-delete")
assertEscSequence("\x1b[5;4~", "alt-shift-page-up")
assertEscSequence("\x1b[6;4~", "alt-shift-page-down")
assertEscSequence("\x1b[1;5A", "ctrl-up")
assertEscSequence("\x1b[1;5B", "ctrl-down")
assertEscSequence("\x1b[1;5C", "ctrl-right")
assertEscSequence("\x1b[1;5D", "ctrl-left")
assertEscSequence("\x1b[1;5H", "ctrl-home")
assertEscSequence("\x1b[1;5F", "ctrl-end")
assertEscSequence("\x1b[3;5~", "ctrl-delete")
assertEscSequence("\x1b[5;5~", "ctrl-page-up")
assertEscSequence("\x1b[6;5~", "ctrl-page-down")
assertEscSequence("\x1b[1;7A", "ctrl-alt-up")
assertEscSequence("\x1b[1;7B", "ctrl-alt-down")
assertEscSequence("\x1b[1;7C", "ctrl-alt-right")
assertEscSequence("\x1b[1;7D", "ctrl-alt-left")
assertEscSequence("\x1b[1;7H", "ctrl-alt-home")
assertEscSequence("\x1b[1;7F", "ctrl-alt-end")
assertEscSequence("\x1b[3;7~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;6A", "ctrl-shift-up")
assertEscSequence("\x1b[1;6B", "ctrl-shift-down")
assertEscSequence("\x1b[1;6C", "ctrl-shift-right")
assertEscSequence("\x1b[1;6D", "ctrl-shift-left")
assertEscSequence("\x1b[1;6H", "ctrl-shift-home")
assertEscSequence("\x1b[1;6F", "ctrl-shift-end")
assertEscSequence("\x1b[3;6~", "ctrl-shift-delete")
assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up")
assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down")
assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down")
// xterm meta & mac
assertEscSequence("\x1b[1;9A", "alt-up")
assertEscSequence("\x1b[1;9B", "alt-down")
assertEscSequence("\x1b[1;9C", "alt-right")
assertEscSequence("\x1b[1;9D", "alt-left")
assertEscSequence("\x1b[1;9H", "alt-home")
assertEscSequence("\x1b[1;9F", "alt-end")
assertEscSequence("\x1b[3;9~", "alt-delete")
assertEscSequence("\x1b[5;9~", "alt-page-up")
assertEscSequence("\x1b[6;9~", "alt-page-down")
assertEscSequence("\x1b[1;10A", "alt-shift-up")
assertEscSequence("\x1b[1;10B", "alt-shift-down")
assertEscSequence("\x1b[1;10C", "alt-shift-right")
assertEscSequence("\x1b[1;10D", "alt-shift-left")
assertEscSequence("\x1b[1;10H", "alt-shift-home")
assertEscSequence("\x1b[1;10F", "alt-shift-end")
assertEscSequence("\x1b[3;10~", "alt-shift-delete")
assertEscSequence("\x1b[5;10~", "alt-shift-page-up")
assertEscSequence("\x1b[6;10~", "alt-shift-page-down")
assertEscSequence("\x1b[1;11A", "alt-up")
assertEscSequence("\x1b[1;11B", "alt-down")
assertEscSequence("\x1b[1;11C", "alt-right")
assertEscSequence("\x1b[1;11D", "alt-left")
assertEscSequence("\x1b[1;11H", "alt-home")
assertEscSequence("\x1b[1;11F", "alt-end")
assertEscSequence("\x1b[3;11~", "alt-delete")
assertEscSequence("\x1b[5;11~", "alt-page-up")
assertEscSequence("\x1b[6;11~", "alt-page-down")
assertEscSequence("\x1b[1;12A", "alt-shift-up")
assertEscSequence("\x1b[1;12B", "alt-shift-down")
assertEscSequence("\x1b[1;12C", "alt-shift-right")
assertEscSequence("\x1b[1;12D", "alt-shift-left")
assertEscSequence("\x1b[1;12H", "alt-shift-home")
assertEscSequence("\x1b[1;12F", "alt-shift-end")
assertEscSequence("\x1b[3;12~", "alt-shift-delete")
assertEscSequence("\x1b[5;12~", "alt-shift-page-up")
assertEscSequence("\x1b[6;12~", "alt-shift-page-down")
assertEscSequence("\x1b[1;13A", "ctrl-alt-up")
assertEscSequence("\x1b[1;13B", "ctrl-alt-down")
assertEscSequence("\x1b[1;13C", "ctrl-alt-right")
assertEscSequence("\x1b[1;13D", "ctrl-alt-left")
assertEscSequence("\x1b[1;13H", "ctrl-alt-home")
assertEscSequence("\x1b[1;13F", "ctrl-alt-end")
assertEscSequence("\x1b[3;13~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down")
assertEscSequence("\x1b[1;15A", "ctrl-alt-up")
assertEscSequence("\x1b[1;15B", "ctrl-alt-down")
assertEscSequence("\x1b[1;15C", "ctrl-alt-right")
assertEscSequence("\x1b[1;15D", "ctrl-alt-left")
assertEscSequence("\x1b[1;15H", "ctrl-alt-home")
assertEscSequence("\x1b[1;15F", "ctrl-alt-end")
assertEscSequence("\x1b[3;15~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down")
// tmux & emacs
assertEscSequence("\x1bOA", "up")
assertEscSequence("\x1bOB", "down")
assertEscSequence("\x1bOC", "right")
assertEscSequence("\x1bOD", "left")
assertEscSequence("\x1bOH", "home")
assertEscSequence("\x1bOF", "end")
// rrvt
assertEscSequence("\x1b[1~", "home")
assertEscSequence("\x1b[4~", "end")
assertEscSequence("\x1b[11~", "f1")
assertEscSequence("\x1b[12~", "f2")
assertEscSequence("\x1b[13~", "f3")
assertEscSequence("\x1b[14~", "f4")
}

View File

@@ -42,26 +42,35 @@ func (r *LightRenderer) closePlatform() {
r.ttyout.Close()
}
func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil {
func openTty(ttyDefault string, mode int) (*os.File, error) {
var in *os.File
var err error
if len(ttyDefault) > 0 {
in, err = os.OpenFile(ttyDefault, mode, 0)
}
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil
}
}
return nil, errors.New("failed to open " + consoleDevice)
if ttyDefault != DefaultTtyDevice {
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
return in, nil
}
}
return nil, errors.New("failed to open " + DefaultTtyDevice)
}
return in, nil
}
func openTtyIn() (*os.File, error) {
return openTty(syscall.O_RDONLY)
func openTtyIn(ttyDefault string) (*os.File, error) {
return openTty(ttyDefault, syscall.O_RDONLY)
}
func openTtyOut() (*os.File, error) {
return openTty(syscall.O_WRONLY)
func openTtyOut(ttyDefault string) (*os.File, error) {
return openTty(ttyDefault, syscall.O_WRONLY)
}
func (r *LightRenderer) setupTerminal() {
@@ -89,8 +98,8 @@ func (r *LightRenderer) findOffset() (row int, col int) {
r.flush()
var err error
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
bytes, err = r.getBytesInternal(bytes, tries > 0)
for tries := range offsetPollTries {
bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
if err != nil {
return -1, -1
}
@@ -105,15 +114,62 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return -1, -1
}
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
b := make([]byte, 1)
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
fd := r.fd()
util.SetNonblock(r.ttyin, nonblock)
_, err := util.Read(fd, b)
if err != nil {
return 0, false
getter := func() (int, getCharResult) {
b := make([]byte, 1)
util.SetNonblock(r.ttyin, nonblock)
_, err := util.Read(fd, b)
if err != nil {
return 0, getCharError
}
return int(b[0]), getCharSuccess
}
return int(b[0]), true
if nonblock || !cancellable {
return getter()
}
rpipe, wpipe, err := os.Pipe()
if err != nil {
// Fallback to blocking read without cancellation
return getter()
}
r.setCancel(func() {
wpipe.Write([]byte{0})
})
defer func() {
r.setCancel(nil)
rpipe.Close()
wpipe.Close()
}()
cancelFd := int(rpipe.Fd())
for range maxSelectTries {
var rfds unix.FdSet
limit := len(rfds.Bits) * unix.NFDBITS
if fd >= limit || cancelFd >= limit {
return getter()
}
rfds.Set(fd)
rfds.Set(cancelFd)
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
if err != nil {
if err == syscall.EINTR {
continue
}
return 0, getCharError
}
if rfds.IsSet(cancelFd) {
return 0, getCharCancelled
}
if rfds.IsSet(fd) {
return getter()
}
}
return 0, getCharError
}
func (r *LightRenderer) Size() TermSize {

View File

@@ -76,12 +76,12 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
}
func openTtyIn() (*os.File, error) {
func openTtyIn(ttyDefault string) (*os.File, error) {
// not used
return nil, nil
}
func openTtyOut() (*os.File, error) {
func openTtyOut(ttyDefault string) (*os.File, error) {
return os.Stderr, nil
}
@@ -151,16 +151,33 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
}
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
if nonblock {
select {
case bc := <-r.ttyinChannel:
return int(bc), true
case <-time.After(timeoutInterval * time.Millisecond):
return 0, false
}
} else {
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
if !nonblock && !cancellable {
bc := <-r.ttyinChannel
return int(bc), true
return int(bc), getCharSuccess
}
var timeout <-chan time.Time
if nonblock {
timeout = time.After(timeoutInterval * time.Millisecond)
}
var cancel chan struct{}
if cancellable {
cancel = make(chan struct{})
r.setCancel(func() {
close(cancel)
})
defer r.setCancel(nil)
}
select {
case bc := <-r.ttyinChannel:
return int(bc), getCharSuccess
case <-cancel:
return 0, getCharCancelled
case <-timeout:
// NOTE: not really an error
return 0, getCharError
}
}

View File

@@ -5,6 +5,7 @@ package tui
import (
"os"
"regexp"
"strings"
"time"
"github.com/gdamore/tcell/v2"
@@ -36,8 +37,6 @@ func (p ColorPair) style() tcell.Style {
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
}
type Attr int32
type TcellWindow struct {
color bool
windowType WindowType
@@ -55,6 +54,7 @@ type TcellWindow struct {
showCursor bool
wrapSign string
wrapSignWidth int
tabstop int
}
func (w *TcellWindow) Top() int {
@@ -98,13 +98,6 @@ const (
Italic = Attr(tcell.AttrItalic)
)
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 7)
AttrClear = Attr(1 << 8)
BoldForce = Attr(1 << 10)
)
func (r *FullscreenRenderer) Bell() {
_screen.Beep()
}
@@ -158,15 +151,6 @@ func (c Color) Style() tcell.Color {
}
}
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
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 (
@@ -197,6 +181,7 @@ func (r *FullscreenRenderer) initScreen() error {
if e = s.Init(); e != nil {
return e
}
s.EnablePaste()
if r.mouse {
s.EnableMouse()
} else {
@@ -263,9 +248,14 @@ func (r *FullscreenRenderer) Size() TermSize {
return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar() Event {
func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventPaste:
if ev.Start() {
return Event{BracketedPasteBegin, 0, nil}
}
return Event{BracketedPasteEnd, 0, nil}
case *tcell.EventResize:
// Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
@@ -347,6 +337,8 @@ func (r *FullscreenRenderer) GetChar() Event {
shift := (mods & tcell.ModShift) > 0
ctrlAlt := ctrl && alt
altShift := alt && shift
ctrlShift := ctrl && shift
ctrlAltShift := ctrl && alt && shift
keyfn := func(r rune) Event {
if alt {
@@ -373,8 +365,11 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyCtrlH:
switch ev.Rune() {
case 0:
if ctrlAlt {
return Event{CtrlAltBackspace, 0, nil}
}
if ctrl {
return Event{Backspace, 0, nil}
return Event{CtrlBackspace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
@@ -435,6 +430,9 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlSlash, 0, nil}
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2:
if ctrl {
return Event{CtrlBackspace, 0, nil}
}
if alt {
return Event{AltBackspace, 0, nil}
}
@@ -442,9 +440,21 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp:
if ctrlAltShift {
return Event{CtrlAltShiftUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftUp, 0, nil}
}
if altShift {
return Event{AltShiftUp, 0, nil}
}
if ctrl {
return Event{CtrlUp, 0, nil}
}
if shift {
return Event{ShiftUp, 0, nil}
}
@@ -453,9 +463,21 @@ func (r *FullscreenRenderer) GetChar() Event {
}
return Event{Up, 0, nil}
case tcell.KeyDown:
if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDown, 0, nil}
}
if altShift {
return Event{AltShiftDown, 0, nil}
}
if ctrl {
return Event{CtrlDown, 0, nil}
}
if shift {
return Event{ShiftDown, 0, nil}
}
@@ -464,9 +486,21 @@ func (r *FullscreenRenderer) GetChar() Event {
}
return Event{Down, 0, nil}
case tcell.KeyLeft:
if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltLeft, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftLeft, 0, nil}
}
if altShift {
return Event{AltShiftLeft, 0, nil}
}
if ctrl {
return Event{CtrlLeft, 0, nil}
}
if shift {
return Event{ShiftLeft, 0, nil}
}
@@ -475,9 +509,21 @@ func (r *FullscreenRenderer) GetChar() Event {
}
return Event{Left, 0, nil}
case tcell.KeyRight:
if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltRight, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftRight, 0, nil}
}
if altShift {
return Event{AltShiftRight, 0, nil}
}
if ctrl {
return Event{CtrlRight, 0, nil}
}
if shift {
return Event{ShiftRight, 0, nil}
}
@@ -490,20 +536,119 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyInsert:
return Event{Insert, 0, nil}
case tcell.KeyHome:
if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltHome, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftHome, 0, nil}
}
if altShift {
return Event{AltShiftHome, 0, nil}
}
if ctrl {
return Event{CtrlHome, 0, nil}
}
if shift {
return Event{ShiftHome, 0, nil}
}
if alt {
return Event{AltHome, 0, nil}
}
return Event{Home, 0, nil}
case tcell.KeyDelete:
if ctrlAltShift {
return Event{CtrlAltShiftDelete, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDelete, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDelete, 0, nil}
}
if altShift {
return Event{AltShiftDelete, 0, nil}
}
if ctrl {
return Event{CtrlDelete, 0, nil}
}
if alt {
return Event{AltDelete, 0, nil}
}
if shift {
return Event{ShiftDelete, 0, nil}
}
return Event{Delete, 0, nil}
case tcell.KeyEnd:
if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltEnd, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftEnd, 0, nil}
}
if altShift {
return Event{AltShiftEnd, 0, nil}
}
if ctrl {
return Event{CtrlEnd, 0, nil}
}
if shift {
return Event{ShiftEnd, 0, nil}
}
if alt {
return Event{AltEnd, 0, nil}
}
return Event{End, 0, nil}
case tcell.KeyPgUp:
if ctrlAltShift {
return Event{CtrlAltShiftPageUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltPageUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftPageUp, 0, nil}
}
if altShift {
return Event{AltShiftPageUp, 0, nil}
}
if ctrl {
return Event{CtrlPageUp, 0, nil}
}
if shift {
return Event{ShiftPageUp, 0, nil}
}
if alt {
return Event{AltPageUp, 0, nil}
}
return Event{PageUp, 0, nil}
case tcell.KeyPgDn:
if ctrlAltShift {
return Event{CtrlAltShiftPageDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltPageDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftPageDown, 0, nil}
}
if altShift {
return Event{AltShiftPageDown, 0, nil}
}
if ctrl {
return Event{CtrlPageDown, 0, nil}
}
if shift {
return Event{ShiftPageDown, 0, nil}
}
if alt {
return Event{AltPageDown, 0, nil}
}
return Event{PageDown, 0, nil}
case tcell.KeyBacktab:
return Event{ShiftTab, 0, nil}
@@ -560,15 +705,19 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) CancelGetChar() {
// TODO
}
func (r *FullscreenRenderer) Pause(clear bool) {
if clear {
r.Close()
_screen.Suspend()
}
}
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
if clear {
r.initScreen()
_screen.Resume()
}
}
@@ -586,14 +735,16 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = util.Max(0, width)
height = util.Max(0, height)
width = max(0, width)
height = max(0, height)
normal := ColBorder
switch windowType {
case WindowList:
normal = ColNormal
case WindowHeader:
normal = ColHeader
case WindowFooter:
normal = ColFooter
case WindowInput:
normal = ColInput
case WindowPreview:
@@ -608,7 +759,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height,
normal: normal,
borderStyle: borderStyle,
showCursor: r.showCursor}
showCursor: r.showCursor,
tabstop: r.tabstop}
w.Erase()
return w
}
@@ -676,6 +828,21 @@ func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
return style
}
func underlineStyleFromAttr(a Attr) tcell.UnderlineStyle {
switch a.UnderlineStyle() {
case UlStyleDouble:
return tcell.UnderlineStyleDouble
case UlStyleCurly:
return tcell.UnderlineStyleCurly
case UlStyleDotted:
return tcell.UnderlineStyleDotted
case UlStyleDashed:
return tcell.UnderlineStyleDashed
default:
return tcell.UnderlineStyleSolid
}
}
func (w *TcellWindow) printString(text string, pair ColorPair) {
lx := 0
a := pair.Attr()
@@ -684,11 +851,18 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
if a&AttrClear == 0 {
style = style.
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0).
Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
}
style = w.withUrl(style)
@@ -723,10 +897,8 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
w.printString(text, pair)
}
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
lx := 0
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
a := pair.Attr()
var style tcell.Style
if w.color {
style = pair.style()
@@ -738,64 +910,73 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
style = w.withUrl(style)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
return w.withUrl(style)
}
func (w *TcellWindow) renderGraphemes(text string, style tcell.Style) {
gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() {
st := style
rs := gr.Runes()
if len(rs) == 1 {
r := rs[0]
switch r {
case '\r':
st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++
w.lastX = 0
lx = 0
continue Loop
}
}
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= w.left+w.width {
w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0
lx = 0
xPos = w.left
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
wgr := uniseg.NewGraphemes(sign)
for wgr.Next() {
rs := wgr.Runes()
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
lx += uniseg.StringWidth(string(rs))
}
xPos = w.left + lx
if len(rs) == 1 && rs[0] == '\r' {
st = style.Dim(true)
rs[0] = '␍'
}
xPos := w.left + w.lastX
yPos := w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
}
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
lx += util.StringWidth(string(rs))
w.lastX += util.StringWidth(string(rs))
}
w.lastX += lx
if w.lastX == w.width {
}
func (w *TcellWindow) renderWrapSign(style tcell.Style) {
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
gr := uniseg.NewGraphemes(sign)
for gr.Next() {
rs := gr.Runes()
_screen.SetContent(w.left+w.lastX, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
w.lastX += uniseg.StringWidth(string(rs))
}
}
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
style := w.pairStyle(pair)
for i, segment := range strings.Split(text, "\n") {
for j, wl := range WrapLine(segment, w.lastX, w.width, w.tabstop, w.wrapSignWidth) {
if i > 0 || j > 0 {
w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0
if j > 0 {
w.renderWrapSign(style)
}
}
if w.lastX < w.width {
w.renderGraphemes(wl.Text, style)
}
}
}
if w.lastX >= w.width {
w.lastY++
w.lastX = 0
return FillNextLine
@@ -818,14 +999,14 @@ func (w *TcellWindow) LinkEnd() {
w.params = nil
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
func (w *TcellWindow) CFill(fg Color, bg Color, ul Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = w.normal.Fg()
}
if bg == colDefault {
bg = w.normal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg, a))
return w.fillString(str, NewColorPair(fg, bg, a).WithUl(ul))
}
func (w *TcellWindow) DrawBorder() {
@@ -859,6 +1040,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = ColListBorder.style()
case WindowHeader:
style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput:
style = ColInputBorder.style()
case WindowPreview:

View File

@@ -10,7 +10,7 @@ import (
"github.com/junegunn/fzf/src/util"
)
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
func assert(t *testing.T, context string, got any, want any) bool {
if got == want {
return true
} else {
@@ -107,18 +107,20 @@ func TestGetCharEventKey(t *testing.T) {
{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.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 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.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{CtrlBackspace, 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, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlBackspace, 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, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,9 +128,41 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl}, wantKey{CtrlUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl}, wantKey{CtrlDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl}, wantKey{CtrlRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl}, wantKey{CtrlLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModShift}, wantKey{ShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModShift}, wantKey{ShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift}, wantKey{ShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModAlt}, wantKey{AltUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModAlt}, wantKey{AltRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModAlt}, wantKey{AltLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftLeft, 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
@@ -137,6 +171,46 @@ func TestGetCharEventKey(t *testing.T) {
// 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}},
{giveKey{tcell.KeyHome, 0, tcell.ModNone}, wantKey{Home, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModNone}, wantKey{End, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModNone}, wantKey{PageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModNone}, wantKey{PageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl}, wantKey{CtrlHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl}, wantKey{CtrlEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl}, wantKey{CtrlDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl}, wantKey{CtrlPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl}, wantKey{CtrlPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModShift}, wantKey{ShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModShift}, wantKey{ShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModShift}, wantKey{ShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift}, wantKey{ShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift}, wantKey{ShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModAlt}, wantKey{AltHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModAlt}, wantKey{AltEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModAlt}, wantKey{AltPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModAlt}, wantKey{AltPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageDown, 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
@@ -179,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
}
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
r.Init()
// run and evaluate the tests
@@ -191,18 +265,22 @@ func TestGetCharEventKey(t *testing.T) {
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()
gotEvent := r.GetChar(true)
// 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()
gotEvent = r.GetChar(true)
}
if gotEvent.Type == Resize {
t.Logf("Resize swallowed")
gotEvent = r.GetChar(true)
}
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)
assert(t, "r.GetChar(true).Type", gotEvent.Type, test.wantKey.Type)
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
}
r.Close()

View File

@@ -44,11 +44,11 @@ func ttyname() string {
}
// TtyIn returns terminal device to read user input
func TtyIn() (*os.File, error) {
return openTtyIn()
func TtyIn(ttyDefault string) (*os.File, error) {
return openTtyIn(ttyDefault)
}
// TtyIn returns terminal device to write to
func TtyOut() (*os.File, error) {
return openTtyOut()
// TtyOut returns terminal device to write to
func TtyOut(ttyDefault string) (*os.File, error) {
return openTtyOut(ttyDefault)
}

View File

@@ -11,11 +11,11 @@ func ttyname() string {
}
// TtyIn on Windows returns os.Stdin
func TtyIn() (*os.File, error) {
func TtyIn(ttyDefault string) (*os.File, error) {
return os.Stdin, nil
}
// TtyOut on Windows returns nil
func TtyOut() (*os.File, error) {
func TtyOut(ttyDefault string) (*os.File, error) {
return nil, nil
}

View File

@@ -2,12 +2,52 @@ package tui
import (
"strconv"
"strings"
"time"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
)
type Attr int32
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Strip = Attr(1 << 12)
// Underline style stored in bits 13-15 (3 bits, values 0-4)
// Only meaningful when the Underline attribute bit is also set.
// 0 = solid (default)
UnderlineStyleShift = 13
UnderlineStyleMask = Attr(0b111 << UnderlineStyleShift)
UlStyleDouble = Attr(0b001 << UnderlineStyleShift)
UlStyleCurly = Attr(0b010 << UnderlineStyleShift)
UlStyleDotted = Attr(0b011 << UnderlineStyleShift)
UlStyleDashed = Attr(0b100 << UnderlineStyleShift)
)
func (a Attr) UnderlineStyle() Attr {
return a & UnderlineStyleMask
}
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return (b &^ AttrRegular) | (a & BoldForce)
}
merged := (a &^ AttrRegular) | b
// When b sets Underline, use b's underline style instead of OR'ing
if b&Underline > 0 {
merged = (merged &^ UnderlineStyleMask) | (b & UnderlineStyleMask)
}
return merged
}
// Types of user action
//
//go:generate stringer -type=EventType
@@ -44,7 +84,6 @@ const (
CtrlZ
Esc
CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash
@@ -72,6 +111,10 @@ const (
ShiftLeft
ShiftRight
ShiftDelete
ShiftHome
ShiftEnd
ShiftPageUp
ShiftPageDown
F1
F2
@@ -92,17 +135,71 @@ const (
AltDown
AltLeft
AltRight
AltDelete
AltHome
AltEnd
AltPageUp
AltPageDown
AltShiftUp
AltShiftDown
AltShiftLeft
AltShiftRight
AltShiftDelete
AltShiftHome
AltShiftEnd
AltShiftPageUp
AltShiftPageDown
CtrlUp
CtrlDown
CtrlLeft
CtrlRight
CtrlHome
CtrlEnd
CtrlBackspace
CtrlDelete
CtrlPageUp
CtrlPageDown
Alt
CtrlAlt
CtrlAltUp
CtrlAltDown
CtrlAltLeft
CtrlAltRight
CtrlAltHome
CtrlAltEnd
CtrlAltBackspace
CtrlAltDelete
CtrlAltPageUp
CtrlAltPageDown
CtrlShiftUp
CtrlShiftDown
CtrlShiftLeft
CtrlShiftRight
CtrlShiftHome
CtrlShiftEnd
CtrlShiftDelete
CtrlShiftPageUp
CtrlShiftPageDown
CtrlAltShiftUp
CtrlAltShiftDown
CtrlAltShiftLeft
CtrlAltShiftRight
CtrlAltShiftHome
CtrlAltShiftEnd
CtrlAltShiftDelete
CtrlAltShiftPageUp
CtrlAltShiftPageDown
Invalid
Fatal
BracketedPasteBegin
BracketedPasteEnd
Mouse
DoubleClick
@@ -130,6 +227,8 @@ const (
Jump
JumpCancel
ClickHeader
ClickFooter
Multi
)
func (t EventType) AsEvent() Event {
@@ -216,6 +315,14 @@ func (a ColorAttr) IsColorDefined() bool {
return a.Color != colUndefined
}
func (a ColorAttr) IsAttrDefined() bool {
return a.Attr&^BoldForce != AttrUndefined
}
func (a ColorAttr) IsUndefined() bool {
return !a.IsColorDefined() && !a.IsAttrDefined()
}
func NewColorAttr() ColorAttr {
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
}
@@ -244,6 +351,14 @@ const (
colMagenta
colCyan
colWhite
colGrey
colBrightRed
colBrightGreen
colBrightYellow
colBrightBlue
colBrightMagenta
colBrightCyan
colBrightWhite
)
type FillReturn int
@@ -257,6 +372,7 @@ const (
type ColorPair struct {
fg Color
bg Color
ul Color
attr Attr
}
@@ -268,7 +384,11 @@ func HexToColor(rrggbb string) Color {
}
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
return ColorPair{fg, bg, attr}
return ColorPair{fg, bg, colDefault, attr}
}
func NoColorPair() ColorPair {
return ColorPair{-1, -1, -1, 0}
}
func (p ColorPair) Fg() Color {
@@ -279,10 +399,28 @@ func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) Ul() Color {
return p.ul
}
func (p ColorPair) WithUl(ul Color) ColorPair {
dup := p
dup.ul = ul
return dup
}
func (p ColorPair) Attr() Attr {
return p.attr
}
func (p ColorPair) IsFullBgMarker() bool {
return p.attr&FullBg > 0
}
func (p ColorPair) ShouldStripColors() bool {
return p.attr&Strip > 0
}
func (p ColorPair) HasBg() bool {
return p.attr&Reverse == 0 && p.bg != colDefault ||
p.attr&Reverse > 0 && p.fg != colDefault
@@ -297,6 +435,9 @@ func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
if other.bg != except {
dup.bg = other.bg
}
if other.ul != except {
dup.ul = other.ul
}
return dup
}
@@ -306,6 +447,18 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
return dup
}
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
dup := p
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
return dup.Merge(fgPair)
}
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
dup := p
bgPair := ColorPair{colUndefined, bg.Color, colUndefined, bg.Attr}
return dup.Merge(bgPair)
}
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
return p.WithAttr(other.attr)
}
@@ -321,12 +474,15 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
type ColorTheme struct {
Colored bool
Input ColorAttr
Ghost ColorAttr
Disabled ColorAttr
Fg ColorAttr
Bg ColorAttr
ListFg ColorAttr
ListBg ColorAttr
AltBg ColorAttr
Nth ColorAttr
Nomatch ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
@@ -334,6 +490,7 @@ type ColorTheme struct {
PreviewBg ColorAttr
DarkBg ColorAttr
Gutter ColorAttr
AltGutter ColorAttr
Prompt ColorAttr
InputBg ColorAttr
InputBorder ColorAttr
@@ -349,6 +506,10 @@ type ColorTheme struct {
HeaderBg ColorAttr
HeaderBorder ColorAttr
HeaderLabel ColorAttr
Footer ColorAttr
FooterBg ColorAttr
FooterBorder ColorAttr
FooterLabel ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
Border ColorAttr
@@ -481,7 +642,7 @@ type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if shape == BorderNone || shape == BorderPhantom {
return BorderStyle{
shape: shape,
shape: BorderNone,
top: ' ',
bottom: ' ',
left: ' ',
@@ -602,6 +763,7 @@ const (
WindowPreview
WindowInput
WindowHeader
WindowFooter
)
type Renderer interface {
@@ -621,7 +783,8 @@ type Renderer interface {
HideCursor()
ShowCursor()
GetChar() Event
GetChar(cancellable bool) Event
CancelGetChar()
Top() int
MaxX() int
@@ -654,7 +817,7 @@ type Window interface {
Print(text string)
CPrint(color ColorPair, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn
LinkBegin(uri string, params string)
LinkEnd()
Erase()
@@ -667,16 +830,18 @@ type FullscreenRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
tabstop int
prevDownTime time.Time
clicks [][2]int
showCursor bool
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int) Renderer {
r := &FullscreenRenderer{
theme: theme,
mouse: mouse,
forceBlack: forceBlack,
tabstop: tabstop,
prevDownTime: time.Unix(0, 0),
clicks: [][2]int{},
showCursor: true}
@@ -684,17 +849,23 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
}
var (
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
NoColorTheme *ColorTheme
EmptyTheme *ColorTheme
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
ColPrompt ColorPair
ColNormal ColorPair
ColInput ColorPair
ColDisabled ColorPair
ColGhost ColorPair
ColMatch ColorPair
ColCursor ColorPair
ColCursorEmpty ColorPair
ColCursorEmptyChar ColorPair
ColAltCursorEmpty ColorPair
ColAltCursorEmptyChar ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
@@ -709,6 +880,9 @@ var (
ColHeader ColorPair
ColHeaderBorder ColorPair
ColHeaderLabel ColorPair
ColFooter ColorPair
ColFooterBorder ColorPair
ColFooterLabel ColorPair
ColSeparator ColorPair
ColScrollbar ColorPair
ColGapLine ColorPair
@@ -725,146 +899,173 @@ var (
ColInputLabel ColorPair
)
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined},
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: 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},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
func NoColorTheme() *ColorTheme {
return &ColorTheme{
Colored: false,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colDefault, AttrUndefined},
ListBg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
ListLabel: ColorAttr{colDefault, AttrUndefined},
ListBorder: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
InputBg: ColorAttr{colDefault, AttrUndefined},
InputBorder: ColorAttr{colDefault, AttrUndefined},
InputLabel: ColorAttr{colDefault, AttrUndefined},
HeaderBg: ColorAttr{colDefault, AttrUndefined},
HeaderBorder: ColorAttr{colDefault, AttrUndefined},
HeaderLabel: ColorAttr{colDefault, AttrUndefined},
GapLine: ColorAttr{colDefault, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
func init() {
defaultColor := ColorAttr{colDefault, AttrUndefined}
undefined := ColorAttr{colUndefined, AttrUndefined}
NoColorTheme = &ColorTheme{
Colored: false,
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: defaultColor,
ListBg: defaultColor,
AltBg: undefined,
SelectedFg: defaultColor,
SelectedBg: defaultColor,
SelectedMatch: defaultColor,
DarkBg: defaultColor,
Prompt: defaultColor,
Match: defaultColor,
Current: undefined,
CurrentMatch: undefined,
Spinner: defaultColor,
Info: defaultColor,
Cursor: defaultColor,
Marker: defaultColor,
Header: defaultColor,
Border: undefined,
BorderLabel: defaultColor,
Ghost: undefined,
Disabled: defaultColor,
PreviewFg: defaultColor,
PreviewBg: defaultColor,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: defaultColor,
PreviewScrollbar: defaultColor,
PreviewLabel: defaultColor,
ListLabel: defaultColor,
ListBorder: defaultColor,
Separator: defaultColor,
Scrollbar: defaultColor,
InputBg: defaultColor,
InputBorder: defaultColor,
InputLabel: defaultColor,
HeaderBg: defaultColor,
HeaderBorder: defaultColor,
HeaderLabel: defaultColor,
FooterBg: defaultColor,
FooterBorder: defaultColor,
FooterLabel: defaultColor,
GapLine: defaultColor,
Nth: undefined,
Nomatch: undefined,
}
EmptyTheme = &ColorTheme{
Colored: true,
Input: undefined,
Fg: undefined,
Bg: undefined,
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: undefined,
Prompt: undefined,
Match: undefined,
Current: undefined,
CurrentMatch: undefined,
Spinner: undefined,
Info: undefined,
Cursor: undefined,
Marker: undefined,
Header: undefined,
Footer: undefined,
Border: undefined,
BorderLabel: undefined,
ListLabel: undefined,
ListBorder: undefined,
Ghost: undefined,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
Separator: undefined,
Scrollbar: undefined,
InputBg: undefined,
InputBorder: undefined,
InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
}
Default16 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined},
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: ColorAttr{colGrey, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined},
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
Current: ColorAttr{colBrightWhite, AttrUndefined},
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined},
Info: ColorAttr{colYellow, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, 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},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
Footer: ColorAttr{colCyan, AttrUndefined},
Border: undefined,
BorderLabel: defaultColor,
Ghost: undefined,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
ListLabel: undefined,
ListBorder: undefined,
Separator: undefined,
Scrollbar: undefined,
InputBg: undefined,
InputBorder: undefined,
InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
}
Dark256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined},
@@ -875,35 +1076,47 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, 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},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
Ghost: undefined,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
ListLabel: undefined,
ListBorder: undefined,
Separator: undefined,
Scrollbar: undefined,
InputBg: undefined,
InputBorder: undefined,
InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
}
Light256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined},
@@ -914,35 +1127,58 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Footer: ColorAttr{31, 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},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
Ghost: undefined,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
ListLabel: undefined,
ListBorder: undefined,
Separator: undefined,
Scrollbar: undefined,
InputBg: undefined,
InputBorder: undefined,
InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
}
}
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
if forceBlack {
theme.Bg = ColorAttr{colBlack, AttrUndefined}
}
if boldify {
boldify := func(c ColorAttr) ColorAttr {
dup := c
if (c.Attr & AttrRegular) == 0 {
dup.Attr |= BoldForce
}
return dup
}
theme.Current = boldify(theme.Current)
theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner)
}
o := func(a ColorAttr, b ColorAttr) ColorAttr {
c := a
if b.Color != colUndefined {
@@ -958,17 +1194,36 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
match := theme.Match
if !baseTheme.Colored && match.IsUndefined() {
match.Attr = Underline
}
theme.Match = o(baseTheme.Match, match)
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
theme.Current = theme.Fg.Merge(o(baseTheme.Current, theme.Current))
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
current := theme.Current
if !baseTheme.Colored && current.IsUndefined() {
current.Attr |= Reverse
}
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
currentMatch := theme.CurrentMatch
if !baseTheme.Colored && currentMatch.IsUndefined() {
currentMatch.Attr |= Reverse | Underline
}
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
theme.Footer = o(baseTheme.Footer, theme.Footer)
// If border color is undefined, set it to default color with dim attribute.
border := theme.Border
if baseTheme.Border.IsUndefined() && border.IsUndefined() {
border.Attr = Dim
}
theme.Border = o(baseTheme.Border, border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
undefined := NewColorAttr()
@@ -981,8 +1236,24 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
ghost := theme.Ghost
if ghost.IsUndefined() {
ghost.Attr = Dim
} else if ghost.IsColorDefined() && !ghost.IsAttrDefined() {
// Don't want to inherit 'bold' from 'input'
ghost.Attr = AttrRegular
}
theme.Ghost = o(theme.Input, ghost)
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
// Use dim gutter on non-colored themes if undefined
gutter := theme.Gutter
if !baseTheme.Colored && gutter.IsUndefined() {
gutter.Attr = Dim
}
theme.Gutter = o(theme.DarkBg, gutter)
theme.AltGutter = o(theme.Gutter, theme.AltGutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
@@ -1020,6 +1291,14 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
theme.FooterBg = o(theme.Bg, theme.FooterBg)
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
if theme.Nomatch.IsUndefined() {
theme.Nomatch.Attr = Dim
}
initPalette(theme)
}
@@ -1028,7 +1307,7 @@ func initPalette(theme *ColorTheme) {
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
bg.Color = colDefault
}
return ColorPair{fg.Color, bg.Color, fg.Attr}
return ColorPair{fg.Color, bg.Color, colDefault, fg.Attr}
}
blank := theme.ListFg
blank.Attr = AttrRegular
@@ -1037,15 +1316,19 @@ func initPalette(theme *ColorTheme) {
ColNormal = pair(theme.ListFg, theme.ListBg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.InputBg)
ColDisabled = pair(theme.Disabled, theme.ListBg)
ColGhost = pair(theme.Ghost, theme.InputBg)
ColDisabled = pair(theme.Disabled, theme.InputBg)
ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltCursorEmpty = pair(blank, theme.AltGutter)
ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
ColMarker = pair(theme.Marker, theme.ListBg)
}
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
@@ -1072,8 +1355,54 @@ func initPalette(theme *ColorTheme) {
ColHeader = pair(theme.Header, theme.HeaderBg)
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg)
ColFooter = pair(theme.Footer, theme.FooterBg)
ColFooterBorder = pair(theme.FooterBorder, theme.FooterBg)
ColFooterLabel = pair(theme.FooterLabel, theme.FooterBg)
}
func runeWidth(r rune) int {
return uniseg.StringWidth(string(r))
}
// WrappedLine represents a single visual line after character-level wrapping.
type WrappedLine struct {
Text string
DisplayWidth int
}
// WrapLine splits a single line (no embedded \n) into visual lines
// that fit within initialMax columns. Character-level wrapping only.
func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []WrappedLine {
lines := []WrappedLine{}
width := 0
line := ""
gr := uniseg.NewGraphemes(input)
maxWidth := initialMax
contMax := max(1, initialMax-wrapSignWidth)
for gr.Next() {
rs := gr.Runes()
str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = strings.Repeat(" ", w)
} else if rs[0] == '\r' {
w++
} else {
w = uniseg.StringWidth(str)
}
width += w
if prefixLength+width <= maxWidth {
line += str
} else {
lines = append(lines, WrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = w
maxWidth = contMax
}
}
lines = append(lines, WrappedLine{string(line), width})
return lines
}

View File

@@ -2,6 +2,46 @@ package tui
import "testing"
func TestWrapLine(t *testing.T) {
// Basic wrapping
lines := WrapLine("hello world", 0, 7, 8, 2)
if len(lines) != 2 || lines[0].Text != "hello w" || lines[1].Text != "orld" {
t.Errorf("Basic wrap: %v", lines)
}
// Exact fit — no wrapping needed
lines = WrapLine("hello", 0, 5, 8, 2)
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
t.Errorf("Exact fit: %v", lines)
}
// With prefix length
lines = WrapLine("hello", 3, 5, 8, 2)
if len(lines) != 2 || lines[0].Text != "he" || lines[1].Text != "llo" {
t.Errorf("Prefix length: %v", lines)
}
// Empty string
lines = WrapLine("", 0, 10, 8, 2)
if len(lines) != 1 || lines[0].Text != "" || lines[0].DisplayWidth != 0 {
t.Errorf("Empty string: %v", lines)
}
// Continuation lines account for wrapSignWidth
lines = WrapLine("abcdefghij", 0, 5, 8, 2)
// First line: "abcde" (5 chars fit in width 5)
// Continuation max: 5-2=3, so "fgh" then "ij"
if len(lines) != 3 || lines[0].Text != "abcde" || lines[1].Text != "fgh" || lines[2].Text != "ij" {
t.Errorf("Continuation: %v", lines)
}
// Tab expansion
lines = WrapLine("\there", 0, 10, 4, 2)
if len(lines) != 1 || lines[0].DisplayWidth != 8 {
t.Errorf("Tab: %v", lines)
}
}
func TestHexToColor(t *testing.T) {
assert := func(expr string, r, g, b int) {
color := HexToColor(expr)

View File

@@ -8,7 +8,7 @@ import (
func TestAtExit(t *testing.T) {
want := []int{3, 2, 1, 0}
var called []int
for i := 0; i < 4; i++ {
for i := range 4 {
n := i
AtExit(func() { called = append(called, n) })
}

View File

@@ -52,7 +52,7 @@ func ToChars(bytes []byte) Chars {
}
runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ {
for i := range bytesUntil {
runes[i] = rune(bytes[i])
}
for i := bytesUntil; i < len(bytes); {
@@ -184,9 +184,10 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces
}
func (chars *Chars) TrimTrailingWhitespaces() {
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
end := len(chars.slice) - whitespaces
chars.slice = chars.slice[0:max(end, maxIndex)]
}
func (chars *Chars) TrimSuffix(runes []rune) {
@@ -248,7 +249,7 @@ func (chars *Chars) Prepend(prefix string) {
}
}
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, wrapWord bool) ([][]rune, bool) {
text := make([]rune, chars.Length())
copy(text, chars.ToRunes())
@@ -258,7 +259,7 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
lines = append(lines, text)
} else {
from := 0
for off := 0; off < len(text); off++ {
for off := range text {
if text[off] == '\n' {
lines = append(lines, text[from:off+1]) // Include '\n'
from = off + 1
@@ -294,9 +295,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
line = line[:len(line)-1]
}
hasWrapSign := false
for {
cols := wrapCols
if len(wrapped) > 0 {
if hasWrapSign {
cols -= wrapSignWidth
}
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
@@ -305,13 +307,28 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
if overflowIdx == 0 {
overflowIdx = 1
}
if wrapWord {
// Find last space/tab at or before overflowIdx
breakIdx := -1
for k := overflowIdx; k > 0; k-- {
if line[k-1] == ' ' || line[k-1] == '\t' {
breakIdx = k
break
}
}
if breakIdx > 0 {
overflowIdx = breakIdx
}
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line[:overflowIdx])
hasWrapSign = true
line = line[overflowIdx:]
continue
}
hasWrapSign = false
// Restore trailing '\n'
if newline {

View File

@@ -51,7 +51,7 @@ func TestTrimLength(t *testing.T) {
func TestCharsLines(t *testing.T) {
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop, false)
fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
@@ -76,8 +76,56 @@ func TestCharsLines(t *testing.T) {
check(true, 100, 3, 1, 1, 8, false)
// With wrap sign (3 + 2)
check(true, 100, 3, 2, 1, 12, false)
check(true, 100, 3, 2, 1, 10, false)
// With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false)
}
func TestCharsLinesWrapWord(t *testing.T) {
// "hello world foo bar" with width 12 should break at word boundaries
chars := ToChars([]byte("hello world foo bar"))
lines, overflow := chars.Lines(false, 100, 12, 0, 8, true)
// "hello world " (12) | "foo bar" (7)
if len(lines) != 2 || overflow {
t.Errorf("Expected 2 lines, got %d (overflow: %v): %v", len(lines), overflow, lines)
}
if string(lines[0]) != "hello world " {
t.Errorf("Expected first line 'hello world ', got %q", string(lines[0]))
}
if string(lines[1]) != "foo bar" {
t.Errorf("Expected second line 'foo bar', got %q", string(lines[1]))
}
// No word boundary: a single long word falls back to character wrap
chars2 := ToChars([]byte("abcdefghijklmnop"))
lines2, _ := chars2.Lines(false, 100, 10, 0, 8, true)
if len(lines2) != 2 {
t.Errorf("Expected 2 lines for long word, got %d: %v", len(lines2), lines2)
}
if string(lines2[0]) != "abcdefghij" {
t.Errorf("Expected first line 'abcdefghij', got %q", string(lines2[0]))
}
// Tab as word boundary
chars3 := ToChars([]byte("hello\tworld"))
lines3, _ := chars3.Lines(false, 100, 7, 0, 8, true)
// "hello\t" should break at tab (width of tab at pos 5 with tabstop 8 = 3, total width = 8 > 7)
// Actually RunesWidth: 'h'=1,'e'=1,'l'=1,'l'=1,'o'=1,'\t'=3 = 8 > 7, overflowIdx=5
// Then word-wrap scans back and finds no space/tab before idx 5 (tab IS at idx 5 but we check line[k-1])
// Wait - let me think: overflowIdx=5, we check k=5 -> line[4]='o', k=4 -> line[3]='l'... no space/tab found
// Falls back to character wrap: "hello" | "\tworld"
if len(lines3) < 2 {
t.Errorf("Expected at least 2 lines for tab test, got %d: %v", len(lines3), lines3)
}
// wrapWord=false still character-wraps
chars4 := ToChars([]byte("hello world"))
lines4, _ := chars4.Lines(false, 100, 8, 0, 8, false)
if len(lines4) != 2 {
t.Errorf("Expected 2 lines with wrapWord=false, got %d: %v", len(lines4), lines4)
}
if string(lines4[0]) != "hello wo" {
t.Errorf("Expected first line 'hello wo', got %q", string(lines4[0]))
}
}

View File

@@ -0,0 +1,39 @@
package util
import "sync"
// ConcurrentSet is a thread-safe set implementation.
type ConcurrentSet[T comparable] struct {
lock sync.RWMutex
items map[T]struct{}
}
// NewConcurrentSet creates a new ConcurrentSet.
func NewConcurrentSet[T comparable]() *ConcurrentSet[T] {
return &ConcurrentSet[T]{
items: make(map[T]struct{}),
}
}
// Add adds an item to the set.
func (s *ConcurrentSet[T]) Add(item T) {
s.lock.Lock()
defer s.lock.Unlock()
s.items[item] = struct{}{}
}
// Remove removes an item from the set.
func (s *ConcurrentSet[T]) Remove(item T) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.items, item)
}
// ForEach iterates over each item in the set and applies the provided function.
func (s *ConcurrentSet[T]) ForEach(fn func(item T)) {
s.lock.RLock()
defer s.lock.RUnlock()
for item := range s.items {
fn(item)
}
}

View File

@@ -6,7 +6,7 @@ import "sync"
type EventType int
// Events is a type that associates EventType to any data
type Events map[EventType]interface{}
type Events map[EventType]any
// EventBox is used for coordinating events
type EventBox struct {
@@ -36,7 +36,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
}
// Set turns on the event type on the box
func (b *EventBox) Set(event EventType, value interface{}) {
func (b *EventBox) Set(event EventType, value any) {
b.cond.L.Lock()
b.events[event] = value
if _, found := b.ignore[event]; !found {

View File

@@ -1,11 +1,11 @@
package util
import (
"cmp"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/mattn/go-isatty"
"github.com/rivo/uniseg"
@@ -18,8 +18,13 @@ func StringWidth(s string) int {
// RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
return StringsWidth(string(runes), prefixWidth, tabstop, limit)
}
// StringsWidth returns the width of the string
func StringsWidth(str string, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0
gr := uniseg.NewGraphemes(string(runes))
gr := uniseg.NewGraphemes(str)
idx := 0
for gr.Next() {
rs := gr.Runes()
@@ -55,66 +60,8 @@ func Truncate(input string, limit int) ([]rune, int) {
return runes, width
}
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {
return first
}
return second
}
// Max16 returns the largest integer
func Max16(first int16, second int16) int16 {
if first >= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
}
// Min returns the smallest integer
func Min(first int, second int) int {
if first <= second {
return first
}
return second
}
// Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
}
return second
}
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 {
if val < min {
return min
}
if val > max {
return max
}
return val
}
// Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int {
if val < min {
return min
}
if val > max {
return max
}
return val
func Constrain[T cmp.Ordered](val, minimum, maximum T) T {
return max(min(val, maximum), minimum)
}
func AsUint16(val int) uint16 {
@@ -126,18 +73,6 @@ func AsUint16(val int) uint16 {
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration {
if val < min {
return min
}
if val > max {
return max
}
return val
}
// IsTty returns true if the file is a terminal
func IsTty(file *os.File) bool {
fd := file.Fd()
@@ -209,7 +144,7 @@ func CompareVersions(v1, v2 string) int {
return n
}
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
for i := 0; i < max(len(parts1), len(parts2)); i++ {
var p1, p2 int
if i < len(parts1) {
p1 = atoi(parts1[i])

View File

@@ -4,72 +4,8 @@ import (
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 {
t.Error("Expected", 5)
}
}
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 {
t.Error("Expected", -1)
@@ -83,22 +19,6 @@ func TestConstrain(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)
@@ -120,18 +40,6 @@ func TestAsUint16(t *testing.T) {
}
}
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) {
o := Once(false)
if o() {

17
test/lib/common.fish Normal file
View File

@@ -0,0 +1,17 @@
# Unset fzf variables
set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_TMUX_OPTS
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
set -e FZF_API_KEY
# Unset completion-specific variables
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
set -gx FZF_COMPLETION_TRIGGER '++'
set -gx fish_history fzf_test
# Add fzf to PATH
fish_add_path <%= BASE %>/bin
# Source key bindings and completion
source "<%= BASE %>/shell/key-bindings.fish"
source "<%= BASE %>/shell/completion.fish"

View File

@@ -11,6 +11,7 @@ require 'net/http'
require 'json'
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
FISH_TEMPLATE = File.read(File.expand_path('common.fish', __dir__))
UNSETS = %w[
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
FZF_TMUX FZF_TMUX_OPTS
@@ -24,7 +25,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__)
BASE = File.expand_path('../..', __dir__)
Dir.chdir(BASE)
FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf".freeze
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
def wait(timeout = DEFAULT_TIMEOUT)
since = Time.now
@@ -66,7 +67,16 @@ class Shell
end
def fish
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
@fish ||=
begin
confdir = '/tmp/fzf-fish'
FileUtils.rm_rf(confdir)
FileUtils.mkdir_p("#{confdir}/fish/conf.d")
File.open("#{confdir}/fish/conf.d/fzf.fish", 'w') do |f|
f.puts ERB.new(FISH_TEMPLATE).result(binding)
end
"rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"
end
end
end
end
@@ -153,6 +163,7 @@ class Tmux
self.until(true) do |lines|
message = "Prepare[#{tries}]"
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
sleep(0.15)
lines[-1] == message
end
rescue Minitest::Assertion

View File

@@ -108,6 +108,40 @@ class TestCore < TestInteractive
assert_equal %w[3 2 5 6 8 7], fzf_output_lines
end
def test_subword_forward
tmux.send_keys "#{FZF} --bind K:kill-subword,F:forward-subword -q 'foo bar foo-bar fooFooBar'", :Enter, :Home
tmux.until { |lines| assert_equal '> foo bar foo-bar fooFooBar', lines.last }
tmux.send_keys 'F', :Delete
tmux.until { |lines| assert_equal '> foobar foo-bar fooFooBar', lines.last }
tmux.send_keys 'K'
tmux.until { |lines| assert_equal '> foo foo-bar fooFooBar', lines.last }
tmux.send_keys 'F', 'K'
tmux.until { |lines| assert_equal '> foo foo fooFooBar', lines.last }
tmux.send_keys 'F', 'F', 'K'
tmux.until { |lines| assert_equal '> foo foo fooFoo', lines.last }
end
def test_subword_backward
tmux.send_keys "#{FZF} --bind K:backward-kill-subword,B:backward-subword -q 'foo bar foo-bar fooBar'", :Enter
tmux.until { |lines| assert_equal '> foo bar foo-bar fooBar', lines.last }
tmux.send_keys 'B', :BSpace
tmux.until { |lines| assert_equal '> foo bar foo-bar foBar', lines.last }
tmux.send_keys 'K'
tmux.until { |lines| assert_equal '> foo bar foo-bar Bar', lines.last }
tmux.send_keys 'B', :BSpace
tmux.until { |lines| assert_equal '> foo bar foobar Bar', lines.last }
tmux.send_keys 'B', 'B', :BSpace
tmux.until { |lines| assert_equal '> foobar foobar Bar', lines.last }
end
def test_multi_max
tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter
@@ -1156,6 +1190,61 @@ class TestCore < TestInteractive
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
end
def test_freeze_left_keep_right
tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
tmux.send_keys '5'
tmux.until { |lines| assert_match(/^> 1␊2␊3␊4␊5␊.*XX$/, lines[-3]) }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('> 1') }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
end
def test_freeze_left_and_right
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX), :Enter
tmux.until { |lines| assert_match(/XX9998 9999 10000$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 2 3XX.*XX9998 9999 10000$/, lines[-3]) }
end
def test_freeze_left_and_right_delimiter
tmux.send_keys %(seq 10000 | tr "\n" ' ' | sed 's/ / , /g' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX --delimiter ' , '), :Enter
tmux.until { |lines| assert_match(/XX, 9999 , 10000 ,$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 , 2 , 3 ,XX.*XX, 9999 , 10000 ,$/, lines[-3]) }
end
def test_freeze_right_exceed_range
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-right 100000 --ellipsis XX), :Enter
['', "'1000"].each do |query|
tmux.send_keys query
tmux.until { |lines| assert lines.any_include?("> #{query}".strip) }
tmux.until do |lines|
assert_match(/ 9998 9999 10000$/, lines[-3])
assert_equal(1, lines[-3].scan('XX').size)
end
end
end
def test_freeze_right_exceed_range_with_freeze_left
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 100000 --ellipsis XX), :Enter
tmux.until do |lines|
assert_match(/^> 1 2 3XX.*9998 9999 10000$/, lines[-3])
assert_equal(1, lines[-3].scan('XX').size)
end
end
def test_freeze_right_with_ellipsis_and_scrolling
tmux.send_keys "{ seq 6; ruby -e 'print \"g\"*1000, \"\\n\"'; seq 8 100; } | #{FZF} --ellipsis='777' --freeze-right 1 --scroll-off 0 --bind a:offset-up", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys(*Array.new(6) { :a })
tmux.until do |lines|
assert_match(/> 777g+$/, lines[-3])
assert_equal(1, lines.count { |l| l.end_with?('g') })
end
end
def test_backward_eof
tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter
tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 }
@@ -1415,6 +1504,11 @@ class TestCore < TestInteractive
tmux.until { assert_match(%r{ --1/10000/10000-- *$}, it[-1]) }
end
def test_info_command_inline_right_no_ansi
tmux.send_keys(%(seq 10000 | #{FZF} --info-command 'echo -e "--$FZF_POS/$FZF_INFO--"' --info inline-right), :Enter)
tmux.until { assert_match(%r{ --1/10000/10000-- *$}, it[-1]) }
end
def test_info_command_and_focus
tmux.send_keys(%(seq 100 | #{FZF} --separator x --info-command 'echo $FZF_POS' --bind focus:clear-query), :Enter)
tmux.until { assert_match(/^ 1 xx/, it[-2]) }
@@ -1494,14 +1588,16 @@ class TestCore < TestInteractive
end
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.send_keys "seq 1000 | #{FZF} --pointer x --query 555 --bind t:track,T:up+track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, 'x 555'
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, 'x 55'
assert_includes lines, '> 55'
end
tmux.send_keys :t
@@ -1511,7 +1607,8 @@ class TestCore < TestInteractive
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 55'
assert_includes lines, 'x 55'
assert_includes lines, '> 5'
end
# Automatically disabled when the tracking item is no longer visible
@@ -1523,16 +1620,33 @@ class TestCore < TestInteractive
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, 'x 52'
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+t'
end
# Automatically disabled when the focus has moved
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, 'x 53'
refute_includes lines[-2], '+t'
end
# Should work even when combined with a focus moving actions
tmux.send_keys 'T'
tmux.until do |lines|
assert_includes lines, 'x 54'
assert_includes lines[-2], '+t'
end
tmux.send_keys 'T'
tmux.until do |lines|
assert_includes lines, 'x 55'
assert_includes lines[-2], '+t'
end
end
def test_one_and_zero
@@ -1632,15 +1746,18 @@ class TestCore < TestInteractive
end
def test_env_vars
def to_vars(lines)
lines.select { it.start_with?('FZF_') }.to_h do
key, val = it.split('=', 2)
def env_vars
return {} unless File.exist?(tempname)
File.readlines(tempname).select { it.start_with?('FZF_') }.to_h do
key, val = it.chomp.split('=', 2)
[key.to_sym, val]
end
end
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window up,99%,noborder --preview 'env | grep ^FZF_ | sort' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
expected = {
FZF_DIRECTION: 'down',
FZF_TOTAL_COUNT: '100',
FZF_MATCH_COUNT: '100',
FZF_SELECT_COUNT: '0',
@@ -1648,31 +1765,32 @@ class TestCore < TestInteractive
FZF_KEY: '',
FZF_POS: '1',
FZF_QUERY: '',
FZF_PROMPT: '>',
FZF_POINTER: '>',
FZF_PROMPT: '> ',
FZF_INPUT_STATE: 'hidden'
}
tmux.until do |lines|
assert_equal expected, to_vars(lines).slice(*expected.keys)
tmux.until do
assert_equal expected, env_vars.slice(*expected.keys)
end
tmux.send_keys :Enter
tmux.until do |lines|
tmux.until do
expected.merge!(FZF_INPUT_STATE: 'enabled', FZF_ACTION: 'show-input', FZF_KEY: 'enter')
assert_equal expected, to_vars(lines).slice(*expected.keys)
assert_equal expected, env_vars.slice(*expected.keys)
end
tmux.send_keys :Tab, :Tab
tmux.until do |lines|
tmux.until do
expected.merge!(FZF_ACTION: 'toggle-down', FZF_KEY: 'tab', FZF_POS: '3', FZF_SELECT_COUNT: '2')
assert_equal expected, to_vars(lines).slice(*expected.keys)
assert_equal expected, env_vars.slice(*expected.keys)
end
tmux.send_keys '99'
tmux.until do |lines|
tmux.until do
expected.merge!(FZF_ACTION: 'char', FZF_KEY: '9', FZF_QUERY: '99', FZF_MATCH_COUNT: '1', FZF_POS: '1')
assert_equal expected, to_vars(lines).slice(*expected.keys)
assert_equal expected, env_vars.slice(*expected.keys)
end
tmux.send_keys :Space
tmux.until do |lines|
tmux.until do
expected.merge!(FZF_INPUT_STATE: 'disabled', FZF_ACTION: 'disable-search', FZF_KEY: 'space')
assert_equal expected, to_vars(lines).slice(*expected.keys)
assert_equal expected, env_vars.slice(*expected.keys)
end
end
@@ -1813,4 +1931,257 @@ class TestCore < TestInteractive
assert_equal ['[0] 1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
end
end
def test_ghost
tmux.send_keys %(seq 100 | #{FZF} --prompt 'X ' --ghost 'Type in query ...' --bind 'space:change-ghost:Y Z' --bind 'enter:transform-ghost:echo Z Y'), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_includes lines, 'X Type in query ...'
end
tmux.send_keys '100'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, 'X 100'
end
tmux.send_keys 'C-u'
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_includes lines, 'X Type in query ...'
end
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, 'X Y Z' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, 'X Z Y' }
end
def test_ghost_inline
tmux.send_keys %(seq 100 | #{FZF} --info 'inline: Y' --no-separator --prompt 'X ' --ghost 'Type in query ...'), :Enter
tmux.until do |lines|
assert_includes lines, 'X Type in query ... Y100/100'
end
tmux.send_keys '100'
tmux.until do |lines|
assert_includes lines, 'X 100 Y1/100'
end
tmux.send_keys 'C-u'
tmux.until do |lines|
assert_includes lines, 'X Type in query ... Y100/100'
end
end
def test_offset_middle
tmux.send_keys %(seq 1000 | #{FZF} --sync --no-input --reverse --height 5 --scroll-off 0 --bind space:offset-middle), :Enter
line = nil
tmux.until { |lines| line = lines.index('> 1') }
tmux.send_keys :PgDn
tmux.until { |lines| assert_includes lines[line + 4], '> 5' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[line + 2], '> 5' }
end
def test_no_input_query
tmux.send_keys %(seq 1000 | #{FZF} --no-input --query 555 --bind space:toggle-input), :Enter
tmux.until { |lines| assert_includes lines, '> 555' }
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
end
def test_no_input_change_query
tmux.send_keys %(seq 1000 | #{FZF} --multi --query 999 --no-input --bind 'enter:show-input+change-query(555)+hide-input,space:change-query(555)+select'), :Enter
tmux.until { |lines| assert_includes lines, '> 999' }
tmux.send_keys :Space
tmux.until do |lines|
assert_includes lines, '>>999'
refute_includes lines, '> 555'
end
tmux.send_keys :Enter
tmux.until do |lines|
refute_includes lines, '>>999'
assert_includes lines, '> 555'
end
end
def test_search_override_query_in_no_input_mode
tmux.send_keys %(seq 1000 | #{FZF} --sync --no-input --bind 'enter:show-input+change-query(555)+hide-input+search(999),space:search(111)+show-input+change-query(777)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '> 999' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '> 777' }
end
def test_change_pointer
tmux.send_keys %(seq 2 | #{FZF} --bind 'a:change-pointer(a),b:change-pointer(bb),c:change-pointer(),d:change-pointer(ddd)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines, 'a 1' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines, 'bb 1' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines, ' 1' }
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines, 'ddd 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, ' 2' }
end
def test_transform_pointer
tmux.send_keys %(seq 2 | #{FZF} --bind 'a:transform-pointer(echo a),b:transform-pointer(echo bb),c:transform-pointer(),d:transform-pointer(echo ddd)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines, 'a 1' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines, 'bb 1' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines, ' 1' }
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines, 'ddd 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, ' 2' }
end
def test_change_header_on_header_window
tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter
tmux.until do |lines|
assert lines.any_include?('100/100')
assert lines.any_include?('foo')
end
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('bar') }
end
def test_trailing_new_line
tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter
tmux.until { |lines| assert_includes lines, '> foo␊' }
end
def test_async_transform
time = Time.now
tmux.send_keys %(
seq 100 | #{FZF} --style full --border --preview : \
--bind 'focus:bg-transform-header(sleep 0.5; echo th.)' \
--bind 'focus:+bg-transform-footer(sleep 0.5; echo tf.)' \
--bind 'focus:+bg-transform-border-label(sleep 0.5; echo tbl.)' \
--bind "focus:+bg-transform-preview-label(sleep 0.5; echo tpl.)" \
--bind 'focus:+bg-transform-input-label(sleep 0.5; echo til.)' \
--bind 'focus:+bg-transform-list-label(sleep 0.5; echo tll.)' \
--bind 'focus:+bg-transform-header-label(sleep 0.5; echo thl.)' \
--bind 'focus:+bg-transform-footer-label(sleep 0.5; echo tfl.)' \
--bind 'focus:+bg-transform-prompt(sleep 0.5; echo tp.)' \
--bind 'focus:+bg-transform-ghost(sleep 0.5; echo tg.)'
).strip, :Enter
tmux.until do |lines|
assert lines.any_include?('100/100')
%w[th tf tbl tpl til tll thl tfl tp tg].each do
assert lines.any_include?("#{it}.")
end
end
elapsed = Time.now - time
assert_operator elapsed, :<, 2
end
def test_bg_cancel
tmux.send_keys %(seq 0 1 | #{FZF} --bind 'space:bg-cancel+bg-transform-header(sleep {}; echo [{}])'), :Enter
tmux.until { assert_equal 2, it.match_count }
tmux.send_keys '1'
tmux.until { assert_equal 1, it.match_count }
tmux.send_keys :Space
tmux.send_keys :BSpace
tmux.until { assert_equal 2, it.match_count }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('[0]') }
sleep(2)
tmux.until do |lines|
assert lines.any_include?('[0]')
refute lines.any_include?('[1]')
end
end
def test_render_order
tmux.send_keys %(seq 100 | #{FZF} --bind='focus:preview(echo boom)+change-footer(bam)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { assert it.any_include?('boom') }
tmux.until { assert it.any_include?('bam') }
end
def test_multi_event
tmux.send_keys %(seq 100 | #{FZF} --multi --bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.send_keys :Tab
tmux.until { assert_equal 1, it.select_count }
tmux.until { assert it.any_include?('Selected 1 item(s)') }
tmux.send_keys :Tab
tmux.until { assert_equal 0, it.select_count }
tmux.until { refute it.any_include?('Selected') }
end
def test_preserve_selection_on_revision_bump
tmux.send_keys %(seq 100 | #{FZF} --multi --sync --query "'1" --bind 'a:select-all+change-header(pressed a),b:change-header(pressed b)+change-nth(1),c:exclude'), :Enter
tmux.until do
assert_equal 20, it.match_count
assert_equal 0, it.select_count
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
end
tmux.send_keys :b
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
refute it.any_include?('pressed a')
assert it.any_include?('pressed b')
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
refute it.any_include?('pressed b')
end
tmux.send_keys :c
tmux.until do
assert_equal 19, it.match_count
assert_equal 19, it.select_count
end
end
def test_trigger
tmux.send_keys %(seq 100 | #{FZF} --bind 'a:up+trigger(a),b:trigger(a,a,b,a)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :a
tmux.until { |lines| assert_includes lines, '> 3' }
tmux.send_keys :b
tmux.until { |lines| assert_includes lines, '> 9' }
end
def test_change_nth_unset_default
tmux.send_keys %(echo foo bar | #{FZF} --nth 2 --query fb --bind space:change-nth:), :Enter
tmux.until do
assert_equal 1, it.item_count
assert_equal 0, it.match_count
end
tmux.send_keys :Space
tmux.until do
assert_equal 1, it.item_count
assert_equal 1, it.match_count
end
end
def test_zero_width_characters
tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter
tmux.until do |lines|
assert_equal 981, lines.match_count
assert_match(/^> XX.*a̱500/, lines[-3])
assert(lines.reverse.drop(5).all? { it.match?(/^ XX.*a̱500.*XX/) })
end
end
end

View File

@@ -403,7 +403,7 @@ class TestExec < TestInteractive
end
def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
tmux.send_keys "seq 100 | fzf --bind 'enter:become:seq {} | fzf'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count }

View File

@@ -304,12 +304,26 @@ class TestFilter < TestBase
def test_boundary_match
# Underscore boundaries should be ranked lower
{
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
default: [' xyz '] + %w[/xyz/ [xyz] -xyz- -xyz_ _xyz- _xyz_],
path: ['/xyz/', ' xyz '] + %w[[xyz] -xyz- -xyz_ _xyz- _xyz_],
history: ['[xyz]', '-xyz-', ' xyz '] + %w[/xyz/ -xyz_ _xyz- _xyz_]
}.each do |scheme, expected|
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
result = `printf -- 'xxyzx\n-xxyz\nxyzx-\n_xyz_\n_xyz-\n-xyz_\n[xyz]\n-xyz-\n xyz \n/xyz/\n' | #{FZF} -f"'xyz'" --scheme=#{scheme}`.lines(chomp: true)
assert_equal expected, result
end
end
def test_accept_nth
# Single field selection
assert_equal 'three', `echo 'one two three' | #{FZF} -d' ' --with-nth 1 --accept-nth -1 -f one`.chomp
# Multiple field selection
writelines(['ID001:John:Developer', 'ID002:Jane:Manager', 'ID003:Bob:Designer'])
assert_equal 'ID001', `#{FZF} -d: --with-nth 2 --accept-nth 1 -f John < #{tempname}`.chomp
assert_equal 'ID002:Manager', `#{FZF} -d: --with-nth 2 --accept-nth 1,3 -f Jane < #{tempname}`.chomp
# Test with different delimiters
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
end
end

View File

@@ -39,11 +39,11 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
block = <<~OUTPUT
> 4
997/997
>
3
2
1
997/997
>
foobar
OUTPUT
tmux.until { assert_block(block, it) }
@@ -53,10 +53,10 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
block = <<~OUTPUT
foobar
> < 997/997
1
2
3
> < 997/997
> 4
OUTPUT
tmux.until { assert_block(block, it) }
@@ -148,10 +148,10 @@ class TestLayout < TestInteractive
4
> 3
2/2
>
2
1
2/2
>
foo
OUTPUT
@@ -178,8 +178,8 @@ class TestLayout < TestInteractive
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
expected = <<~OUTPUT
3
2
3
2
> 1
> < 3/3
@@ -197,8 +197,8 @@ class TestLayout < TestInteractive
3
2
3
2
> 1
> < 3/3
@@ -247,7 +247,7 @@ class TestLayout < TestInteractive
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
expected = <<~OUTPUT
2
2
> 1
> < 100/100
@@ -275,12 +275,12 @@ class TestLayout < TestInteractive
def test_fzf_multi_line
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
block = <<~BLOCK
998
999
1000
1
2
998
999
1000
1
2
>>0
3/3 (3)
>
@@ -312,11 +312,11 @@ class TestLayout < TestInteractive
>
3/3 (3)
>>0
1
2
1
2
3
1
2
1
2
3
BLOCK
tmux.until { assert_block(block, it) }
end
@@ -609,11 +609,11 @@ class TestLayout < TestInteractive
4
> 3
2
1
98/98
>
2
1
hello
BLOCK
@@ -666,12 +666,12 @@ class TestLayout < TestInteractive
4
> 3
98/98
>
2
1
98/98
>
BLOCK
tmux.until { assert_block(block1, it) }
@@ -979,6 +979,128 @@ class TestLayout < TestInteractive
end
end
def test_layout_default_with_footer
prefix = %[
seq 3 | #{FZF} --no-list-border --height ~100% \
--border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \
--header-label HEAD --header-label-pos 3:bottom \
--bind 'space:transform-footer-label(echo foot)+change-header-label(head)'
].strip + ' '
suffixes = [
%(),
%[--header "$(seq 101 102)"],
%[--header "$(seq 101 102)" --header-first],
%[--header "$(seq 101 102)" --header-lines 2],
%[--header "$(seq 101 102)" --header-lines 2 --header-first],
%[--header "$(seq 101 102)" --header-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-first],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --no-header-lines-border],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border none],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input],
%[--header "$(seq 101 102)" --footer-border sharp --input-border line],
%[--header "$(seq 101 102)" --style full:sharp --header-first]
]
output = <<~BLOCK
201 201 201 201 201 201 201 201 201 201 201 201 201 FOOT FOOT
202 202 202 202 202 202 202 202 202 202 202 202 202 201 201
FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT 202 202
3 3 3 > 3 > 3 3 3 > 3 > 3 > 3 > 3 > 3 > 3
2 2 2 2 2 2 2 2 3
> 1 > 1 > 1 1 1 > 1 > 1 2 2 1 2 2 2 2 3
3/3 101 3/3 101 1/1 3/3 1 1 1 1 1 > 1 2
> 102 > 102 > 101 > 101 101 101 101 > 1
3/3 101 1/1 101 102 102 102 102 102
> 102 > 102 HEAD 101 HEAD HEAD HEAD 101 1/1 101
3/3 102 1/1 1/1 1/1 102 > 102 3/3 >
> HEAD > > > HEAD HEAD >
1/1
> 101 101
102 102
HEAD HEAD
BLOCK
expects = []
output.each_line.first.scan(/\S+/) do
offset = Regexp.last_match.offset(0)
expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n")
end
suffixes.zip(expects).each do |suffix, block|
tmux.send_keys(prefix + suffix, :Enter)
tmux.until { assert_block(block, it) }
tmux.send_keys :Space
tmux.until { assert_block(block.downcase, it) }
teardown
setup
end
end
def test_layout_reverse_list_with_footer
prefix = %[
seq 3 | #{FZF} --layout reverse-list --no-list-border --height ~100% \
--border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \
--header-label HEAD --header-label-pos 3:bottom \
--bind 'space:transform-footer-label(echo foot)+change-header-label(head)'
].strip + ' '
suffixes = [
%(),
%[--header "$(seq 101 102)"],
%[--header "$(seq 101 102)" --header-first],
%[--header "$(seq 101 102)" --header-lines 2],
%[--header "$(seq 101 102)" --header-lines 2 --header-first],
%[--header "$(seq 101 102)" --header-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-first],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input],
%[--header "$(seq 101 102)" --footer-border sharp --input-border line],
%[--header "$(seq 101 102)" --style full:sharp --header-first]
]
output = <<~BLOCK
201 201 201 201 201 201 201 201 201 201 201 FOOT FOOT
202 202 202 202 202 202 202 202 202 202 202 201 201
FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT 202 202
> 1 > 1 > 1 1 1 > 1 > 1 1
2 2 2 2 2 2 2 2 1 1 1 > 1
3 3 3 > 3 > 3 3 3 > 3 2 2 2 2 > 1
3/3 101 3/3 101 1/1 3/3 3 2
> 102 > 102 > 101 > 101 > 3 > 3 > 3 101 3
3/3 101 1/1 101 102 102 102
> 102 > 102 HEAD 101 HEAD 101 1/1 101
3/3 102 1/1 102 > 102 3/3 >
> HEAD > HEAD HEAD >
1/1
> 101 101
102 102
HEAD HEAD
BLOCK
expects = []
output.each_line.first.scan(/\S+/) do
offset = Regexp.last_match.offset(0)
expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n")
end
suffixes.zip(expects).each do |suffix, block|
tmux.send_keys(prefix + suffix, :Enter)
tmux.until { assert_block(block, it) }
tmux.send_keys :Space
tmux.until { assert_block(block.downcase, it) }
teardown
setup
end
end
def test_change_header_and_label_at_once
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
block = <<~BLOCK
@@ -991,4 +1113,189 @@ class TestLayout < TestInteractive
BLOCK
tmux.until { assert_block(block, it) }
end
def test_label_truncation
command = <<~CMD
seq 10 | #{FZF} --style full --border --header-lines=1 --preview ':' \\
--border-label "#{'b' * 1000}" \\
--preview-label "#{'p' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--input-label "#{'i' * 1000}" \\
--list-label "#{'l' * 1000}"
CMD
writelines(command.lines.map(&:chomp))
tmux.send_keys("sh #{tempname}", :Enter)
tmux.until do |lines|
text = lines.join
assert_includes text, 'b··'
assert_includes text, 'l··p'
assert_includes text, 'p··'
assert_includes text, 'h··'
assert_includes text, 'i··'
end
end
def test_separator_no_ellipsis
tmux.send_keys %(seq 10 | #{FZF} --separator "$(seq 1000 | tr '\\n' ' ')"), :Enter
tmux.until do |lines|
assert_equal 10, lines.match_count
refute_includes lines.join, '··'
end
end
def test_header_border_no_pointer_and_marker
tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter
block = <<~BLOCK
1
9/9
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_gutter_default
tmux.send_keys %(seq 10 | fzf), :Enter
block = <<~BLOCK
3
2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_gutter_default_no_unicode
tmux.send_keys %(seq 10 | fzf --no-unicode), :Enter
block = <<~BLOCK
3
2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_gutter_custom
tmux.send_keys %(seq 10 | fzf --gutter x), :Enter
block = <<~BLOCK
x 3
x 2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
# https://github.com/junegunn/fzf/issues/4537
def test_no_scrollbar_preview_toggle
x = 'x' * 300
y = 'y' * 300
tmux.send_keys %(yes #{x} | head -1000 | fzf --bind 'tab:toggle-preview' --border --no-scrollbar --preview 'echo #{y}' --preview-window 'border-left'), :Enter
# │ ▌ xxxxxxxx·· │ yyyyyyyy│
tmux.until do |lines|
lines.any? { it.match?(/x·· │ y+│$/) }
end
tmux.send_keys :Tab
# │ ▌ xxxxxxxx·· │
tmux.until do |lines|
lines.none? { it.match?(/x··y│$/) }
end
tmux.send_keys :Tab
tmux.until do |lines|
lines.any? { it.match?(/x·· │ y+│$/) }
end
end
def test_header_and_footer_should_not_be_wider_than_list
tmux.send_keys %(WIDE=$(printf 'x%.0s' {1..1000}); (echo $WIDE; echo $WIDE) | fzf --header-lines 1 --style full --header-border bottom --header-lines-border top --ellipsis XX --header "$WIDE" --footer "$WIDE" --no-footer-border), :Enter
tmux.until do |lines|
matches = lines.filter_map { |line| line[/x+XX/] }
assert_equal 4, matches.length
assert_equal 1, matches.uniq.length
end
end
def test_combinations
skip unless ENV['LONGTEST']
base = [
'--pointer=@',
'--exact',
'--query=123',
'--header="$(seq 101 103)"',
'--header-lines=3',
'--footer "$(seq 201 203)"',
'--preview "echo foobar"'
]
options = [
['--separator==', '--no-separator'],
['--info=default', '--info=inline', '--info=inline-right'],
['--no-input-border', '--input-border'],
['--no-header-border', '--header-border=none', '--header-border'],
['--no-header-lines-border', '--header-lines-border'],
['--no-footer-border', '--footer-border'],
['--no-list-border', '--list-border'],
['--preview-window=right', '--preview-window=up', '--preview-window=down', '--preview-window=left'],
['--header-first', '--no-header-first'],
['--layout=default', '--layout=reverse', '--layout=reverse-list']
]
# Combination of all options
combinations = options[0].product(*options.drop(1))
combinations.each_with_index do |combination, index|
opts = base + combination
command = %(seq 1001 2000 | #{FZF} #{opts.join(' ')})
puts "# #{index + 1}/#{combinations.length}\n#{command}"
tmux.send_keys command, :Enter
tmux.until do |lines|
layout = combination.find { it.start_with?('--layout=') }.split('=').last
header_first = combination.include?('--header-first')
# Input
input = lines.index { it.include?('> 123') }
assert(input)
# Info
info = lines.index { it.include?('11/997') }
assert(info)
assert(layout == 'reverse' ? input <= info : input >= info)
# List
item1 = lines.index { it.include?('1230') }
item2 = lines.index { it.include?('1231') }
assert_equal(item1, layout == 'default' ? item2 + 1 : item2 - 1)
# Preview
assert(lines.any? { it.include?('foobar') })
# Header
header1 = lines.index { it.include?('101') }
header2 = lines.index { it.include?('102') }
assert_equal(header2, header1 + 1)
assert((layout == 'reverse') == header_first ? input > header1 : input < header1)
# Footer
footer1 = lines.index { it.include?('201') }
footer2 = lines.index { it.include?('202') }
assert_equal(footer2, footer1 + 1)
assert(layout == 'reverse' ? footer1 > item2 : footer1 < item2)
# Header lines
hline1 = lines.index { it.include?('1001') }
hline2 = lines.index { it.include?('1002') }
assert_equal(hline1, layout == 'default' ? hline2 + 1 : hline2 - 1)
assert(layout == 'reverse' ? hline1 > header1 : hline1 < header1)
end
tmux.send_keys :Enter
end
end
end

View File

@@ -189,6 +189,20 @@ class TestPreview < TestInteractive
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
end
def test_preview_asterisk
tmux.send_keys %(seq 5 | #{FZF} --multi --preview 'echo [{}/{+}/{*}/{*n}]' --preview-window '+{1}'), :Enter
tmux.until { |lines| assert_equal 5, lines.match_count }
tmux.until { |lines| assert_includes lines[1], ' [1/1/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' [2/1/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' [3/1 2/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys '5'
tmux.until { |lines| assert_includes lines[1], ' [5/1 2/5/4] ' }
tmux.send_keys '5'
tmux.until { |lines| assert_includes lines[1], ' [/1 2//] ' }
end
def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
@@ -369,6 +383,16 @@ class TestPreview < TestInteractive
end
end
def test_preview_follow_wrap
tmux.send_keys "seq 1 | #{FZF} --preview 'seq 1000' --preview-window right,2,follow,wrap", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until do |lines|
idx = lines.rindex { it.include?('│ 10 │') }
assert_includes lines[idx + 1], '│ ↳ │'
assert_includes lines[idx + 2], '│ ↳ │'
end
end
def test_close
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -527,7 +551,7 @@ class TestPreview < TestInteractive
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.until do |lines|
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
assert_equal ['╭────╮', '│ 10 │', '│ ↳ │', '│ 10 │', '│ ↳ │'], lines.take(5).map(&:strip)
end
end
@@ -558,4 +582,31 @@ class TestPreview < TestInteractive
assert_equal 1, lines.match_count
end
end
def test_preview_wrap_sign_between_ansi_fragments
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 10,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 1234567890 │') })
assert_equal(2, lines.count { |line| line.include?('│ ↳ hello │') })
end
end
def test_preview_wrap_sign_between_ansi_fragments_overflow
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 2,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 12 │') })
assert_equal(0, lines.count { |line| line.include?('│ h') })
end
end
def test_preview_wrap_sign_between_ansi_fragments_overflow2
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 1,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 1 │') })
assert_equal(0, lines.count { |line| line.include?('│ h') })
end
end
end

113
test/test_raw.rb Normal file
View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Testing raw mode
class TestRaw < TestInteractive
def test_raw_mode
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind ctrl-x:toggle-raw,a:enable-raw,b:disable-raw --gutter '▌' --multi --bind 'space:transform-prompt:echo "[[$FZF_RAW]] "'), :Enter
tmux.until { assert_equal 1000, it.match_count }
tmux.send_keys 1
tmux.until { assert_equal 272, it.match_count }
tmux.send_keys :Up
tmux.until { assert_includes it, '> 2' }
tmux.send_keys 'C-p'
tmux.until do
assert_includes it, '> 10'
assert_includes it, '▖ 9'
end
tmux.send_keys 'C-x'
tmux.until do
assert_includes it, '> 10'
assert_includes it, '▌ 1'
end
tmux.send_keys :Up, 'C-x'
tmux.until do
assert_includes it, '> 11'
assert_includes it, '▖ 10'
end
tmux.send_keys 1
tmux.until { assert_equal 28, it.match_count }
tmux.send_keys 'C-p'
tmux.until do
assert_includes it, '> 101'
assert_includes it, '▖ 100'
end
tmux.send_keys 'C-n'
tmux.until do
assert_includes it, '> 11'
assert_includes it, '▖ 10'
end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { assert_equal 3, it.select_count }
tmux.send_keys 'C-x'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '▌ 110'
assert_includes it, '>>11'
end
tmux.send_keys 'a'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '>>11'
assert_includes it, '▖ 10'
end
tmux.send_keys :Down, :Space
tmux.until { assert_includes it, '[[0]] 11' }
tmux.send_keys :Up, :Space
tmux.until { assert_includes it, '[[1]] 11' }
tmux.send_keys 'b'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '▌ 110'
assert_includes it, '>>11'
end
tmux.send_keys :Space
tmux.until { assert_includes it, '[[]] 11' }
tmux.send_keys 'C-u', '5'
tmux.until { assert_includes it, '> 5' }
tmux.send_keys 'C-x', 'C-p', 'C-p'
tmux.until do
assert_includes it, '> 25'
assert_includes it, '▖ 24'
end
tmux.send_keys 'C-x'
tmux.until do
assert_includes it, '> 25'
assert_includes it, '▌ 15'
end
# 35 is the closest match in raw mode
tmux.send_keys 'C-x', :Up, :Up, :Up, :Up, :Up, :Up, 'C-x'
tmux.until do
assert_includes it, '> 35'
assert_includes it, '▌ 25'
end
end
def test_raw_best
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind space:best), :Enter
tmux.send_keys 999
tmux.until { assert_includes it, '> 1' }
tmux.send_keys :Space
tmux.until { assert_includes it, '> 999' }
end
end

View File

@@ -31,22 +31,32 @@ class TestServer < TestInteractive
end
def test_listen_with_api_key
post_uri = URI('http://localhost:6266')
uri = URI('http://localhost:6266')
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
# Incorrect API Key
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
res = Net::HTTP.get_response(uri, headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
end
# Valid API Key
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '200', res.code
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
res = Net::HTTP.get_response(uri, headers)
assert_equal '200', res.code
assert_equal 'yo', JSON.parse(res.body, symbolize_names: true)[:query]
end
end
end

View File

@@ -27,6 +27,10 @@ module TestShell
tmux.prepare
end
def trigger
'**'
end
def test_ctrl_t
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
@@ -134,7 +138,7 @@ module TestShell
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar'
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
@@ -165,7 +169,11 @@ module CompletionTest
FileUtils.touch(File.expand_path(f))
end
tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/10', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys ' !d'
tmux.until { |lines| assert_equal 2, lines.match_count }
@@ -179,7 +187,11 @@ module CompletionTest
# ~USERNAME**<TAB>
user = `whoami`.chomp
tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{user}**", :Tab
if shell == :fish
tmux.send_keys "cat ~#{user}", 'C-t'
else
tmux.send_keys "cat ~#{user}#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys "/#{user}"
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
@@ -190,14 +202,29 @@ module CompletionTest
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat ~such**', :Tab
if shell == :fish
tmux.send_keys 'cat ~such', 'C-t'
else
tmux.send_keys "cat ~such#{trigger}", :Tab
end
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
tmux.until(true) do |lines|
if shell == :fish
# Fish's string escape quotes filenames with ~ to prevent tilde expansion
assert_equal 'cat no\\~such\\~user', lines[-1]
else
assert_equal 'cat no~such~user', lines[-1]
end
end
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
if shell == :fish
tmux.send_keys 'cat /tmp/fzf\\ test/', 'C-t'
else
tmux.send_keys "cat /tmp/fzf\\ test/#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'foobar$'
tmux.until do |lines|
@@ -210,7 +237,11 @@ module CompletionTest
# Should include hidden files
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/hidden', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
end
tmux.until(true) do |lines|
assert_equal 100, lines.match_count
assert lines.any_include?('/tmp/fzf-test/.hidden-')
@@ -223,70 +254,89 @@ module CompletionTest
end
def test_file_completion_root
tmux.send_keys 'ls /**', :Tab
if shell == :fish
tmux.send_keys 'ls /', 'C-t'
else
tmux.send_keys "ls /#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Enter
end
def test_dir_completion
FileUtils.mkdir_p('/tmp/fzf-test-dir')
(1..100).each do |idx|
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
FileUtils.mkdir_p("/tmp/fzf-test-dir/d#{idx}")
end
FileUtils.touch('/tmp/fzf-test/d55/xxx')
FileUtils.touch('/tmp/fzf-test-dir/d55/xxx')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
if shell == :fish
tmux.send_keys 'cd /tmp/fzf-test-dir/', 'C-t'
else
tmux.send_keys "cd /tmp/fzf-test-dir/#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55
# Tab selects items in C-t's --multi mode, so skip for fish
tmux.send_keys :Tab, :Tab unless shell == :fish # Tab does not work here
tmux.send_keys '55/$'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55/'
assert_includes lines, '> 55/$'
assert_includes lines, '> /tmp/fzf-test-dir/d55/'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
# C-t appends a trailing space after the result
tmux.send_keys :BSpace if shell == :fish
tmux.send_keys :xx
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
# Should not match regular files (bash-only)
if instance_of?(TestBash)
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
end
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55', lines[-1] }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dir')
end
def test_process_completion
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys 'kill **', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
skip('fish background job format differs') if shell == :fish
begin
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys "kill #{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
end
end
end
end
def test_custom_completion
skip('fish does not use _fzf_compgen_path; path completion is via ctrl-t') if shell == :fish
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab
tmux.send_keys "ls /tmp/#{trigger}", :Tab
tmux.until { |lines| assert_equal 11, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert_equal 3, lines.select_count }
@@ -295,11 +345,12 @@ module CompletionTest
end
def test_unset_completion
skip('fish has native completion for set and unset variables') if shell == :fish
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
# Using tmux
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
@@ -308,34 +359,57 @@ module CompletionTest
# FZF_TMUX=1
new_shell
tmux.focus
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
end
def test_completion_in_command_sequence
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
if shell == :fish
FileUtils.mkdir_p('/tmp/fzf-test-seq')
FileUtils.touch('/tmp/fzf-test-seq/fzffoobar')
tmux.prepare
# Fish uses Shift-Tab for fzf completion (no trigger system)
command = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobr'
expected = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobar'
tmux.send_keys command, :BTab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
tmux.until { |lines| assert_equal expected, lines[-1] }
else
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
expected = 'echo foo; QUX=THUD unset FZFFOOBAR'
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal expected, lines[-1] }
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-seq') if shell == :fish
end
def test_file_completion_unicode
FileUtils.mkdir_p('/tmp/fzf-test')
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
# Shell-agnostic file creation
File.write('/tmp/fzf-test/fzf-unicode 테스트1', "test3\n")
File.write('/tmp/fzf-test/fzf-unicode 테스트2', "test4\n")
tmux.send_keys 'cd /tmp/fzf-test', :Enter
tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab
if shell == :fish
tmux.send_keys 'cat fzf-unicode', 'C-t'
else
tmux.send_keys "cat fzf-unicode#{trigger}", :Tab
end
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
@@ -358,36 +432,41 @@ module CompletionTest
end
def test_custom_completion_api
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys "#{command} b**", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
skip('bash-specific _comprun/declare syntax') if shell == :fish
begin
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys "#{command} b#{trigger}", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
ensure
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
end
ensure
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
end
def test_ssh_completion
skip('fish uses native ssh completion') if shell == :fish
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
tmux.send_keys 'ssh jg@localhost**', :Tab
tmux.send_keys "ssh jg@localhost#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
tmux.send_keys " -i /tmp/fzf-test-ssh#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 5
assert_equal 0, lines.select_count
@@ -399,11 +478,344 @@ module CompletionTest
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
tmux.send_keys 'localhost**', :Tab
tmux.send_keys "localhost#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
end
def test_option_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long')
end
def test_option_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command -- --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- --opt=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long-ddash')
end
def test_option_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -o=SECURI#{trigger}", :Tab
end
case shell
when :bash, :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -o=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short')
end
def test_option_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -- -o=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURITY.md'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- -o=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short-ddash')
end
def test_option_no_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --optSECURI', 'C-t'
else
tmux.send_keys "some-command --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long')
end
def test_option_no_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --optSECURI', 'C-t'
else
tmux.send_keys "some-command -- --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long-ddash')
end
def test_option_no_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -oSECURI', 'C-t'
else
tmux.send_keys "some-command -oSECURI#{trigger}", :Tab
end
case shell
when :bash, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -oSECURITY.md', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short')
end
def test_option_no_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -oSECURI', 'C-t'
else
tmux.send_keys "some-command -- -oSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short-ddash')
end
def test_filename_with_newline
FileUtils.mkdir_p('/tmp/fzf-test-newline')
FileUtils.touch("/tmp/fzf-test-newline/xyz\nwith\nnewlines")
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-newline', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'cat xyz', 'C-t'
else
tmux.send_keys "cat xyz#{trigger}", :Tab
end
case shell
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# fish escapes newlines in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz\\nwith\\nnewlines', lines[-1] }
when :bash, :zsh
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# bash and zsh replace newlines with spaces in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz with newlines', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-newline')
end
def test_path_with_special_chars
FileUtils.mkdir_p('/tmp/fzf-test-[special]')
FileUtils.touch('/tmp/fzf-test-[special]/xyz123')
tmux.prepare
if shell == :fish
tmux.send_keys 'ls /tmp/fzf-test-\[special\]/xyz', 'C-t'
else
tmux.send_keys "ls /tmp/fzf-test-\\[special\\]/xyz#{trigger}", :Tab
end
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls /tmp/fzf-test-\\[special\\]/xyz123', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-[special]')
end
def test_query_with_dollar_anchor
FileUtils.mkdir_p('/tmp/fzf-test-dollar-anchor')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/file.txt')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/filetxt.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-dollar-anchor', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls txt$', 'C-t'
else
tmux.send_keys "ls txt$#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> txt$'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls file.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dollar-anchor')
end
def test_single_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-single-flag')
FileUtils.touch('/tmp/fzf-test-single-flag/-testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-single-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls -', 'C-t'
else
tmux.send_keys "ls -#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> -'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls -testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-single-flag')
end
def test_double_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-double-flag')
FileUtils.touch('/tmp/fzf-test-double-flag/--testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-double-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls --', 'C-t'
else
tmux.send_keys "ls --#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> --'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls --testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-double-flag')
end
end
class TestBash < TestBase
@@ -424,7 +836,7 @@ class TestBash < TestBase
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
tmux.paste '_completion_loader() { complete -o default fake; }'
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'C-c'
@@ -433,7 +845,7 @@ class TestBash < TestBase
tmux.send_keys :Tab, 'C-u'
tmux.prepare
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
end
end
@@ -455,24 +867,142 @@ class TestZsh < TestBase
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
['unset', '\unset', "'unset'"].each do |command|
tmux.prepare
tmux.send_keys "#{command} FZFFOOBR**", :Tab
tmux.send_keys "#{command} FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
tmux.send_keys 'C-c'
end
end
# Helper function to run test with Perl and again with Awk
def self.test_perl_and_awk(name, &block)
define_method(:"test_#{name}") do
instance_eval(&block)
end
define_method(:"test_#{name}_awk") do
tmux.send_keys "unset 'commands[perl]'", :Enter
tmux.prepare
# Verify perl is actually unset (0 = not found)
tmux.send_keys 'echo ${+commands[perl]}', :Enter
tmux.until { |lines| assert_equal '0', lines[-1] }
tmux.prepare
instance_eval(&block)
end
end
def prepare_ctrl_r_test
tmux.send_keys ':', :Enter
tmux.send_keys 'echo match-collision', :Enter
tmux.prepare
tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
tmux.prepare
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'echo "trailing_space "', :Enter
tmux.prepare
tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
end
test_perl_and_awk 'ctrl_r_accept_or_print_query' do
set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '1 foobar'
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
end
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
# Leading number in multi-line history content is not confused with index
prepare_ctrl_r_test
tmux.send_keys "'line 1"
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
end
end
test_perl_and_awk 'ctrl_r_multi_selection' do
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
end
end
test_perl_and_awk 'ctrl_r_no_multi_selection' do
set_var('FZF_CTRL_R_OPTS', '--no-multi')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| refute_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
end
end
# NOTE: 'Perl/$history' won't see foreign cmds immediately, unlike 'awk/fc'.
# Perl passes only because another cmd runs between mocking and triggering C-r
# https://github.com/junegunn/fzf/issues/4061
# https://zsh.org/mla/users/2024/msg00692.html
test_perl_and_awk 'ctrl_r_foreign_commands' do
histfile = "#{tempname}-foreign-hist"
tmux.send_keys "HISTFILE=#{histfile}", :Enter
tmux.prepare
# SHARE_HISTORY picks up foreign commands; marked with * in fc
tmux.send_keys 'setopt SHARE_HISTORY', :Enter
tmux.prepare
tmux.send_keys 'fzf_cmd_local', :Enter
tmux.prepare
# Mock foreign command (for testing only; don't edit your HISTFILE this way)
tmux.send_keys "echo ': 0:0;fzf_cmd_foreign' >> $HISTFILE", :Enter
tmux.prepare
# Verify fc shows foreign command with asterisk
tmux.send_keys 'fc -rl -1', :Enter
tmux.until { |lines| assert(lines.any? { |l| l.match?(/^\s*\d+\* fzf_cmd_foreign/) }) }
tmux.prepare
# Test ctrl-r correctly extracts the foreign command
tmux.send_keys 'C-r'
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '^fzf_cmd_'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal %w[fzf_cmd_foreign fzf_cmd_local], lines[-2..]
end
ensure
FileUtils.rm_f(histfile)
end
end
class TestFish < TestBase
include TestShell
include CompletionTest
def shell
:fish
end
def trigger
'++'
end
def new_shell
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
tmux.send_keys 'env FZF_TMUX=1 XDG_CONFIG_HOME=/tmp/fzf-fish fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines }
end
@@ -490,15 +1020,23 @@ class TestFish < TestBase
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
offset = -6
block = <<~BLOCK
echo "foo
bar"
echo "bar
foo"
BLOCK
if shell == :fish
offset = -4
block = <<~FISH
echo "foo␊bar"
echo "bar␊foo"
FISH
end
tmux.until do |lines|
block.lines.each_with_index do |line, idx|
assert_includes lines[-6 + idx], line.chomp
assert_includes lines[idx + offset], line.chomp
end
end
tmux.send_keys :BTab, :BTab

View File

@@ -33,6 +33,9 @@ for opt in "$@"; do
esac
done
cd "$(dirname "${BASH_SOURCE[0]}")"
fzf_base=$(pwd)
ask() {
while true; do
read -p "$1 ([y]/n) " -r
@@ -64,19 +67,19 @@ remove_line() {
line_no=1
continue
fi
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 ))
line_no=$(($(sed 's/:.*//' <<< "$line") + line_no - 1))
content=$(sed 's/^[0-9]*://' <<< "$line")
match=1
echo " - Line #$line_no: $content"
echo " - Line #$line_no: $content"
[ "$content" = "$1" ] || ask " - Remove?"
if [ $? -eq 0 ]; then
temp=$(mktemp)
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
cat "$temp" > "$src" && rm -f "$temp" || break
echo " - Removed"
echo " - Removed"
else
echo " - Skipped"
line_no=$(( line_no + 1 ))
echo " - Skipped"
line_no=$((line_no + 1))
fi
done
[ $match -eq 0 ] && echo " - Nothing found"
@@ -94,12 +97,15 @@ done
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ -f "$bind_file" ]; then
remove_line "$bind_file" "fzf_key_bindings"
remove_line "$bind_file" "fzf_completion_setup"
remove_line "$bind_file" "fzf --fish | source"
fi
if [ -d "${fish_dir}/functions" ]; then
remove "${fish_dir}/functions/fzf.fish"
remove "${fish_dir}/functions/fzf_key_bindings.fish"
remove_line "$bind_file" "source \"${fzf_base}/shell/completion.fish\""
remove_line "$bind_file" "source \"${fzf_base}/shell/key-bindings.fish\""
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
rmdir "${fish_dir}/functions"
@@ -109,6 +115,6 @@ if [ -d "${fish_dir}/functions" ]; then
fi
config_dir=$(dirname "$prefix_expand")
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then
if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then
rmdir "$config_dir"
fi