Compare commits

...

546 Commits

Author SHA1 Message Date
Junegunn Choi
ce58d08ee3 Use LSD radix sort for Result sorting in matcher
Replace comparison-based pdqsort with LSD radix sort on the uint64
sort key. Radix sort is O(n) vs O(n log n) and avoids pointer-chasing
cache misses in the comparison function. Sort scratch buffer is reused
across iterations to reduce GC pressure.

Benchmark (single-threaded, Chromium file list):
- linux query (180K matches): ~16% faster
- src query (high match count): ~31% faster
- Rare matches: equivalent (falls back to pdqsort for n < 128)
2026-03-01 11:58:16 +09:00
Junegunn Choi
997a7e5947 Add direct algo fast path in matchChunk
For the common case of a single fuzzy term with no nth transform,
call the algo function directly from matchChunk, bypassing the
MatchItem -> extendedMatch -> iter dispatch chain. This eliminates
3 function calls and the per-match []Offset heap allocation.
2026-03-01 11:58:16 +09:00
Junegunn Choi
88e48619d6 Return Result by value from MatchItem 2026-03-01 11:18:43 +09:00
Junegunn Choi
2db14b4308 Enhance --bench output with formatted times, match count, and selectivity
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-03-01 11:16:52 +09:00
junegunn
90c4269d4e Deploying to master from @ junegunn/fzf@6087055305 🚀
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-28 15:01:49 +00:00
Junegunn Choi
6087055305 Enable uint64 compareRanks on arm64
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
Extend the uint64 rank comparison trick (comparing [4]uint16 as a
single uint64) to arm64 builds. ARM64 is little-endian like x86, so
the same unsafe.Pointer cast produces correct lexicographic ordering.

This replaces a 4-iteration loop with a single uint64 comparison,
speeding up the sort phase.

Chromium file list, single-threaded:
  linux:  126ms -> 126ms (sort not dominant)
  src:    462ms -> 438ms (-5%, sort-heavy)
2026-02-28 14:42:28 +09:00
Junegunn Choi
2f9df91171 Add --threads option to control matcher concurrency
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
By default, fzf uses 8 * NumCPU goroutines (capped at 32) for
parallel matching. --threads N overrides this to use exactly N
goroutines, which is useful for benchmarking and profiling.
2026-02-26 14:51:59 +09:00
Junegunn Choi
12e24d368c Add --bench flag for repeatable filter-mode timing
fzf --filter PATTERN --bench 3s < input

Repeats matcher.scan() for the given duration, clears cache between
iterations, and prints stats (iterations, avg, min, max) to stderr.
2026-02-25 10:16:37 +09:00
Junegunn Choi
55193ee4dc Fix double subtraction of header lines from FZF_TOTAL_COUNT
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 #4692
2026-02-25 00:50:47 +09:00
Junegunn Choi
ff6a3bbee0 Add GitHub action for labelling PRs 2026-02-24 20:27:29 +09:00
Junegunn Choi
dce248ac6d Revert "Add GitHub action for labelling PRs"
This reverts commit 0ff13dcf.
2026-02-24 20:26:39 +09:00
Junegunn Choi
0ff13dcfbe Add GitHub action for labelling PRs 2026-02-24 20:20:04 +09:00
Junegunn Choi
4d6a7757b8 Fix adaptive height calculation to exclude header lines
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
The adaptive height (--height ~100%) was using the raw chunklist count
including header items, making the window too tall by headerLines rows.
2026-02-23 02:21:41 +09:00
Junegunn Choi
b9804f5873 Add change-header-lines action to dynamically change --header-lines
All input lines now enter the chunklist with sequential indices, and
header lines are excluded from matching via Pattern.startIndex and
PassMerger offset. This allows the number of header lines to be changed
at runtime with change-header-lines(N), transform-header-lines, and
bg-transform-header-lines actions.

- Remove EvtHeader event; header items are read directly from chunks
- Add startIndex to Pattern and PassMerger for skipping header items
- Add targetIndex field to Terminal for cursor repositioning across
  header-lines changes

Close #4659
2026-02-23 01:48:03 +09:00
Junegunn Choi
98a3b1fff8 Revert "Skip dead zones in FuzzyMatchV2 score matrix computation"
This reverts commit 6df5ca17e8.
2026-02-23 01:48:03 +09:00
Junegunn Choi
6df5ca17e8 Skip dead zones in FuzzyMatchV2 score matrix computation
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
In Phase 3 of FuzzyMatchV2, when a cell's left neighbor score is <= 1
and the current character doesn't match the pattern character, the
cell's score is guaranteed to be 0 (since gap penalties are -1 and -3).
Skip the bonus/gap computation entirely and fast-forward through
consecutive non-matching characters in the dead zone.

This yields 6-11% faster fuzzy searches on typical workloads.
2026-02-22 03:09:29 +09:00
Junegunn Choi
09ca45f7db Increase chunkSize from 100 to 1000 to reduce lock contention
With chunkSize=100 and 10M items, 100K chunks cause ~300K mutex
lock/unlock operations per search across 32 goroutines competing
for a single sync.Mutex in ChunkCache.

Increasing to 1000 reduces chunks to 10K, cutting contention overhead.
Benchmarks on 10M items show 14-80% faster searches depending on query
selectivity.
2026-02-22 03:09:28 +09:00
junegunn
09fe3a4180 Deploying to master from @ junegunn/fzf@b908f7a0ec 🚀
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-21 15:03:44 +00:00
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
Junegunn Choi
0012183ede 0.60.3
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-03 17:10:49 +09:00
Junegunn Choi
8916cbc6ab [windows] Prevent fzf from consuming user input while paused
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
This partly fixes #4260.

fzf still can consume the first key stroke.
2025-03-03 14:04:16 +09:00
junegunn
21ce70054f Deploying to master from @ junegunn/fzf@3ba82b6d87 🚀
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-02 00:02:11 +00:00
Junegunn Choi
3ba82b6d87 Make truncateQuery faster
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/issues/4292#issuecomment-2687051731
2025-02-27 15:49:15 +09:00
Junegunn Choi
e771c5d057 Update README 2025-02-27 14:01:13 +09:00
Junegunn Choi
4e5e925e39 Increase the query length limit from 300 to 1000
Close #4292
2025-02-27 11:43:58 +09:00
Junegunn Choi
b7248d4115 Remove temp files before 'become' when using --tmux option
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
Close #4283

But the temp files for the `f` flags in the 'become' template will not
be removed, because we will need them after "becoming" another program.

  e.g. fzf --bind 'enter:become:cat {f}'
2025-02-26 20:47:09 +09:00
Junegunn Choi
639253840f Trim trailing whitespaces after processing ANSI sequences
Close #4282
2025-02-26 16:17:12 +09:00
Junegunn Choi
710ebdf9c1 Make --accept-nth compatible with --select-1
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 #4287
2025-02-26 00:25:23 +09:00
bitraid
bb64d84ce4 [fish] Enable multiple history commands insertion (#4280)
Enable inserting multiple history commands. To disable, set `--no-multi`
through `$FZF_CTRL_R_OPTS`.

Also, remove the usage of `become` action, to avoid leaving behind
temporary files.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-02-26 00:18:56 +09:00
alex-huff
cd1da27ff2 Fix condition for using item numlines cache (#4285) 2025-02-25 20:25:26 +09:00
Junegunn Choi
c1accc2e5b Use '/' as path separator on MSYS2
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 #4281
2025-02-25 10:12:19 +09:00
Junegunn Choi
e4489dcbc1 Fix regression: Trim trailing whitespaces when using --with-nth
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
https://github.com/junegunn/fzf/issues/4272#issuecomment-2677279620
2025-02-24 18:40:13 +09:00
Junegunn Choi
c0d407f7ce 0.60.2
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-02-23 19:52:57 +09:00
Junegunn Choi
461115afde Add support for {n} in --with-nth and --accept-nth templates
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
Close #4275
2025-02-23 19:47:56 +09:00
junegunn
bae1965231 Deploying to master from @ junegunn/fzf@b89c77ec9a 🚀
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-02-23 00:02:08 +00:00
Junegunn Choi
b89c77ec9a Mention that actions after accept or abort are ignored (#4271)
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
2025-02-22 22:19:16 +09:00
Junegunn Choi
1ca5f09d7b Explain the difference of template from a single field index expression
Close #4272
2025-02-22 22:14:49 +09:00
Junegunn Choi
d79902ae59 Fix 'jump' when pointer is empty
Fix #4270
2025-02-22 19:05:30 +09:00
phanium
77568e114f Don't trim last field when delimiter is regex (#4266)
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-02-21 22:21:55 +09:00
Junegunn Choi
a24d274a3c 0.60.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-02-20 21:42:56 +09:00
Junegunn Choi
dac81432d6 [zsh/key-bindings] don't unescape FZF_DEFAULT_OPTS (addendum: #4262) 2025-02-20 20:58:21 +09:00
Steve Williams
309b5081ef [zsh/completion] don't unescape FZF_DEFAULT_OPTS (#4262) 2025-02-20 20:55:23 +09:00
bitraid
91bc4f2671 [fish] Add comment about fish version compatibility
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-02-20 08:30:30 +09:00
bitraid
4c9d37d919 [fish] Reorder functions
Move the helper functions to the top of the main function, and the main
function commands (bind command) to the bottom.
2025-02-20 08:30:30 +09:00
bitraid
7e9566f66a [fish] Refactor bind commands
Use single check for each default command variable.
2025-02-20 08:30:30 +09:00
bitraid
3f7e8a475d [fish] Refactor fzf-cd-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Simplify result variable check.
- Set the command line using a single call to commandline.
2025-02-20 08:30:30 +09:00
bitraid
1cf7c0f334 [fish] Refactor fzf-history-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Pass all fzf options (except query) through FZF_DEFAULT_OPTS variable.
2025-02-20 08:30:30 +09:00
bitraid
ff8ee9ee4e [fish] Refactor fzf-file-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Simplify result variable check.
- Insert file names using a single call to commandline.
2025-02-20 08:30:30 +09:00
bitraid
cbbd939a94 [fish] Refactor __fzf_parse_commandline, remove __fzf_get_dir
The __fzf_get_dir function was called only once, and was basically a
single command in a while loop.
2025-02-20 08:30:30 +09:00
bitraid
f232df2887 [fish] __fzfcmd: Don't set FZF_TMUX
The FZF_TMUX variable check has already been changed from numeric to
string, so there is no need to set it to 0 if it's empty or undefined.
2025-02-20 08:30:30 +09:00
bitraid
16bfb2c80c [fish] Refactor __fzf_defaults
Append all arguments after the first one, so that functions don't have
to pass all appending options as a single string. Also, output
everything as a single string (an array of one item).
2025-02-20 08:30:30 +09:00
Junegunn Choi
0ba066123e Fix case where preview window is not scrollable (#4258)
When the last rendered line was wrapped, fzf would incorrectly determine
the scrollability of the window.
2025-02-20 08:22:43 +09:00
Junegunn Choi
81c51c26cc [man] Describe what 'smart-case' mode is
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
Close #4256
2025-02-20 08:02:04 +09:00
Junegunn Choi
6fa8295ac5 walker: Append path separator to directories
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
Close #4255
2025-02-18 22:03:59 +09:00
Junegunn Choi
f975b40236 Fix {q} in preview window affected by 'search' action
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-02-18 10:08:47 +09:00
Alexei Șerșun
01d9d9c8c8 Normalize char before pattern lookup (#4252)
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
There is an edge-case in FuzzyMatchV1 during backward scan, related to
normalization: if string is initially denormalized (e.g. Unicode symbol),
backward scan will proceed further to the next char; however, when the
score is computed, the string is normalized first, then scanned based on
the pattern. This leads to accessing pattern index increment, which
itself leads to out-of-bound index access, resulting in a panic.

To illustrate the process, here's the sequence of operations when search
is perfored:

1. during backward scan by "minim" pattern

```
xxxxx Minímal example
      ^^^^^^^^^^^^
      ||||||||||||
      miniiiiiiiim <- compute score for this substring
```
2. during compute score by "minim" pattern
```
      Minímal exam
      minimal exam <- normalize chars before computing the score
      ^^^^^^
      ||||||
      minim <- at this point the pattern is already fully scanned and index
              is out-of-the-bound
```

In this commit the char is normalized during backward scan, to detect
properly the boundaries for the pattern.
2025-02-17 20:50:15 +09:00
Junegunn Choi
1eafc4e5d9 Ignore NULL byte before CSI 6N response
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
Close #2455
2025-02-16 21:18:01 +09:00
junegunn
38e4020aa8 Deploying to master from @ junegunn/fzf@ac32fbb3b2 🚀
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-02-16 00:02:15 +00:00
Junegunn Choi
ac32fbb3b2 Avoid printing items in an extremely narrow screen
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-02-13 22:12:25 +09:00
Junegunn Choi
7d26eca5cc Truncate wrap sign in the list section if necessary 2025-02-13 21:50:53 +09:00
Junegunn Choi
3347d61591 0.60.0
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-02-13 00:54:21 +09:00
Junegunn Choi
9abf2c8c9c Allow suffix match on --nth with custom --delimiter
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
When --nth is used with a custom --delimiter, the last delimiter was
included in the search scope, forcing you to write the delimiter in
a suffix-match query. This commit removes the last delimiter from the
search scope.

  # No need to write 'bar,$'
  echo foo,bar,baz | fzf --delimiter , --nth 2 --filter 'bar$'

This can be seen as a breaking change, but I'm gonna say it's a bug fix.

Fix #3983
2025-02-12 20:53:32 +09:00
Junegunn Choi
84e2262ad6 Make --accept-nth and --with-nth support templates 2025-02-12 20:15:04 +09:00
Junegunn Choi
378137d34a Simplify code
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-02-11 23:43:43 +09:00
Junegunn Choi
66ca16f836 Truncate wrap signs in extremely narrow preview window 2025-02-11 23:41:54 +09:00
bitraid
282884ad83 [fish] Unescape query from commandline (#4236)
More natural processing of the query taken from command line, by
unquoting/unescaping the token. Unescaped open quotes are removed.
Because of how `string unescape` works, if both single and double quotes
are present, with the outer quotes open, only the outer quotes are
removed.

Examples:
`'foo bar'`, `"foo bar"`, `foo\ bar` becomes `foo bar`
`"foobar`, `'foobar`, `foo"bar`, `foo'bar` becomes `foobar`
`'"foo"'`, `'"foo"` becomes `"foo"`
`"'foo'"`, `"'foo'` becomes `'foo'`
`"'foo` becomes `'foo`
`'"foo` becomes `"foo`
2025-02-11 23:19:40 +09:00
dependabot[bot]
7877ac42f0 Bump golang.org/x/term from 0.28.0 to 0.29.0 (#4234)
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
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/term/compare/v0.28.0...v0.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 00:41:55 +09:00
Junegunn Choi
19ef8891e3 Print --wrap-sign in preview window
Close #4233
2025-02-11 00:01:50 +09:00
Coko
bfea9e53a6 fzf-preview.sh: Use kitten icat on ghostty (#4232)
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-02-09 20:02:05 +09:00
Junegunn Choi
a2420026ab Rename actions: exclude and exclude-multi
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
https://github.com/junegunn/fzf/pull/4231#issuecomment-2646067669
2025-02-09 13:52:20 +09:00
Junegunn Choi
1be1991299 Add exclude-current action
https://github.com/junegunn/fzf/pull/4231#issuecomment-2646063208
2025-02-09 13:37:22 +09:00
Junegunn Choi
67dd7e1923 Add 'exclude' action for excluding current/selected items from the result (#4231)
Close #4185
2025-02-09 13:22:33 +09:00
Junegunn Choi
2b584586ed Add --accept-nth option to transform the output
This option can be used to replace a sed or awk in the post-processing step.

  ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
  ps -ef | fzf --multi --header-lines 1 --accept-nth 2

This may not be a very "Unix-y" thing to do, so I've always felt that fzf
shouldn't have such an option, but I've finally changed my mind because:

* fzf can be configured with a custom delimiter that is a fixed string
  or a regular expression.
* In such cases, you'd need to repeat the delimiter again in the
  post-processing step.
* Also, tools like awk or sed may interpret a regular expression
  differently, causing mismatches.

You can still use sed, cut, or awk if you prefer.

Close #3987
Close #1323
2025-02-09 11:53:35 +09:00
Eric Chen
a1994ff0ab Update README.md (#4225)
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-02-09 09:19:15 +09:00
junegunn
ca0e858871 Deploying to master from @ junegunn/fzf@06c6615507 🚀 2025-02-09 00:02:24 +00:00
bitraid
06c6615507 [fish] Fix for directories with special characters (#4230)
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
Using CTRL-T or ALT-C when the current command line token contained a
directory with special characters, the script would fail to detect it.
For exampe, an existing directory named `it\'s\ a\ test`, instead of
using it as walker-root, it would use it as the query.
2025-02-08 22:18:05 +09:00
Junegunn Choi
818d0be436 Fix change-header-label+change-header
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 #4227
2025-02-07 20:57:09 +09:00
Junegunn Choi
fcd2baa945 Fix scrolling performance when --wrap is enabled
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 #4221
2025-02-06 22:30:39 +09:00
Junegunn Choi
62e0a2824a Fix nth highlighting
Fix #4222
2025-02-06 19:57:39 +09:00
Junegunn Choi
bbe1721a18 0.59.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-02-02 23:39:47 +09:00
Junegunn Choi
c1470a51b8 Update Dockerfile
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-02-02 23:10:53 +09:00
Junegunn Choi
6ee31d5dc5 Fix failing test case 2025-02-02 17:46:14 +09:00
Junegunn Choi
65d74387e7 Stop processing more actions after a terminal action (accept, abort, etc.)
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-02-02 16:28:32 +09:00
junegunn
7d0ea599c4 Deploying to master from @ junegunn/fzf@b7795a3dea 🚀 2025-02-02 00:02:12 +00:00
Junegunn Choi
b7795a3dea Fix RuboCop errors
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
2025-02-02 02:48:04 +09:00
Junegunn Choi
323f6f6202 Fix mode switching example in CHANGELOG 2025-02-02 02:26:13 +09:00
Junegunn Choi
0c61223884 Fix tcell renderer's pause and resume 2025-02-02 02:23:48 +09:00
Junegunn Choi
32234be7a2 FZF_KEY enhancements
* 'enter' instead of 'ctrl-m'
* 'space' instead of ' '
2025-02-02 02:23:47 +09:00
Junegunn Choi
178b49832e Fix {show,hide,toggle}-input and add test cases
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-02-01 17:23:22 +09:00
Junegunn Choi
18cbb4a84d Display header lines at the top in 'reverse-list' layout 2025-02-01 17:03:59 +09:00
Junegunn Choi
e84afe196a Add {show,hide,toggle}-input and expose $FZF_INPUT_STATE 2025-02-01 17:03:59 +09:00
Junegunn Choi
e1e171a3c4 Add toggle-bind 2025-02-01 17:03:59 +09:00
Junegunn Choi
d075c00015 Fix --layout reverse-list --no-input
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-02-01 09:28:02 +09:00
Junegunn Choi
6c0ca4a64a Add --no-input to hide the input section (#4210)
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
Close #2890
Close #1396
 
You can't type in queries in this mode, and the only way to trigger an
fzf search is to use `search(...)` action.

  # Click header to trigger search
  fzf --header '[src] [test]' --no-input --layout reverse \
      --header-border bottom --input-border \
      --bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
2025-01-30 00:50:46 +09:00
dependabot[bot]
6b5d461411 Bump crate-ci/typos from 1.28.4 to 1.29.4 (#4161)
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
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.4 to 1.29.4.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.4...v1.29.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 17:11:12 +09:00
Junegunn Choi
7419e0dde1 Update ADVANCED.md 2025-01-29 17:09:22 +09:00
bitraid
cf2bb5e40e [fish] Improve fish binary path detection (#4208)
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
Instead of exporting a local `$SHELL` containing the location of fish in
`$PATH` when global `$SHELL` is not fish, always set `--with-shell` with
the actual binary path of fish that the function is running from.
2025-01-28 21:34:21 +09:00
Moritz Dietz
f466e94d65 Fix typos in ADVANCED.md (#4209) 2025-01-28 21:26:52 +09:00
Junegunn Choi
eb0257d48f Enhance --min-height option to take number followed by + 2025-01-28 18:34:12 +09:00
Junegunn Choi
b83dd6c6b4 Update ADVANCED example using 'search' action 2025-01-28 17:48:46 +09:00
Junegunn Choi
51c207448d Set the default value of --min-height depending on other options
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-01-27 20:33:47 +09:00
Junegunn Choi
a6a558da30 Update junegunn/go-shellwords 2025-01-27 19:21:22 +09:00
Junegunn Choi
2bf5fa27be [completion] Replace 'tr' with built-in string substitution 2025-01-27 19:19:08 +09:00
Junegunn Choi
af7940746f Fix test case 2025-01-27 18:12:25 +09:00
Junegunn Choi
a2aa1a156c Allow {q} placeholders with range expressions
e.g. {q:1}, {q:2..}
2025-01-27 18:04:57 +09:00
Junegunn Choi
2f8a72a42a More match_count fixes 2025-01-27 15:22:39 +09:00
Junegunn Choi
8179ca5eaa Fix edge cases in --bind where ',' or ':' are chained (#4206)
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-01-27 09:30:53 +09:00
Junegunn Choi
4b74f882c7 [test] Prefer match_count over item_count
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
match_count can lag behind item_count and can cause intermittent failures.
2025-01-27 02:08:52 +09:00
Junegunn Choi
7cf45af502 Add --bind example (multi-key binding) 2025-01-27 02:08:39 +09:00
Junegunn Choi
46c21158d8 Update CHANGELOG 2025-01-27 01:52:24 +09:00
Junegunn Choi
80da0776f8 Allow actions to multiple keys and events at once
Close #4206
2025-01-27 01:46:21 +09:00
Junegunn Choi
e91f10ab16 Enhance click-header event
* Expose the name of the mouse action as $FZF_KEY
* Trigger click-header on mouse up
* Enhanced clickable header for `kill` completion
2025-01-27 01:10:08 +09:00
Junegunn Choi
2c15cd7923 [completion] Make kill completion header clickable
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-01-26 16:11:15 +09:00
Junegunn Choi
d6584543e9 Make click-header export $FZF_CLICK_HEADER_{NTH,WORD} 2025-01-26 15:37:42 +09:00
junegunn
c13228f346 Deploying to master from @ junegunn/fzf@7220d8233e 🚀
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-01-26 00:02:08 +00:00
Junegunn Choi
7220d8233e Add 'search' and 'transform-search'
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 #4202
2025-01-26 01:50:08 +09:00
Junegunn Choi
0237bf09bf Split integration test file (#4205)
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-01-25 19:57:40 +09:00
Junegunn Choi
04017c25bb Add 'bell' action to ring the terminal bell
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-01-25 11:22:32 +09:00
Junegunn Choi
02199cd609 Update CHANGLOG 2025-01-25 10:58:24 +09:00
bitraid
26b9f5831a [fish] Fix compatibility with v3.1.2 - v3.3.1 (#4200)
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
Don't use the command substitution syntax: $(cmd)

Fix #4196
2025-01-24 17:15:43 +09:00
Junegunn Choi
243a76002c Option to prioritize file name matches (#4192)
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
* 'pathname' is a new tiebreak option for prioritizing matches occurring
  in the file name of the path.

* `--scheme=path` will automatically set `--tiebreak=pathname,length`.

* fzf will automatically choose `path` scheme when the input is a TTY device,
  where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND`
  which is usually a command for listing files.

Close #4191
2025-01-24 00:54:53 +09:00
Junegunn Choi
c71e4ddee4 Make it possible to change one-time preview window
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-01-23 18:45:36 +09:00
Junegunn Choi
32eb8c1be9 Fix resizing of a one-time preview window 2025-01-23 18:41:06 +09:00
Junegunn Choi
c587017830 Fix header window location and size 2025-01-23 14:45:36 +09:00
Junegunn Choi
fb885652cc Fix RuboCop errors
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-01-23 09:43:12 +09:00
Junegunn Choi
afc2f05e5e Fix --info-command when focus event is bound
Fix #4198
2025-01-23 09:31:51 +09:00
Junegunn Choi
06547d0cbe Add --header-lines-border to separate two headers
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
Examples:
  # Border only around the header from --header-lines
  seq 10 | fzf --header 'hello' --header-lines 2 --header-lines-border

  # Both headers with borders
  seq 10 | fzf --header 'hello' --header-lines 2 --header-border --header-lines-border

  # Use 'none' to still separate two headers but without a border
  seq 10 | fzf --header 'hello' --header-lines 2 --header-border --header-lines-border none --list-border
2025-01-23 01:39:57 +09:00
Junegunn Choi
578108280e Support OSC 8 sequence with BEL characters
Fix #4193
2025-01-22 19:16:08 +09:00
Junegunn Choi
65db7352b7 0.58.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-01-20 02:00:03 +09:00
Junegunn Choi
a4db8bd7b5 Make 'current-fg' inherit from 'fg' to simplify configuration
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
If you do not want 'current-fg' to inherit attributes of 'fg', prefix it
with 'regular:' to reset them.

  # italic and underline
  fzf --color fg:italic,current-fg:underline

  # only underline
  fzf --color fg:italic,current-fg:regular:underline
2025-01-20 01:02:58 +09:00
dependabot[bot]
f1c1b02d77 Bump github.com/gdamore/tcell/v2 from 2.7.4 to 2.8.1 (#4175)
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.7.4 to 2.8.1.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.7.4...v2.8.1)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  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-01-20 01:01:59 +09:00
Elliott Sales de Andrade
6580f32b43 Fix a non-constant format string (#4189)
Go 1.24 now has a vet check about this that causes `go test` to fail:
https://github.com/golang/go/issues/60529
2025-01-20 00:32:50 +09:00
Junegunn Choi
b028cbd8bd Clarify print(...) action
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-01-19 13:55:35 +09:00
junegunn
a1a5418318 Deploying to master from @ junegunn/fzf@5a32634b74 🚀
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-01-19 00:02:12 +00:00
bitraid
5a32634b74 [fish] Allow setting multi-select and list reload for history (#4179)
* [fish] Drop support for versions older than 3.0b1

* [fish] Use `set` instead of `read` for `$result`

This effectively makes CTRL-R non-blocking (the previous attempt was
unsuccessful).

* [fish] Allow FZF_CTRL_R_OPTS to set multi-select
2025-01-19 01:38:18 +09:00
Junegunn Choi
c1875af70b Add 'gap-line' color for the horizontal line on each gap
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
Color inheritance: border >> list-border >> gap-line
2025-01-18 13:48:46 +09:00
Junegunn Choi
0a10d14e19 [fish] CTRL-R: Make loading non-blocking
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-01-18 02:33:28 +09:00
Junegunn Choi
ed8ceec66f Add FZF_NTH to man page
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-01-17 23:17:58 +09:00
piguagua
03760011d7 chore: fix comment (#4181)
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
Signed-off-by: piguagua <piguagua@aliyun.com>
2025-01-17 14:31:07 +09:00
Junegunn Choi
0d5aebb806 Allow setting border styles at once with --style full:STYLE 2025-01-17 13:12:51 +09:00
Junegunn Choi
1313510890 Do not apply nth style when the whole range is covered
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-01-16 10:06:11 +09:00
Junegunn Choi
b712f2bb6a Export the current nth value as $FZF_NTH 2025-01-16 09:23:25 +09:00
Junegunn Choi
938c15ec63 Skip merging nth offsets when unnecessary 2025-01-16 09:05:59 +09:00
Junegunn Choi
3e7f032ec2 Allow displaying --nth parts in a different text style
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
Close #4183
2025-01-16 01:38:45 +09:00
Junegunn Choi
b42f5bfb19 Add --gap-line to --help output and man page
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-01-15 23:40:42 +09:00
Junegunn Choi
717562b264 Disallow incorrect wrapping range expression for --nth 2025-01-15 22:39:48 +09:00
Junegunn Choi
9d6637c1b3 Add gap line
Close #4182
2025-01-15 22:23:52 +09:00
Junegunn Choi
56fef7c8df Simplify nth comparison when reusing transformed tokens
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-01-13 17:37:50 +09:00
Junegunn Choi
ba0935c71f Fix change-nth
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
* Proper clean-up of caches
* Force rerender list after the action
2025-01-13 12:45:01 +09:00
Junegunn Choi
d83eb2800a Add change-nth action
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
Example:
  # Start with --nth 1, then 2, then 3, then back to the default, 1
  echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo

Close #4172
Close #3109
2025-01-13 00:13:31 +09:00
Junegunn Choi
6f943112a9 Align header with the list
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-01-12 14:58:55 +09:00
Junegunn Choi
f422893b8e Add --style to the CHANGELOG
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-01-12 10:29:15 +09:00
bitraid
22b498489c [fish] Optimize history formatting without perl (#4171) 2025-01-12 10:27:26 +09:00
Junegunn Choi
5460517bd2 Treat a single-character delimiter as a plain string delimiter
even if it's a regular expression meta-character

Close #4170
2025-01-12 10:23:43 +09:00
junegunn
9a6e557e52 Deploying to master from @ junegunn/fzf@4fdc07927f 🚀 2025-01-12 00:02:26 +00:00
Junegunn Choi
4fdc07927f Refactor --preview-border=line
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
2025-01-11 19:34:26 +09:00
Junegunn Choi
9030b67e4f Fix window sizing with borders on the right
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-01-11 11:39:51 +09:00
Junegunn Choi
43eafdf4b7 Fix preview scrollbar with '--preview-window bottom,border-line'
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-01-11 00:53:07 +09:00
Junegunn Choi
dfb88edb5e Make preview-scrollbar color conditionally inherit from scrollbar color 2025-01-11 00:51:49 +09:00
Junegunn Choi
bd3e65df4d Trim unsupported OSC sequences (#4169)
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 #4169
2025-01-10 20:53:47 +09:00
Junegunn Choi
d7b13f3408 Add a test case for the mixed delimiter ANSI sequence (#4169) 2025-01-10 20:31:51 +09:00
Junegunn Choi
14ef8e8051 Support ANSI sequences with mixed ; and : delimiters (#4169)
`make bench` shows no loss of performance.
2025-01-10 17:43:13 +09:00
bitraid
cc1d9f124e [fish] Fix history formatting when perl is missing (#4166)
Don't add tab after escaped newline.
2025-01-10 14:03:21 +09:00
Kid
93c0299606 [fish] remove defunct bind feature detection (#4165)
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-01-09 19:16:24 +09:00
Junegunn Choi
55e3c73221 fzf-preview.sh: Support FILEPATH:LINE[:COL] argument 2025-01-09 17:00:46 +09:00
Junegunn Choi
6783417504 Do not export $LINES and $COLUMNS for non-preview processes
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 #4164
2025-01-08 10:00:57 +09:00
Junegunn Choi
fa3f706e71 Refactor option parser
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-01-07 19:16:41 +09:00
Junegunn Choi
9c2f6cae88 Fix adaptive height with --header-border 2025-01-07 19:16:16 +09:00
Junegunn Choi
a30181e240 Update man page sections
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-01-07 00:20:36 +09:00
dependabot[bot]
b4ccf64e62 Bump golang.org/x/term from 0.27.0 to 0.28.0 (#4162)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/term/compare/v0.27.0...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 23:36:41 +09:00
Junegunn Choi
88d768bf6b Restructure --help output 2025-01-06 23:34:14 +09:00
Junegunn Choi
6444cc7905 Render preview label if possible when --preview-border=line
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-01-06 10:09:59 +09:00
Junegunn Choi
328af1f397 Remove header indentation when unnecessary
# Indent the header to align with the entries in the list
  fzf --header 'Hello' --header-border --list-border

  # No extra indentation required
  fzf --header 'Hello' --header-border
2025-01-06 09:57:58 +09:00
Junegunn Choi
5ae60e2e80 Add style presets: --style=[default|minimal|full]
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
Close #4160
2025-01-06 02:10:44 +09:00
Junegunn Choi
0e0b868342 Add preview border style 'line'
It draws a single line between the preview window and the rest of the
interface. i.e. automatically choose between 'left', 'right', 'top', and
'bottom' depending on the position of the preview window.
2025-01-06 00:44:59 +09:00
Junegunn Choi
a5beb08ed7 Border around the header section
Close #4159
2025-01-05 23:02:52 +09:00
Junegunn Choi
45fc7b903d [install] Unset FZF_DEFAULT_OPTS when checking the binary
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-01-05 11:33:40 +09:00
junegunn
4f2c274942 Deploying to master from @ junegunn/fzf@93415493b4 🚀
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-01-05 00:02:19 +00:00
phanium
93415493b4 fix: make header align with list (#4158)
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-01-05 01:13:23 +09:00
Junegunn Choi
8e4d338de9 Fix adaptive height in the presence of --list-border and --input-border
seq 10 | fzf --height=~100%
2025-01-04 19:19:18 +09:00
Junegunn Choi
8a71e091a8 Fix '--tmux border-native' 2025-01-04 18:47:00 +09:00
Andreas Auernhammer
120cd7f25a Add border-native option to --tmux flag (#4157)
This commit adds the `border-native` resulting in the following:

```
--tmux[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]
```

By default, when not specified, the `-B` flag is passed to the
`tmux popup-window` command such that no border is drawn around
the tmux popup window.

When the `border-native` option is present, the `-B` flag is omitted
and the popup window is drawn using the border style configured in
the tmux config file.

Fixes #4156

Signed-off-by: Andreas Auernhammer <github@aead.dev>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-01-04 18:30:32 +09:00
Junegunn Choi
fb3bf6c984 Fix cursor placement of tcell renderer
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
2025-01-03 19:56:07 +09:00
dependabot[bot]
d57e1f8baa Bump crate-ci/typos from 1.28.2 to 1.28.4 (#4141)
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
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.2 to 1.28.4.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.2...v1.28.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-03 13:49:18 +09:00
Minseo Kim
15ca9ad8eb Replace bash to sh in Makefile (#4138)
Some operating systems do not ship with bash by default, e.g. BSDs,
which breaks the build.
2025-01-03 13:48:51 +09:00
Junegunn Choi
c2e1861747 Update --help output
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-01-02 23:54:59 +09:00
Junegunn Choi
543d41f3dd Do not try to print anything is screen height is zero 2025-01-02 23:44:47 +09:00
Junegunn Choi
e5cfc988ec Fix RuboCop error
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-01-02 16:55:56 +09:00
Junegunn Choi
ee3916be17 Border around the input section (prompt + info)
Close #4154
2025-01-02 16:25:00 +09:00
Junegunn Choi
fd513f8af8 Add missing --list-border=* parser
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
Patch suggested by @bitraid
2024-12-31 19:39:46 +09:00
Junegunn Choi
9a2b7f559c Add --list-border for additional border around the list section
Close #4148
2024-12-31 17:05:14 +09:00
junegunn
b8d2b0df7e Deploying to master from @ junegunn/fzf@fe3a9c603e 🚀
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
2024-12-29 00:02:16 +00:00
Hong Xu
fe3a9c603e fzf-preview.sh: Don't include the file name in type information (#4143)
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
Reduce the changes of misjudging the type, e.g., when file is under an `image/`
directory.
2024-12-26 14:58:10 +09:00
junegunn
97030d4cb1 Deploying to master from @ junegunn/fzf@b2c3e567da 🚀
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
2024-12-22 00:02:14 +00:00
bitraid
b2c3e567da [fish] Partly revert change of 0167691 (#4137)
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
Don't use the `-f` switch of `string split`, because it was added in
fish version 3.2.0.
2024-12-20 10:05:09 +09:00
Junegunn Choi
ca5e633399 Add toggle-hscroll
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
2024-12-19 21:05:26 +09:00
Junegunn Choi
e60a9a628b Add toggle-multi-line action 2024-12-19 21:05:26 +09:00
bitraid
0167691941 [fish] Small syntax modification of some commands
No actual change, just for consistency with the rest of the code.
2024-12-19 20:50:04 +09:00
bitraid
3b0f976380 [fish] Enable home dir expansion of leading ~/
Enable expanding to user's home directory, when pressing <Ctrl-T> or
<Alt-C>, and the current command line token starts with `~/`.
2024-12-19 20:50:04 +09:00
bitraid
7bd298b536 [fish] Don't strip leading dot (.) character
Fix the removal of the leading dot character from the query, when
<Ctrl-T> was pressed and the current command line token started with a
dot. It was also removed when <Alt-C> was pressed and the directory
didn't exist under the current path.
2024-12-19 20:50:04 +09:00
Junegunn Choi
0476a65fca 0.57.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
2024-12-15 17:04:04 +09:00
junegunn
2cb2af115a Deploying to master from @ junegunn/fzf@789226ff6d 🚀
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
2024-12-15 00:02:31 +00:00
Junegunn Choi
789226ff6d Fix test failure
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
cdcab26 removed excessive clearing of the windows. But it caused the
problem where the right side of the preview window border was not
cleared when hiding the preview window with the scrollbar disabled.
2024-12-14 22:42:40 +09:00
Junegunn Choi
805efc5bf1 Remove unused interface 2024-12-14 22:31:39 +09:00
Junegunn Choi
cdcab26766 Fix redundant clearing of the windows with non-default bg color 2024-12-14 22:06:14 +09:00
Junegunn Choi
ec3acb1932 Update CHANGELOG
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
2024-12-12 13:53:58 +09:00
Junegunn Choi
d30e37434e Less flickering of the candidate list when resizing the preview window 2024-12-12 13:53:08 +09:00
Junegunn Choi
20d5b2e20e Avoid redrawing the windows on the first click on the border 2024-12-12 13:53:08 +09:00
Junegunn Choi
6c6be4ab1a Simplify resize code 2024-12-12 13:53:08 +09:00
Junegunn Choi
d004eb1f7c Redraw preview scrollbar when window width changes 2024-12-12 13:53:08 +09:00
Junegunn Choi
3148b0f3e8 Restore previous behavior 2024-12-12 13:53:08 +09:00
Junegunn Choi
3fc0bd26a5 Disallow dragging the wrong sides of the border 2024-12-12 13:53:08 +09:00
Junegunn Choi
6c9025ff17 Update comments 2024-12-12 13:53:08 +09:00
Junegunn Choi
289997e373 Refactor 2024-12-12 13:53:08 +09:00
Junegunn Choi
db44cbdff0 Change test case expectation (hard-coded minimum width removed) 2024-12-12 13:53:08 +09:00
Junegunn Choi
da9179335c Respect the properties of the currently active preview window options 2024-12-12 13:53:08 +09:00
Julian Prein
cdf641fa3e Use Has{Top,Right,Bottom,Left}() where possible
De-duplicate code and reduce the amount of code that has to be changed
when new BorderShapes are being added. This also adds and uses the
missing HasBottom().
2024-12-12 13:53:08 +09:00
Julian Prein
66dbee10f5 Fix minimum preview width without left/right borders
When the chosen preview border shape has no left and/or right border,
the minimum total preview window size decreases. But due to the
hardcoded value for the minimum size of the preview window the size
could not be decreased further than 5.
2024-12-12 13:53:08 +09:00
Julian Prein
19e9b620ba Fix maximum preview height without horizontal separator
The minimum window height decreases when no extra line for the
horizontal separator is used (e.g. with `--info=inline --no-separator`).
In this case the preview window should be able to occupy this extra
line.
2024-12-12 13:53:08 +09:00
Julian Prein
e4e4700aff Make the preview window resizable by mouse drag
Enable resizing the preview window by dragging its border with the
mouse. This works with all border styles except for `none`.
Counter-intuitively, having the border only on the opposite side of the
window works too - dragging from it will first decrease the preview size
to its minimum.
2024-12-12 13:53:08 +09:00
dependabot[bot]
bb55045596 Bump golang.org/x/term from 0.26.0 to 0.27.0 (#4124)
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
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/term/compare/v0.26.0...v0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 23:06:49 +09:00
dependabot[bot]
d7e51cdeb5 Bump crate-ci/typos from 1.28.1 to 1.28.2 (#4123)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.1 to 1.28.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.1...v1.28.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 23:06:32 +09:00
junegunn
7f4964b366 Deploying to master from @ junegunn/fzf@a6957aba11 🚀
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
2024-12-08 00:02:15 +00:00
LangLangBart
a6957aba11 chore: completion test command sequence (#4115)
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
cleanup zsh global scope
2024-12-03 20:34:26 +09:00
dependabot[bot]
b5f94f961d Bump crate-ci/typos from 1.27.3 to 1.28.1 (#4114)
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
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.3 to 1.28.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.27.3...v1.28.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 00:25:33 +09:00
Junegunn Choi
e182d3db7a Fix line wrap toggle when switching between screens
Fix #4099
2024-12-02 22:25:23 +09:00
Junegunn Choi
3e6e0528a6 [install] grep -> \grep
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
2024-12-01 23:22:36 +09:00
buttering
ac508a1ce4 Enhance install script to handle commented and uncommented lines (#3632) (#4112)
* Enhance install script to handle commented and uncommented lines (#3632)

Resolves #3632

Enhance install script to handle commented and uncommented lines in shell file with user prompts for modification.
- Track commented and uncommented lines in the file.
- Prompt user to append or skip if the line is commented.
- Ensure new lines are added only when necessary, based on user input.
- To the `fish_user_key_bindings.fish`, the original logic would append the line to the end if no corresponding statement was found. I’ve adopted the same behavior for commented lines.

* Refactor append_line function to improve line existence check.

- Replaced `lno` variable with `lines` to store matching lines and simplified the logic.
- Improved line existence check, now prints all matching lines directly and handles commented lines separately.
- Removed unnecessary variables like `all_commented`, `commented_lines`, and `non_commented_lines`.

* Fix indentation

---------

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-12-01 23:21:12 +09:00
junegunn
d7fc1e09b1 Deploying to master from @ junegunn/fzf@3b0c86e401 🚀
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
2024-12-01 00:02:24 +00:00
Junegunn Choi
3b0c86e401 Much faster image 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
Fix #3984
2024-11-29 00:26:12 +09:00
Junegunn Choi
61d10d8ffa Update README and CHANGELOG
Close #4022
2024-11-28 19:46:56 +09:00
Junegunn Choi
7d9548919e Extend --walker-skip to support multi-component patterns
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
fzf --walker-skip 'foo/bar'

Close #4107
2024-11-26 17:26:16 +09:00
msabathier
bee80a730f Allow walking multiple root directories (#4109)
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
Co-authored-by: Martin Sabathier <martin.sabathier.ext@corys.fr>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-11-25 19:25:30 +09:00
Junegunn Choi
ac3e24c99c Export FZF_PREVIEW_* variables to other processes as well
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
Close #4098
2024-11-24 18:49:10 +09:00
junegunn
e7e852bdb3 Deploying to master from @ junegunn/fzf@2b7f168571 🚀
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
2024-11-24 00:03:09 +00:00
bitraid
2b7f168571 [fish] Enable keys for scripts that use read
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
Remove the check that exits when the shell is non-interactive, so that
the key bindings will work for the read command, when used by scripts.
2024-11-18 19:08:34 +09:00
bitraid
5b3da1d878 [fish] Use more native syntax
Mainly, replace [ with test. Also, change $FZF_TMUX check from numeric
to string, so that it won't show error if doesn't contain a number.
2024-11-18 19:08:34 +09:00
bitraid
99f1bc0177 [fish] Format history using builtins if perl is missing 2024-11-18 19:08:34 +09:00
bitraid
ed76f076dd [fish] Replace external commands with builtins
- Use `string collect` instead of cat to get the contents of
  $FZF_DEFAULT_OPTS_FILE. Also, check if the file is readable first.
- Use `string split` instead of cut to set $FISH_MAJOR, $FISH_MINOR.
- Use `string replace` instead of perl to strip leading tabs.
2024-11-18 19:08:34 +09:00
bitraid
4d357d1063 [fish] Improve commandline parsing
- Enable using unescaped quotes for exact-match, exact-boundary-match.
- Enable suffix-exact-match.
- Enable inverse-exact-match, inverse-prefix/suffix-exact-match.
- Allow searching for double quotes and backslashes.
- Combine multiple consecutive slashes into one.
- Workaround for test command bug, allowing $dir or $commandline be a
  single `!`.
2024-11-18 19:08:34 +09:00
junegunn
961ae1541c Deploying to master from @ junegunn/fzf@add1aec685 🚀
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
2024-11-17 00:02:20 +00:00
Junegunn Choi
add1aec685 0.56.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
Generate Sponsors README / deploy (push) Has been cancelled
2024-11-15 10:06:01 +09:00
LangLangBart
03d6ba7496 fix(zsh): handle backtick trigger edge case (#4090)
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
2024-11-14 16:07:52 +09:00
LangLangBart
71e4d5cc51 revert(zsh): remove 'fc -RI' call in the history widget (#4093) 2024-11-14 10:38:05 +09:00
110 changed files with 19623 additions and 7788 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

64
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
go:
- changed-files:
- any-glob-to-any-file:
- src/**
- main.go
- go.mod
- go.sum
shell:
- changed-files:
- any-glob-to-any-file:
- shell/**
bash:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.bash
zsh:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.zsh
fish:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.fish
vim:
- changed-files:
- any-glob-to-any-file:
- plugin/**
docs:
- changed-files:
- any-glob-to-any-file:
- '*.md'
- doc/**
- man/**
ci:
- changed-files:
- any-glob-to-any-file:
- .github/**
build:
- changed-files:
- any-glob-to-any-file:
- Makefile
- .goreleaser.yml
- Dockerfile
test:
- changed-files:
- any-glob-to-any-file:
- test/**
- src/**/*_test.go
install:
- changed-files:
- any-glob-to-any-file:
- install
- install.ps1
- uninstall

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

17
.github/workflows/labeler.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Label PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
configuration-path: .github/labeler.yml

View File

@@ -1,5 +1,5 @@
---
name: Test fzf on Linux
name: build
on:
push:
@@ -16,33 +16,33 @@ env:
jobs:
build:
runs-on: ubuntu-latest
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.1.0
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: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
run: bundle install
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
run: make lint
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose

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: crate-ci/typos@v1.27.3
- uses: actions/checkout@v5
- uses: crate-ci/typos@v1.29.4

2
.gitignore vendored
View File

@@ -3,7 +3,6 @@ bin/fzf.exe
dist
target
pkg
Gemfile.lock
.DS_Store
doc/tags
vendor
@@ -12,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,9 +1,13 @@
AllCops:
NewCops: enable
Layout/LineLength:
Enabled: false
Metrics:
Enabled: false
Lint/ShadowingOuterLocalVariable:
Enabled: false
Lint/NestedMethodDefinition:
Enabled: false
Style/MethodCallWithArgsParentheses:
Enabled: true
AllowedMethods:
@@ -28,5 +32,11 @@ Style/WordArray:
MinSize: 1
Minitest/AssertEqual:
Enabled: false
Minitest/EmptyLineBeforeAssertionMethods:
Enabled: false
Naming/VariableNumber:
Enabled: false
Lint/EmptyBlock:
Enabled: false
Style/SafeNavigationChainLength:
Enabled: false

View File

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

View File

@@ -1,8 +1,8 @@
Advanced fzf examples
======================
* *Last update: 2024/06/24*
* *Requires fzf 0.54.0 or later*
* *Last update: 2025/02/02*
* *Requires fzf 0.59.0 or later*
---
@@ -22,6 +22,7 @@ Advanced fzf examples
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
* [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status)
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
![image](https://user-images.githubusercontent.com/700826/113379932-dfeac200-93b5-11eb-9e28-df1a2ee71f90.png)
*(See `Layout` section of the man page to see the full list of options)*
*(See man page to see the full list of options)*
But you definitely don't want to repeat `--height=40% --layout=reverse
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
@@ -128,7 +129,7 @@ fzf --height 70% --tmux 70%
You can also specify the position, width, and height of the popup window in
the following format:
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]`
```sh
# 100% width and 60% height
@@ -362,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}' \
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind 'enter:become(vim {1} +{2})'
```
### Controlling Ripgrep search and fzf search simultaneously
`search` and `transform-search` action allow you to trigger an fzf search with
an arbitrary query string. This frees fzf from strictly following the prompt
input, enabling custom search syntax.
In the example below, `transform` action is used to conditionally trigger
`reload` for ripgrep, followed by `search` for fzf. The first word of the
query initiates the Ripgrep process to generate the initial results, while the
remainder of the query is passed to fzf for secondary filtering.
```sh
#!/usr/bin/env bash
export TEMP=$(mktemp -u)
trap 'rm -f "$TEMP"' EXIT
INITIAL_QUERY="${*:-}"
TRANSFORMER='
rg_pat={q:1} # The first word is passed to ripgrep
fzf_pat={q:2..} # The rest are passed to fzf
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
echo "$rg_pat" > "$TEMP"
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
fi
echo "+search:$fzf_pat"
'
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--with-shell 'bash -c' \
--bind "start,change:transform:$TRANSFORMER" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
Log tailing
-----------
@@ -529,8 +568,7 @@ pods() {
--info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \
--bind 'start,ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {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,865 @@
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
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
- [walker] Append '/' to directory entries on MSYS2 (#4281)
- Trim trailing whitespaces after processing ANSI sequences (#4282)
- Remove temp files before `become` when using `--tmux` option (#4283)
- Fix condition for using item numlines cache (#4285) (@alex-huff)
- Make `--accept-nth` compatible with `--select-1` (#4287)
- Increase the query length limit from 300 to 1000 (#4292)
- [windows] Prevent fzf from consuming user input while paused (#4260)
0.60.2
------
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
- Thanks to @phanen for the fix
- Fixed 'jump' action when the pointer is an empty string
0.60.1
------
- Bug fixes and minor improvements
- Built-in walker now prints directory entries with a trailing slash
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
0.60.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
- Added `--accept-nth` for choosing output fields
```sh
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
# Becomes
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
git branch | fzf | cut -c3-
# Can be rewritten as
git branch | fzf --accept-nth -1
```
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
```sh
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
# foo, baz, bar
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
# foo,baz,bar,foo,bar
```
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
```sh
seq 100 | fzf --bind 'ctrl-x:exclude'
# 'exclude-multi' will exclude the selected items or the current item
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
```
- Preview window now prints wrap indicator when wrapping is enabled
```sh
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
```
- Bug fixes and improvements
0.59.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
- Prioritizing file name matches (#4192)
- Added a new tiebreak option `pathname` for prioritizing file name matches
- `--scheme=path` now sets `--tiebreak=pathname,length`
- fzf will automatically choose `path` scheme
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
```sh
# Use --header-lines-border to separate two headers
ps -ef | fzf --style full --layout reverse --header-lines 1 \
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
--header-lines-border bottom --no-list-border
```
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
```sh
# Click on the header line to limit search scope
ps -ef | fzf --style full --layout reverse --header-lines 1 \
--header-lines-border bottom --no-list-border \
--color fg:dim,nth:regular \
--bind 'click-header:transform-nth(
echo $FZF_CLICK_HEADER_NTH
)+transform-prompt(
echo "$FZF_CLICK_HEADER_WORD> "
)'
```
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
- `kill` completion for bash and zsh were updated to use this feature
- Added `--no-input` option to completely disable and hide the input section
```sh
# Click header to trigger search
fzf --header '[src] [test]' --no-input --layout reverse \
--header-border bottom --input-border \
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
# Vim-like mode switch
fzf --layout reverse-list --no-input \
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
--bind 'enter,esc,ctrl-c:transform:
if [[ $FZF_INPUT_STATE = enabled ]]; then
echo "rebind(j,k,/)+hide-input"
elif [[ $FZF_KEY = enter ]]; then
echo accept
else
echo abort
fi
'
```
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
```sh
export TEMP=$(mktemp -u)
trap 'rm -f "$TEMP"' EXIT
TRANSFORMER='
rg_pat={q:1} # The first word is passed to ripgrep
fzf_pat={q:2..} # The rest are passed to fzf
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
echo "$rg_pat" > "$TEMP"
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
fi
echo "+search:$fzf_pat"
'
fzf --ansi --disabled \
--with-shell 'bash -c' \
--bind "start,change:transform:$TRANSFORMER"
```
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
```sh
# Load 'ps -ef' output on start and reload it on CTRL-R
fzf --bind 'start,ctrl-r:reload:ps -ef'
```
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
```sh
# You will only see the input section which takes 3 lines
fzf --style=full --height 1% --min-height 3
# You will see 3 items in the list section
fzf --style full --height 1% --min-height 3+
```
- Shell integration scripts were updated to use `--min-height 20+` by default
- `--header-lines` will be displayed at the top in `reverse-list` layout
- Added `bell` action to ring the terminal bell
```sh
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
```
- Added `toggle-bind` action
- Bug fixes and improvements
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
0.58.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
This version introduces three new border types, `--list-border`, `--input-border`, and `--header-border`, offering much greater flexibility for customizing the user interface.
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-4-borders.png" />
Also, fzf now offers "style presets" for quick customization, which can be activated using the `--style` option.
| Preset | Screenshot |
| :--- | :--- |
| `default` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-default.png"/> |
| `full` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-full.png"/> |
| `minimal` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-minimal.png"/> |
- Style presets (#4160)
- `--style=full[:BORDER_STYLE]`
- `--style=default`
- `--style=minimal`
- Border and label for the list section (#4148)
- Options
- `--list-border[=STYLE]`
- `--list-label=LABEL`
- `--list-label-pos=COL[:bottom]`
- Colors
- `list-fg`
- `list-bg`
- `list-border`
- `list-label`
- Actions
- `change-list-label`
- `transform-list-label`
- Border and label for the input section (prompt line and info line) (#4154)
- Options
- `--input-border[=STYLE]`
- `--input-label=LABEL`
- `--input-label-pos=COL[:bottom]`
- Colors
- `input-fg` (`query`)
- `input-bg`
- `input-border`
- `input-label`
- Actions
- `change-input-label`
- `transform-input-label`
- Border and label for the header section (#4159)
- Options
- `--header-border[=STYLE]`
- `--header-label=LABEL`
- `--header-label-pos=COL[:bottom]`
- Colors
- `header-fg` (`header`)
- `header-bg`
- `header-border`
- `header-label`
- Actions
- `change-header-label`
- `transform-header-label`
- Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
- Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
- fzf will now render a dashed line (`┈┈`) in each `--gap` for better visual separation.
```sh
# All bash/zsh functions, highlighted
declare -f |
perl -0 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --layout reverse --multi --highlight-line --gap
```
* You can customize the line using `--gap-line[=STR]`.
- You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
```sh
fzf --tmux border-native --bind 'enter:execute:less {}'
```
- Added `toggle-multi-line` action
- Added `toggle-hscroll` action
- Added `change-nth` action for dynamically changing the value of the `--nth` option
```sh
# Start with --nth 1, then 2, then 3, then back to the default, 1
echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo
```
- `--nth` parts of each line can now be rendered in a different text style
```sh
# nth in a different style
ls -al | fzf --nth -1 --color nth:italic
ls -al | fzf --nth -1 --color nth:reverse
ls -al | fzf --nth -1 --color nth:reverse:bold
# Dim the other parts
ls -al | fzf --nth -1 --color nth:regular,fg:dim
# With 'change-nth'. The current nth option is exported as $FZF_NTH.
ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
--color nth:regular,fg:dim \
--bind 'ctrl-n:change-nth(8..|1|2|3|4|5|6|7|)' \
--bind 'result:transform-prompt:echo "${FZF_NTH}> "'
```
- A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character.
- This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
- Bug fixes
- Bug fixes and improvements in fish scripts (thanks to @bitraid)
0.57.0
------
- You can now resize the preview window by dragging the border
- Built-in walker improvements
- `--walker-root` can take multiple directory arguments. e.g. `--walker-root include src lib`
- `--walker-skip` can handle multi-component patterns. e.g. `--walker-skip target/build`
- Removed long processing delay when displaying images in the preview window
- `FZF_PREVIEW_*` environment variables are exported to all child processes (#4098)
- Bug fixes in fish scripts
0.56.3
------
- Bug fixes in zsh scripts
- fix(zsh): handle backtick trigger edge case (#4090)
- revert(zsh): remove 'fc -RI' call in the history widget (#4093)
- Thanks to @LangLangBart for the contributions
0.56.2
------
- Bug fixes
@@ -150,7 +1009,7 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
```sh
# Now this will work as expected. Previously, this would print an invalid header line.
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
# `load` event would fire and the header would be prematurely updated.
fzf --header 'Loading ...' --header-lines 1 \
--bind 'start:reload:sleep 1; ps -ef' \

View File

@@ -1,5 +1,5 @@
FROM ubuntu:24.04
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
FROM rubylang/ruby:3.4.1-noble
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
RUN gem install --no-document -v 5.22.3 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile
@@ -9,4 +9,4 @@ RUN rm -f /etc/bash.bashrc
COPY . /fzf
RUN cd /fzf && make install && ./install --all
ENV LANG=C.UTF-8
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]

8
Gemfile Normal file
View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'minitest', '5.25.4'
gem 'rubocop', '1.71.0'
gem 'rubocop-minitest', '0.36.0'
gem 'rubocop-performance', '1.23.1'

47
Gemfile.lock Normal file
View File

@@ -0,0 +1,47 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.9.1)
language_server-protocol (3.17.0.3)
minitest (5.25.4)
parallel (1.26.3)
parser (3.3.7.0)
ast (~> 2.4.1)
racc
racc (1.8.1)
rainbow (3.1.1)
regexp_parser (2.10.0)
rubocop (1.71.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (1.13.0)
unicode-display_width (2.6.0)
PLATFORMS
arm64-darwin-23
ruby
DEPENDENCIES
minitest (= 5.25.4)
rubocop (= 1.71.0)
rubocop-minitest (= 0.36.0)
rubocop-performance (= 1.23.1)
BUNDLED WITH
2.6.2

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,11 +1,20 @@
SHELL := bash
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
@@ -14,7 +23,7 @@ endif
ifeq ($(VERSION),)
$(error Not on git repository; cannot determine $$FZF_VERSION)
endif
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION
@@ -83,12 +92,20 @@ test: $(SOURCES)
github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util
itest:
ruby test/runner.rb
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
lint: $(SOURCES) test/test_go.rb
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
rubocop --require rubocop-minitest --require rubocop-performance
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
@@ -176,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 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

287
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

@@ -9,12 +9,24 @@
# - https://iterm2.com/utilities/imgcat
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
>&2 echo "usage: $0 FILENAME[:LINENO][:IGNORED]"
exit 1
fi
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
center=0
if [[ ! -r $file ]]; then
if [[ $file =~ ^(.+):([0-9]+)\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
file=${BASH_REMATCH[1]}
center=${BASH_REMATCH[2]}
elif [[ $file =~ ^(.+):([0-9]+):[0-9]+\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
file=${BASH_REMATCH[1]}
center=${BASH_REMATCH[2]}
fi
fi
type=$(file --brief --dereference --mime -- "$file")
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
@@ -32,28 +44,28 @@ if [[ ! $type =~ image/ ]]; then
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never --highlight-line="${center:-0}" -- "$file"
exit
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))
fi
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. Use icat (from Kitty) if kitten is installed
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then

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:

18
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.7.4
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
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.27.0
golang.org/x/term v0.26.0
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
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.15 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
golang.org/x/text v0.28.0 // indirect
)
go 1.20
go 1.23.0

34
go.sum
View File

@@ -1,19 +1,18 @@
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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
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.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=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-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=
@@ -35,21 +34,20 @@ 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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=

215
install
View File

@@ -2,7 +2,7 @@
set -u
version=0.56.2
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
@@ -83,7 +83,7 @@ ask() {
check_binary() {
echo -n " - Checking fzf executable ... "
local output
output=$("$fzf_base"/bin/fzf --version 2>&1)
output=$(FZF_DEFAULT_OPTS= "$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
@@ -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
@@ -295,35 +297,49 @@ EOF
fi
append_line() {
set -e
local update line file pat lno
local update line file pat lines
update="$1"
line="$2"
file="$3"
pat="${4:-}"
lno=""
at_lno="${5:-}"
lines=""
echo "Update $file:"
echo " - $line"
if [ -f "$file" ]; then
if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $pat ]]; then
lines=$(\grep -nF "$pat" "$file")
else
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
fi
fi
if [ -n "$lno" ]; then
echo " - Already exists: line #$lno"
else
if [ $update -eq 1 ]; then
if [ -n "$lines" ]; then
echo " - Already exists:"
sed 's/^/ Line /' <<< "$lines"
update=0
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
echo " - But they all seem to be commented"
ask " - Continue modifying $file?"
update=$?
fi
fi
set -e
if [ "$update" -eq 1 ]; then
if [[ -z $at_lno ]]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
echo " + Added"
else
echo " ~ Skipped"
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
fi
echo " + Added"
else
echo " ~ Skipped"
fi
echo
set +e
}
@@ -346,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.56.2"
$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.56"
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 "Nov 2024" "fzf 0.56.2" "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

File diff suppressed because it is too large Load Diff

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
@@ -1081,7 +1103,7 @@ endfunction
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = { 'options': ['--multi'] }
let opts = { 'options': ['--multi', '--scheme', 'path'] }
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
if s:is_win && !&shellslash

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%} --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
@@ -409,15 +480,41 @@ _fzf_complete_kill() {
}
_fzf_proc_completion() {
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
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
)
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( ${FZF_NTH//,/ } )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=${new_nths[*]}
new_nths=${new_nths// /,}
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_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
)
}
_fzf_proc_completion_post() {
command awk '{print $2}'
__fzf_exec_awk '{print $2}'
}
# To use custom hostname lists, override __fzf_list_hosts.
@@ -434,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
@@ -452,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
}
@@ -546,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
}
@@ -588,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 "--height ${FZF_TMUX_HEIGHT:-40%} --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 _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@"
@@ -122,11 +143,10 @@ __fzf_comprun() {
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
__fzf_extract_command() {
setopt localoptions noksh_arrays
# Control completion with the "compstate" parameter, insert and list noting
# Control completion with the "compstate" parameter, insert and list nothing
compstate[insert]=
compstate[list]=
cmd_word="${words[1]}"
cmd_word="${(Q)words[1]}"
}
__fzf_generic_path_completion() {
@@ -154,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
@@ -243,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
@@ -267,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
}
@@ -291,7 +353,33 @@ _fzf_complete_unalias() {
}
_fzf_complete_kill() {
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( ${FZF_NTH//,/ } )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=${new_nths[*]}
new_nths=${new_nths// /,}
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_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
@@ -299,20 +387,13 @@ _fzf_complete_kill() {
}
_fzf_complete_kill_post() {
awk '{print $2}'
__fzf_exec_awk '{print $2}'
}
fzf-completion() {
trap 'unset cmd_word' EXIT
local tokens prefix trigger tail matches lbuf d_cmds
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# Check if at least one completion system (old or new) is active
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
if ! zmodload -e zsh/compctl; then
zmodload -i zsh/compctl
fi
fi
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER})
@@ -323,7 +404,7 @@ fzf-completion() {
# Explicitly allow for empty trigger.
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
@@ -338,9 +419,26 @@ fzf-completion() {
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
# Make the 'cmd_word' global
zle __fzf_extract_command || :
[[ -z "$cmd_word" ]] && return
{
cursor_pos=$CURSOR
# Move the cursor before the trigger to preserve word array elements when
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
CURSOR=$((cursor_pos - ${#trigger} - 1))
# Check if at least one completion system (old or new) is active.
# If at least one user-defined completion widget is detected, nothing will
# be completed if neither the old nor the new completion system is enabled.
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
zmodload -F zsh/compctl 2>/dev/null
fi
# Create a completion widget to access the 'words' array (man zshcompwid)
zle -C __fzf_extract_command .complete-word __fzf_extract_command
zle __fzf_extract_command
} always {
CURSOR=$cursor_pos
# Delete the completion widget
zle -D __fzf_extract_command 2>/dev/null
}
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@@ -348,7 +446,7 @@ fzf-completion() {
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd_word} > /dev/null"; then
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
zle reset-prompt
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
@@ -368,8 +466,6 @@ fzf-completion() {
unset binding
}
# Completion widget to gain access to the 'words' array (man zshcompwid)
zle -C __fzf_extract_command .complete-word __fzf_extract_command
# Normal widget
zle -N fzf-completion
bindkey '^I' 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%} --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,26 +7,155 @@
# - $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
status is-interactive; or exit 0
# Key bindings
# ------------
# 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
function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
# 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
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
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline)
@@ -34,59 +163,81 @@ function fzf_key_bindings
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
return
else
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- $prefix
commandline -it -- (string escape $i)
commandline -it -- ' '
end
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--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
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"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
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])
# merge history from other sessions before searching
if test -z "$fish_private_mode"
builtin history merge
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
# history's -z flag is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
if type -P perl > /dev/null 2>&1
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
and commandline -- $result
else
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
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 '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)
if test "$total_lines" -eq 1
commandline -- $result
else
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
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
commandline -f repaint
end
@@ -96,102 +247,40 @@ function fzf_key_bindings
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=dir,follow,hidden --scheme=path" \
"$FZF_ALT_C_OPTS --no-multi --print0")
if [ -n "$result" ]
cd -- $result
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
# Remove last token from commandline.
commandline -t ""
commandline -it -- $prefix
end
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
cd -- $result
commandline -rt -- $prefix
end
commandline -f repaint
end
function __fzfcmd
test -n "$FZF_TMUX"; or set FZF_TMUX 0
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
if [ -n "$FZF_TMUX_OPTS" ]
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if [ $FZF_TMUX -eq 1 ]
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
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
bind \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
end
if bind -M insert > /dev/null 2>&1
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind -M insert \ec fzf-cd-widget
end
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# eval is used to do shell expansion on paths
eval set commandline $commandline
if [ -z $commandline ]
# Default to current directory with no --query
set dir '.'
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
if [ "$dir" = "." -a (string sub -l 1 -- $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")
end
end
echo $dir
echo $fzf_query
echo $prefix
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
set dir $argv
# Strip all trailing slashes. Ignore if $dir is root dir (/)
if [ (string length -- $dir) -gt 1 ]
set dir (string replace -r '/*$' -- '' $dir)
end
# Iteratively check if dir exists and strip tail end of path
while [ ! -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")
end
echo $dir
bind -M insert \ec fzf-cd-widget
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 "--height ${FZF_TMUX_HEIGHT:-40%} --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
# CTRL-T - Paste the selected file path(s) into the command line
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
@@ -106,31 +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
# Import commands from other shells if SHARE_HISTORY is enabled, as the
# 'history' array only updates after executing a non-empty command.
selected="$(
if [[ -o sharehistory ]]; then
fc -RI
fi
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_FILE='' $(__fzfcmd))"
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,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
@@ -138,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,120 +12,182 @@ 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[actChangeHeader-17]
_ = x[actChangeMulti-18]
_ = x[actChangePreviewLabel-19]
_ = x[actChangePrompt-20]
_ = x[actChangeQuery-21]
_ = x[actClearScreen-22]
_ = x[actClearQuery-23]
_ = x[actClearSelection-24]
_ = x[actClose-25]
_ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28]
_ = x[actFatal-29]
_ = x[actForwardChar-30]
_ = x[actForwardWord-31]
_ = x[actKillLine-32]
_ = x[actKillWord-33]
_ = x[actUnixLineDiscard-34]
_ = x[actUnixWordRubout-35]
_ = x[actYank-36]
_ = x[actBackwardKillWord-37]
_ = x[actSelectAll-38]
_ = x[actDeselectAll-39]
_ = x[actToggle-40]
_ = x[actToggleSearch-41]
_ = x[actToggleAll-42]
_ = x[actToggleDown-43]
_ = x[actToggleUp-44]
_ = x[actToggleIn-45]
_ = x[actToggleOut-46]
_ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49]
_ = x[actToggleWrap-50]
_ = x[actTrackCurrent-51]
_ = x[actUntrackCurrent-52]
_ = x[actDown-53]
_ = x[actUp-54]
_ = x[actPageUp-55]
_ = x[actPageDown-56]
_ = x[actPosition-57]
_ = x[actHalfPageUp-58]
_ = x[actHalfPageDown-59]
_ = x[actOffsetUp-60]
_ = x[actOffsetDown-61]
_ = x[actOffsetMiddle-62]
_ = x[actJump-63]
_ = x[actJumpAccept-64]
_ = x[actPrintQuery-65]
_ = x[actRefreshPreview-66]
_ = x[actReplaceQuery-67]
_ = x[actToggleSort-68]
_ = x[actShowPreview-69]
_ = x[actHidePreview-70]
_ = x[actTogglePreview-71]
_ = x[actTogglePreviewWrap-72]
_ = x[actTransform-73]
_ = x[actTransformBorderLabel-74]
_ = x[actTransformHeader-75]
_ = x[actTransformPreviewLabel-76]
_ = x[actTransformPrompt-77]
_ = x[actTransformQuery-78]
_ = x[actPreview-79]
_ = x[actChangePreview-80]
_ = x[actChangePreviewWindow-81]
_ = x[actPreviewTop-82]
_ = x[actPreviewBottom-83]
_ = x[actPreviewUp-84]
_ = x[actPreviewDown-85]
_ = x[actPreviewPageUp-86]
_ = x[actPreviewPageDown-87]
_ = x[actPreviewHalfPageUp-88]
_ = x[actPreviewHalfPageDown-89]
_ = x[actPrevHistory-90]
_ = x[actPrevSelected-91]
_ = x[actPrint-92]
_ = x[actPut-93]
_ = x[actNextHistory-94]
_ = x[actNextSelected-95]
_ = x[actExecute-96]
_ = x[actExecuteSilent-97]
_ = x[actExecuteMulti-98]
_ = x[actSigStop-99]
_ = x[actFirst-100]
_ = x[actLast-101]
_ = x[actReload-102]
_ = x[actReloadSync-103]
_ = x[actDisableSearch-104]
_ = x[actEnableSearch-105]
_ = x[actSelect-106]
_ = x[actDeselect-107]
_ = x[actUnbind-108]
_ = x[actRebind-109]
_ = x[actBecome-110]
_ = x[actShowHeader-111]
_ = x[actHideHeader-112]
_ = 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[actChangeHeaderLines-22]
_ = x[actChangeFooter-23]
_ = x[actChangeHeaderLabel-24]
_ = x[actChangeFooterLabel-25]
_ = x[actChangeInputLabel-26]
_ = x[actChangeListLabel-27]
_ = x[actChangeMulti-28]
_ = x[actChangeNth-29]
_ = x[actChangePointer-30]
_ = x[actChangePreview-31]
_ = x[actChangePreviewLabel-32]
_ = x[actChangePreviewWindow-33]
_ = x[actChangePrompt-34]
_ = x[actChangeQuery-35]
_ = x[actClearScreen-36]
_ = x[actClearQuery-37]
_ = x[actClearSelection-38]
_ = x[actClose-39]
_ = x[actDeleteChar-40]
_ = x[actDeleteCharEof-41]
_ = x[actEndOfLine-42]
_ = x[actFatal-43]
_ = x[actForwardChar-44]
_ = x[actForwardWord-45]
_ = x[actForwardSubWord-46]
_ = x[actKillLine-47]
_ = x[actKillWord-48]
_ = x[actKillSubWord-49]
_ = x[actUnixLineDiscard-50]
_ = x[actUnixWordRubout-51]
_ = x[actYank-52]
_ = x[actBackwardKillWord-53]
_ = x[actBackwardKillSubWord-54]
_ = x[actSelectAll-55]
_ = x[actDeselectAll-56]
_ = x[actToggle-57]
_ = x[actToggleSearch-58]
_ = x[actToggleAll-59]
_ = x[actToggleDown-60]
_ = x[actToggleUp-61]
_ = x[actToggleIn-62]
_ = x[actToggleOut-63]
_ = x[actToggleTrack-64]
_ = x[actToggleTrackCurrent-65]
_ = x[actToggleHeader-66]
_ = x[actToggleWrap-67]
_ = x[actToggleWrapWord-68]
_ = x[actToggleMultiLine-69]
_ = x[actToggleHscroll-70]
_ = x[actToggleRaw-71]
_ = x[actEnableRaw-72]
_ = x[actDisableRaw-73]
_ = x[actTrackCurrent-74]
_ = x[actToggleInput-75]
_ = x[actHideInput-76]
_ = x[actShowInput-77]
_ = x[actUntrackCurrent-78]
_ = x[actDown-79]
_ = x[actDownMatch-80]
_ = x[actUp-81]
_ = x[actUpMatch-82]
_ = x[actPageUp-83]
_ = x[actPageDown-84]
_ = x[actPosition-85]
_ = x[actHalfPageUp-86]
_ = x[actHalfPageDown-87]
_ = x[actOffsetUp-88]
_ = x[actOffsetDown-89]
_ = x[actOffsetMiddle-90]
_ = x[actJump-91]
_ = x[actJumpAccept-92]
_ = x[actPrintQuery-93]
_ = x[actRefreshPreview-94]
_ = x[actReplaceQuery-95]
_ = x[actToggleSort-96]
_ = x[actShowPreview-97]
_ = x[actHidePreview-98]
_ = x[actTogglePreview-99]
_ = x[actTogglePreviewWrap-100]
_ = x[actTogglePreviewWrapWord-101]
_ = x[actTransform-102]
_ = x[actTransformBorderLabel-103]
_ = x[actTransformGhost-104]
_ = x[actTransformHeader-105]
_ = x[actTransformHeaderLines-106]
_ = x[actTransformFooter-107]
_ = x[actTransformHeaderLabel-108]
_ = x[actTransformFooterLabel-109]
_ = x[actTransformInputLabel-110]
_ = x[actTransformListLabel-111]
_ = x[actTransformNth-112]
_ = x[actTransformPointer-113]
_ = x[actTransformPreviewLabel-114]
_ = x[actTransformPrompt-115]
_ = x[actTransformQuery-116]
_ = x[actTransformSearch-117]
_ = x[actTrigger-118]
_ = x[actBgTransform-119]
_ = x[actBgTransformBorderLabel-120]
_ = x[actBgTransformGhost-121]
_ = x[actBgTransformHeader-122]
_ = x[actBgTransformHeaderLines-123]
_ = x[actBgTransformFooter-124]
_ = x[actBgTransformHeaderLabel-125]
_ = x[actBgTransformFooterLabel-126]
_ = x[actBgTransformInputLabel-127]
_ = x[actBgTransformListLabel-128]
_ = x[actBgTransformNth-129]
_ = x[actBgTransformPointer-130]
_ = x[actBgTransformPreviewLabel-131]
_ = x[actBgTransformPrompt-132]
_ = x[actBgTransformQuery-133]
_ = x[actBgTransformSearch-134]
_ = x[actBgCancel-135]
_ = x[actSearch-136]
_ = x[actPreview-137]
_ = x[actPreviewTop-138]
_ = x[actPreviewBottom-139]
_ = x[actPreviewUp-140]
_ = x[actPreviewDown-141]
_ = x[actPreviewPageUp-142]
_ = x[actPreviewPageDown-143]
_ = x[actPreviewHalfPageUp-144]
_ = x[actPreviewHalfPageDown-145]
_ = x[actPrevHistory-146]
_ = x[actPrevSelected-147]
_ = x[actPrint-148]
_ = x[actPut-149]
_ = x[actNextHistory-150]
_ = x[actNextSelected-151]
_ = x[actExecute-152]
_ = x[actExecuteSilent-153]
_ = x[actExecuteMulti-154]
_ = x[actSigStop-155]
_ = x[actBest-156]
_ = x[actFirst-157]
_ = x[actLast-158]
_ = x[actReload-159]
_ = x[actReloadSync-160]
_ = x[actDisableSearch-161]
_ = x[actEnableSearch-162]
_ = x[actSelect-163]
_ = x[actDeselect-164]
_ = x[actUnbind-165]
_ = x[actRebind-166]
_ = x[actToggleBind-167]
_ = x[actBecome-168]
_ = x[actShowHeader-169]
_ = x[actHideHeader-170]
_ = x[actBell-171]
_ = x[actExclude-172]
_ = x[actExcludeMulti-173]
_ = x[actAsync-174]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
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, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600}
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 {
@@ -401,7 +401,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
if i == 0 {
fmt.Print(" ")
for j := int(f); j <= lastIdx; j++ {
fmt.Printf(" " + string(T[j]) + " ")
fmt.Print(" " + string(T[j]) + " ")
}
fmt.Println()
}
@@ -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
@@ -767,6 +769,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
char = unicode.To(unicode.LowerCase, char)
}
}
if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
@@ -824,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_)
@@ -846,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,
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
bytes[math.MaxUint16] = 'z'
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
}
func TestLongStringWithNormalize(t *testing.T) {
bytes := make([]byte, 30000)
for i := range bytes {
bytes[i] = 'x'
}
unicodeString := string(bytes) + " Minímal example"
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
}

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 {
@@ -44,7 +45,7 @@ func (s *ansiState) ToString() string {
}
ret := ""
if s.attr&tui.Bold > 0 {
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
ret += "1;"
}
if s.attr&tui.Dim > 0 {
@@ -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 := ""
@@ -98,11 +127,11 @@ func isPrint(c uint8) bool {
return '\x20' <= c && c <= '\x7e'
}
func matchOperatingSystemCommand(s string) int {
func matchOperatingSystemCommand(s string, start int) int {
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
// ^ match starting here
// ^ match starting here after the first printable character
//
i := 5 // prefix matched in nextAnsiEscapeSequence()
i := start // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s) && isPrint(s[i]); i++ {
}
if i < len(s) {
@@ -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' {
@@ -191,12 +223,20 @@ Loop:
}
}
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' {
j := 2
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
// ------
for ; i+j < len(s) && isNumeric(s[i+j]); j++ {
}
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
// ---------------
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && isPrint(s[i+j+1]) {
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
return i, i + k
}
}
}
@@ -257,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 {
@@ -310,20 +367,19 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
return trimmed, nil, state
}
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
func parseAnsiCode(s string) (int, byte, string) {
var remaining string
var i int
if delimiter == 0 {
// 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
}
} else {
i = strings.IndexByte(s, delimiter)
}
if i >= 0 {
delimiter = s[i]
sep = s[i]
remaining = s[i+1:]
s = s[:i]
}
@@ -335,42 +391,59 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
return -1, sep, remaining
}
code = code*10 + int(ch)
}
return code, delimiter, remaining
return code, sep, remaining
}
return -1, delimiter, 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 ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
state.url = nil
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
stLen := 2
if strings.HasSuffix(ansiCode, "\a") {
stLen = 1
}
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
state.url = nil
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
params := ansiCode[4 : 4+paramsEnd]
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
state.url = &url{uri: uri, params: params}
}
}
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]
@@ -378,11 +451,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0
ptr := &state.fg
var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
var sep byte
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
count++
switch state256 {
case 0:
@@ -393,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:
@@ -404,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:
@@ -418,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:
@@ -425,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 {
@@ -467,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))
}
}
@@ -335,6 +335,28 @@ func TestExtractColor(t *testing.T) {
assert((*offsets)[0], 0, 6, 2, -1, true)
assert((*offsets)[1], 6, 11, 200, 100, false)
})
state = nil
var color24 tui.Color = (1 << 24) + (180 << 16) + (190 << 8) + 254
src = "\x1b[1mhello \x1b[22;1;38:2:180:190:254mworld"
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 2 {
t.Fail()
}
if state.fg != color24 || state.attr != 1 {
t.Fail()
}
assert((*offsets)[0], 0, 6, -1, -1, true)
assert((*offsets)[1], 6, 11, color24, -1, true)
})
src = "\x1b]133;A\x1b\\hello \x1b]133;C\x1b\\world"
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert((*offsets)[0], 0, 11, color24, -1, true)
})
}
func TestAnsiCodeStringConversion(t *testing.T) {
@@ -347,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")
@@ -358,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, 0)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
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,10 +41,31 @@ 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]
}
// GetItems returns the first n items from the given chunks
func GetItems(chunks []*Chunk, n int) []Item {
items := make([]Item, 0, n)
for _, chunk := range chunks {
for i := 0; i < chunk.count && len(items) < n; i++ {
items = append(items, chunk.items[i])
}
if len(items) >= n {
break
}
}
return items
}
// CountItems returns the total number of Items
func CountItems(cs []*Chunk) int {
if len(cs) == 0 {

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

@@ -26,16 +26,20 @@ const (
previewCancelWait = 500 * time.Millisecond
previewChunkDelay = 100 * time.Millisecond
previewDelayed = 500 * time.Millisecond
maxPatternLength = 300
maxPatternLength = 1000
maxMulti = math.MaxInt32
// Background processes
maxBgProcesses = 30
maxBgProcessesPerAction = 3
// Matcher
numPartitionsMultiplier = 8
maxPartitions = 32
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk
chunkSize int = 100
chunkSize int = 1000
// Pre-allocated memory slices to minimize GC
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
@@ -61,7 +65,6 @@ const (
EvtSearchNew
EvtSearchProgress
EvtSearchFin
EvtHeader
EvtReady
EvtQuit
)

View File

@@ -2,10 +2,13 @@
package fzf
import (
"fmt"
"maps"
"os"
"sync"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -15,7 +18,6 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/
type revision struct {
@@ -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
}
}
@@ -95,23 +113,18 @@ func Run(opts *Options) (int, error) {
cache := NewChunkCache()
var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 {
if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(data)
item.text.Index = itemIndex
itemIndex++
return true
})
} else {
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
@@ -127,15 +140,19 @@ func Run(opts *Options) (int, error) {
}
}
}
trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines {
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
return false
}
transformed := nthTransformer(tokens, itemIndex)
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++
@@ -165,7 +182,7 @@ func Run(opts *Options) (int, error) {
}
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
@@ -188,17 +205,37 @@ func Run(opts *Options) (int, error) {
forward = false
case byBegin:
forward = true
case byPathname:
withPos = true
forward = false
}
}
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
nth := opts.Nth
inputRevision := revision{}
snapshotRevision := revision{}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
patternCache := make(map[string]*Pattern)
denyMutex := sync.Mutex{}
denylist := make(map[int32]struct{})
clearDenylist := func() {
denyMutex.Lock()
if len(denylist) > 0 {
patternCache = make(map[string]*Pattern)
}
denylist = make(map[int32]struct{})
denyMutex.Unlock()
}
headerLines := int32(opts.HeaderLines)
headerUpdated := false
patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock()
denylistCopy := maps.Clone(denylist)
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
// Filtering mode
if opts.Filter != nil {
@@ -209,6 +246,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)
@@ -217,9 +256,12 @@ func Run(opts *Options) (int, error) {
func(runes []byte) bool {
item := Item{}
if chunkList.trans(&item, runes) {
if item.Index() < headerLines {
return false
}
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
opts.Printer(transformer(&item))
found = true
}
mutex.Unlock()
@@ -233,11 +275,51 @@ func Run(opts *Options) (int, error) {
// NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{
if opts.Bench > 0 {
// Benchmark mode: repeat scan for the given duration
totalItems := CountItems(snapshot)
var matchCount int
var times []time.Duration
deadline := time.Now().Add(opts.Bench)
for time.Now().Before(deadline) {
cache.Clear()
start := time.Now()
result := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
times = append(times, time.Since(start))
matchCount = result.merger.Length()
}
// Print stats
var total time.Duration
minD, maxD := times[0], times[0]
for _, d := range times {
total += d
if d < minD {
minD = d
}
if d > maxD {
maxD = d
}
}
avg := total / time.Duration(len(times))
selectivity := float64(matchCount) / float64(totalItems) * 100
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%)\n",
len(times),
float64(avg.Microseconds())/1000,
float64(minD.Microseconds())/1000,
float64(maxD.Microseconds())/1000,
total.Seconds(),
totalItems, matchCount, selectivity)
return ExitOk, nil
}
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
}
}
@@ -274,6 +356,7 @@ func Run(opts *Options) (int, error) {
// Event coordination
reading := true
ticks := 0
startTick := 0
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
@@ -281,10 +364,11 @@ func Run(opts *Options) (int, error) {
query := []rune{}
determine := func(final bool) {
if heightUnknown {
if total >= maxFit || final {
items := max(0, total-int(headerLines))
if items >= maxFit || final {
deferred = false
heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
}
} else if deferred {
deferred = false
@@ -296,11 +380,15 @@ func Run(opts *Options) (int, error) {
var snapshot []*Chunk
var count int
restart := func(command commandSpec, environ []string) {
if !useSnapshot {
clearDenylist()
}
reading = true
headerUpdated = false
startTick = ticks
chunkList.Clear()
itemIndex = 0
inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
readyChan := make(chan bool)
go reader.restart(command, environ, readyChan)
<-readyChan
@@ -342,7 +430,8 @@ func Run(opts *Options) (int, error) {
} else {
reading = reading && evt == EvtReadNew
}
if useSnapshot && evt == EvtReadFin {
if useSnapshot && evt == EvtReadFin { // reload-sync
clearDenylist()
useSnapshot = false
}
if !useSnapshot {
@@ -357,7 +446,11 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
if headerLines > 0 && !headerUpdated {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
headerUpdated = int32(total) >= headerLines
}
if heightUnknown && !deferred {
determine(!reading)
}
@@ -367,12 +460,38 @@ func Run(opts *Options) (int, error) {
var command *commandSpec
var environ []string
var changed bool
headerLinesChanged := false
switch val := value.(type) {
case searchRequest:
sort = val.sort
command = val.command
environ = val.environ
changed = val.changed
bump := false
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
denyMutex.Lock()
for _, itemIndex := range val.denylist {
denylist[itemIndex] = struct{}{}
}
denyMutex.Unlock()
bump = true
}
if val.nth != nil {
// Change nth and clear caches
nth = *val.nth
bump = true
}
if val.headerLines != nil {
headerLines = int32(*val.headerLines)
headerUpdated = false
headerLinesChanged = true
bump = true
}
if bump {
patternCache = make(map[string]*Pattern)
cache.Clear()
inputRevision.bumpMinor()
}
if command != nil {
useSnapshot = val.sync
}
@@ -404,6 +523,14 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision
}
}
if headerLinesChanged {
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
if headerLines > 0 {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
} else {
terminal.UpdateHeader(nil)
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false
@@ -413,19 +540,15 @@ func Run(opts *Options) (int, error) {
terminal.UpdateProgress(val)
}
case EvtHeader:
headerPadded := make([]string, opts.HeaderLines)
copy(headerPadded, value.([]string))
terminal.UpdateHeader(headerPadded)
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)
@@ -433,8 +556,9 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 {
opts.Printer("")
}
for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
transformer := buildItemTransformer(opts)
for i := range count {
opts.Printer(transformer(merger.Get(i).item))
}
if count == 0 {
exitCode = ExitNoMatch
@@ -442,7 +566,7 @@ func Run(opts *Options) (int, error) {
stop = true
return
}
determine(val.final)
determine(merger.final)
}
}
terminal.UpdateList(val)
@@ -455,8 +579,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

@@ -6,10 +6,17 @@ import (
"github.com/junegunn/fzf/src/util"
)
type transformed struct {
// Because nth can be changed dynamically by change-nth action, we need to
// keep the revision number at the time of transformation.
revision revision
tokens []Token
}
// Item represents each input line. 56 bytes.
type Item struct {
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
transformed *[]Token // 8
transformed *transformed // 8
origText *[]byte // 8
colors *[]ansiOffset // 8
}
@@ -44,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
}
return item.text.ToString()
}
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
transformed := transformer(tokens, item.Index())
return StripLastDelimiter(transformed, delimiter)
}

View File

@@ -3,7 +3,6 @@ package fzf
import (
"fmt"
"runtime"
"sort"
"sync"
"time"
@@ -19,6 +18,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 +42,8 @@ type Matcher struct {
reqBox *util.EventBox
partitions int
slab []*util.Slab
mergerCache map[string]*Merger
sortBuf [][]Result
mergerCache map[string]MatchResult
revision revision
}
@@ -40,8 +54,11 @@ 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)
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
if threads > 0 {
partitions = threads
}
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
@@ -51,7 +68,8 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(),
partitions: partitions,
slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger),
sortBuf: make([][]Result, partitions),
mergerCache: make(map[string]MatchResult),
revision: revision}
}
@@ -85,43 +103,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 +169,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, pattern.startIndex)
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)
@@ -196,11 +216,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort && request.pattern.sortable {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches))
}
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
}
resultChan <- partialResult{idx, sliceMatches}
}(idx, m.slab[idx], 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,50 +4,57 @@ 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
// a single, globally-sorted list
type Merger struct {
pattern *Pattern
lists [][]Result
merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
tac bool
final bool
count int
pass bool
revision revision
minIndex int32
pattern *Pattern
lists [][]Result
merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
tac bool
final bool
count int
pass bool
startIndex int
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
// original order. startIndex items are skipped from the beginning.
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
var minIndex, maxIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
}
si := int(startIndex)
mg := Merger{
pattern: nil,
chunks: chunks,
tac: tac,
count: 0,
pass: true,
revision: revision,
minIndex: minIndex}
pattern: nil,
chunks: chunks,
tac: tac,
count: 0,
pass: true,
startIndex: si,
revision: revision,
minIndex: minIndex + startIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks {
mg.count += chunk.count
}
mg.count = max(0, mg.count-si)
return &mg
}
// 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 +66,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)
@@ -109,6 +117,7 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac {
idx = mg.count - idx - 1
}
idx += mg.startIndex
firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count
@@ -137,6 +146,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

@@ -9,9 +9,13 @@ import (
)
func TestDelimiterRegex(t *testing.T) {
// Valid regex
// Valid regex, but a single character -> string
delim := delimiterRegexp(".")
if delim.regex == nil || delim.str != nil {
if delim.regex != nil || *delim.str != "." {
t.Error(delim)
}
delim = delimiterRegexp("|")
if delim.regex != nil || *delim.str != "|" {
t.Error(delim)
}
// Broken regex -> string
@@ -138,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)
@@ -164,11 +168,11 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
check(tui.CtrlM, "Return")
check(tui.Enter, "Return")
checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab")
check(tui.ShiftTab, "btab")
@@ -178,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)
}
@@ -191,7 +195,7 @@ func TestParseKeys(t *testing.T) {
check(tui.ShiftLeft, "shift-left")
check(tui.ShiftRight, "shift-right")
check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter")
check(tui.Enter, "Enter")
check(tui.Backspace, "bspace")
}
@@ -207,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-,")
}
@@ -296,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")
}
@@ -305,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")
}
@@ -316,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")
}
@@ -329,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")
}
@@ -346,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)
@@ -437,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) {
@@ -458,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

@@ -60,8 +60,13 @@ type Pattern struct {
cacheKey string
delimiter Delimiter
nth []Range
revision revision
procFun map[termType]algo.Algo
cache *ChunkCache
denylist map[int32]struct{}
startIndex int32
directAlgo algo.Algo
directTerm *term
}
var _splitRegex *regexp.Regexp
@@ -72,7 +77,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
var asString string
if extended {
@@ -140,11 +145,15 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
sortable: sortable,
cacheable: cacheable,
nth: nth,
revision: revision,
delimiter: delimiter,
cache: cache,
denylist: denylist,
startIndex: startIndex,
procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey()
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -241,6 +250,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool {
if len(p.denylist) > 0 {
return false
}
if !p.extended {
return len(p.text) == 0
}
@@ -265,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
return strings.Join(cacheableTerms, "\t")
}
// buildDirectAlgo returns the algo function and term for the direct fast path
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
if !p.extended || len(p.nth) > 0 {
return nil, nil
}
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
t := &p.termSets[0][0]
if !t.inv && t.typ == termFuzzy {
return fuzzyAlgo, t
}
}
return nil, nil
}
// CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string {
return p.cacheKey
@@ -294,38 +322,99 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{}
// Skip header items in chunks that contain them
startIdx := 0
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
startIdx = int(p.startIndex - chunk.items[0].Index())
if startIdx >= chunk.count {
return matches
}
}
// Fast path: single fuzzy term, no nth, no denylist.
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
// and avoiding per-match []Offset heap allocation.
if p.directAlgo != nil && len(p.denylist) == 0 {
t := p.directTerm
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&chunk.items[idx].text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
&chunk.items[idx], res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
} else {
for _, result := range space {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&result.item.text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
result.item, res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
}
return matches
}
if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
matches = append(matches, match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
matches = append(matches, match)
}
}
}
return matches
}
if space == nil {
for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
for idx := startIdx; idx < chunk.count; idx++ {
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
matches = append(matches, match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
if _, prs := p.denylist[result.item.Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
matches = append(matches, match)
}
}
}
return matches
}
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
// MatchItem returns the match result if the Item is a match.
// A zero-value Result (with item == nil) indicates no match.
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
if p.extended {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
return buildResult(item, offsets, bonus), offsets, pos
}
return nil, nil, nil
return Result{}, nil, nil
}
offset, bonus, pos := p.basicMatch(item, withPos, slab)
if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
return buildResult(item, offsets, bonus), offsets, pos
}
return nil, nil, nil
return Result{}, nil, nil
}
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
@@ -393,12 +482,22 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
func (p *Pattern) transformInput(item *Item) []Token {
if item.transformed != nil {
return *item.transformed
transformed := *item.transformed
if transformed.revision == p.revision {
return transformed.tokens
}
}
tokens := Tokenize(item.text.ToString(), p.delimiter)
ret := Transform(tokens, p.nth)
item.transformed = &ret
// Strip the last delimiter to allow suffix match
if len(ret) > 0 && !p.delimiter.IsAwk() {
chars := ret[len(ret)-1].text
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
newChars := util.ToChars(stringBytes(stripped))
ret[len(ret)-1].text = &newChars
}
item.transformed = &transformed{p.revision, ret}
return ret
}

View File

@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
}
func TestExact(t *testing.T) {
@@ -135,12 +135,12 @@ func TestOrigTextAndTransformed(t *testing.T) {
chunk.items[0] = Item{
text: util.ToChars([]byte("junegunn")),
origText: &origBytes,
transformed: &trans}
transformed: &transformed{pattern.revision, trans}}
pattern.extended = extended
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
if !(matches[0].item.text.ToString() == "junegunn" &&
string(*matches[0].item.origText) == "junegunn.choi" &&
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
reflect.DeepEqual((*matches[0].item.transformed).tokens, trans)) {
t.Error("Invalid match result", matches)
}
@@ -148,7 +148,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
if !(match.item.text.ToString() == "junegunn" &&
string(*match.item.origText) == "junegunn.choi" &&
offsets[0][0] == 0 && offsets[0][1] == 5 &&
reflect.DeepEqual(*match.item.transformed, trans)) {
reflect.DeepEqual((*match.item.transformed).tokens, trans)) {
t.Error("Invalid match result", match, offsets, extended)
}
if !((*pos)[0] == 4 && (*pos)[1] == 0) {

View File

@@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
})
}()
var command string
var command, input string
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else {
input, err := fifo("proxy-input")
input, err = fifo("proxy-input")
if err != nil {
return ExitError, err
}
@@ -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,10 +145,13 @@ 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
}
os.Remove(temp)
os.Remove(input)
os.Remove(output)
executor.Become(ttyin, env, command)
}
return code, err

View File

@@ -7,6 +7,8 @@ import (
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@@ -120,7 +122,7 @@ func (r *Reader) readChannel(inputChan chan string) bool {
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
r.startEventPoller()
var success bool
signalReady := func() {
@@ -137,7 +139,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
signalReady()
success = r.readFiles(root, opts, ignores)
success = r.readFiles(roots, opts, ignores)
} else {
success = r.readFromCommand(cmd, initEnv, signalReady)
}
@@ -176,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
@@ -265,30 +267,66 @@ func trimPath(path string) string {
return byteString(bytes)
}
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
conf := fastwalk.Config{
Follow: opts.follow,
// Use forward slashes when running a Windows binary under WSL or MSYS
ToSlash: fastwalk.DefaultToSlash(),
Sort: fastwalk.SortFilesFirst,
}
ignoresBase := []string{}
ignoresFull := []string{}
ignoresSuffix := []string{}
sep := string(os.PathSeparator)
if _, ok := os.LookupEnv("MSYSTEM"); ok {
sep = "/"
}
for _, ignore := range ignores {
if strings.ContainsRune(ignore, os.PathSeparator) {
if strings.HasPrefix(ignore, sep) {
ignoresSuffix = append(ignoresSuffix, ignore)
} else {
// 'foo/bar' should match
// * 'foo/bar'
// * 'baz/foo/bar'
// * but NOT 'bazfoo/bar'
ignoresFull = append(ignoresFull, ignore)
ignoresSuffix = append(ignoresSuffix, sep+ignore)
}
} else {
ignoresBase = append(ignoresBase, ignore)
}
}
fn := func(path string, de os.DirEntry, err error) error {
if err != nil {
return nil
}
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 ignores {
if ignore == base {
if slices.Contains(ignoresBase, base) {
return filepath.SkipDir
}
if slices.Contains(ignoresFull, path) {
return filepath.SkipDir
}
for _, ignore := range ignoresSuffix {
if strings.HasSuffix(path, ignore) {
return filepath.SkipDir
}
}
if path != sep {
path += sep
}
}
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
@@ -301,7 +339,11 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
}
return nil
}
return fastwalk.Walk(&conf, root, fn) == nil
noerr := true
for _, root := range roots {
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
}
return noerr
}
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {

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
@@ -29,8 +33,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item}
numChars := item.text.Length()
minBegin := math.MaxUint16
minEnd := math.MaxUint16
maxEnd := 0
@@ -38,13 +40,21 @@ 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
}
}
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
}
// buildResultFromBounds builds a Result from pre-computed offset bounds.
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
result := Result{item: item}
numChars := item.text.Length()
for idx, criterion := range sortCriteria {
val := uint16(math.MaxUint16)
switch criterion {
@@ -69,10 +79,24 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
}
case byLength:
val = item.TrimLength()
case byPathname:
if validOffsetFound {
lastDelim := -1
s := item.text.ToString()
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' || s[i] == '\\' {
lastDelim = i
break
}
}
if lastDelim <= minBegin {
val = util.AsUint16(minBegin - lastDelim)
}
}
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) {
@@ -104,21 +128,21 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, 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 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
if len(itemColors) == 0 && len(nthOffsets) == 0 {
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
}
// Find max column
var maxCol int32
for _, off := range matchOffsets {
for _, off := range append(matchOffsets, nthOffsets...) {
if off[1] > maxCol {
maxCol = off[1]
}
@@ -129,20 +153,37 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
}
}
cols := make([]int, maxCol)
type cellInfo struct {
index int
color bool
match bool
nth bool
fbg tui.Color
}
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] = colorIndex + 1 // 1-based index of itemColors
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}
}
}
}
for _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ {
// Negative of 1-based index of itemColors
// - The extra -1 means highlighted
if cols[i] >= 0 {
cols[i] = cols[i]*-1 - 1
}
cols[i].match = true
}
}
for _, off := range nthOffsets {
for i := off[0]; i < off[1]; i++ {
cols[i].nth = true
}
}
@@ -152,35 +193,46 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
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 != 0 && idx > start {
if curr < 0 {
color := colMatch
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
if curr.nth {
color = colBase.WithAttr(attrNth).Merge(colMatch)
} else {
color = colBase.Merge(colMatch)
}
var url *url
if curr < -1 && theme.Colored {
ansi := itemColors[-curr-2]
if curr.color {
ansi := itemColors[curr.index]
url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch)
// hl or hl+ only sets the foreground color, so colMatch is the
@@ -193,19 +245,40 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
if color.Fg().IsDefault() && origColor.HasBg() {
color = origColor
if curr.nth {
color = color.WithAttr(attrNth &^ tui.AttrRegular)
}
} else {
color = origColor.MergeNonDefault(color)
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else {
ansi := itemColors[curr-1]
} else if curr.color {
ansi := itemColors[curr.index]
base := colBase
if curr.nth {
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: ansiToColorPair(ansi, colBase),
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: color,
match: false,
url: nil})
}
}
}
@@ -266,3 +339,79 @@ func (a ByRelevanceTac) Swap(i, j int) {
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks(a[i], a[j], true)
}
// radixSortResults sorts Results by their points key using LSD radix sort.
// O(n) time complexity vs O(n log n) for comparison sort.
// The sort is stable, so equal-key items maintain original (item-index) order.
// For tac mode, runs of equal keys are reversed after sorting.
func radixSortResults(a []Result, tac bool, scratch []Result) []Result {
n := len(a)
if n < 128 {
if tac {
sort.Sort(ByRelevanceTac(a))
} else {
sort.Sort(ByRelevance(a))
}
return scratch[:0]
}
if cap(scratch) < n {
scratch = make([]Result, n)
}
buf := scratch[:n]
src, dst := a, buf
scattered := 0
for pass := range 8 {
shift := uint(pass) * 8
var count [256]int
for i := range src {
count[byte(sortKey(&src[i])>>shift)]++
}
// Skip if all items have the same byte value at this position
if count[byte(sortKey(&src[0])>>shift)] == n {
continue
}
var offset [256]int
for i := 1; i < 256; i++ {
offset[i] = offset[i-1] + count[i-1]
}
for i := range src {
b := byte(sortKey(&src[i]) >> shift)
dst[offset[b]] = src[i]
offset[b]++
}
src, dst = dst, src
scattered++
}
// If odd number of scatters, data is in buf, copy back to a
if scattered%2 == 1 {
copy(a, src)
}
// Handle tac: reverse runs of equal keys so equal-key items
// are in reverse item-index order
if tac {
i := 0
for i < n {
ki := sortKey(&a[i])
j := i + 1
for j < n && sortKey(&a[j]) == ki {
j++
}
if j-i > 1 {
for l, r := i, j-1; l < r; l, r = l+1, r-1 {
a[l], a[r] = a[r], a[l]
}
}
i = j
}
}
return scratch
}

View File

@@ -1,4 +1,4 @@
//go:build !386 && !amd64
//go:build !386 && !amd64 && !arm64
package fzf
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}
func sortKey(r *Result) uint64 {
return uint64(r.points[0]) | uint64(r.points[1])<<16 | uint64(r.points[2])<<32 | uint64(r.points[3])<<48
}

View File

@@ -2,6 +2,7 @@ package fzf
import (
"math"
"math/rand"
"sort"
"testing"
@@ -124,14 +125,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, tui.Dark256, colBase, colMatch, 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 {
@@ -155,20 +156,87 @@ func TestColorOffset(t *testing.T) {
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
// [{[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}}
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
// {[35 40] {4 8 1}}]
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
assert(5, 27, 30, colUnderline)
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
assert(7, 32, 33, colUnderline)
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
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, 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}}
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
// {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
assert(5, 27, 30, colUnderline)
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
assert(7, 32, 33, colUnderline)
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
expected := tui.Bold | attr
if attr == tui.AttrRegular {
expected = tui.Bold
}
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
}
}
func TestRadixSortResults(t *testing.T) {
sortCriteria = []criterion{byScore, byLength}
rng := rand.New(rand.NewSource(42))
for _, n := range []int{128, 256, 500, 1000} {
for _, tac := range []bool{false, true} {
// Build items with random points and indices
items := make([]*Item, n)
for i := range items {
items[i] = &Item{text: util.Chars{Index: int32(i)}}
}
results := make([]Result, n)
for i := range results {
results[i] = Result{
item: items[i],
points: [4]uint16{
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
},
}
}
// Make some duplicates to test stability
for i := 0; i < n/4; i++ {
j := rng.Intn(n)
k := rng.Intn(n)
results[j].points = results[k].points
}
// Copy for reference sort
expected := make([]Result, n)
copy(expected, results)
if tac {
sort.Sort(ByRelevanceTac(expected))
} else {
sort.Sort(ByRelevance(expected))
}
// Radix sort
var scratch []Result
scratch = radixSortResults(results, tac, scratch)
for i := range results {
if results[i] != expected[i] {
t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
n, tac, i, results[i].item.Index(), expected[i].item.Index())
break
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
//go:build 386 || amd64
//go:build 386 || amd64 || arm64
package fzf
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}
func sortKey(r *Result) uint64 {
return *(*uint64)(unsafe.Pointer(&r.points[0]))
}

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
@@ -484,7 +493,12 @@ func TestParsePlaceholder(t *testing.T) {
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
`{q}`: `{qq}`,
`{q}`: `{qq}`,
`{q:1}`: `{qq:1}`,
`{q:2..}`: `{qq:2..}`,
`{q:..}`: `{qq:..}`,
`{q:2..-1}`: `{qq:2..-1}`,
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
// IV. escaping placeholder
`\{}`: `{}`,
@@ -507,6 +521,34 @@ func TestParsePlaceholder(t *testing.T) {
}
}
func TestExtractPassthroughs(t *testing.T) {
for _, middle := range []string{
"\x1bPtmux;\x1b\x1bbar\x1b\\",
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
"\x1b]1337;bar\x1b\\",
"\x1b]1337;bar\x1bbar\x1b\\",
"\x1b]1337;bar\a",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
} {
line := "foo" + middle + "baz"
loc := findPassThrough(line)
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
t.Error("failed to find passthrough")
}
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
passthroughs, result := extractPassThroughs(line)
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
t.Error("failed to extract passthroughs")
}
}
}
/* utilities section */
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
@@ -516,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)
@@ -532,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)
@@ -547,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 {
/*
@@ -585,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)
@@ -657,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

@@ -9,13 +9,22 @@ import (
func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
fzf := args[0]
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
if opts.BorderShape == tui.BorderUndefined {
args = append(args, "--border")
fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"}
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.
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")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
for _, arg := range append(args, rest...) {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
@@ -33,7 +42,10 @@ func runTmux(args []string, opts *Options) (int, error) {
// M Both The mouse position
// W Both The window position on the status line
// S -y The line above or below the status line
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
tmuxArgs := []string{"display-popup", "-E", "-d", dir}
if !opts.Tmux.border {
tmuxArgs = append(tmuxArgs, "-B")
}
switch opts.Tmux.position {
case posUp:
tmuxArgs = append(tmuxArgs, "-xC", "-y0")

View File

@@ -6,6 +6,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
"github.com/junegunn/fzf/src/util"
)
@@ -18,6 +19,48 @@ type Range struct {
end int
}
func (r Range) IsFull() bool {
return r.begin == rangeEllipsis && r.end == rangeEllipsis
}
func compareRanges(r1 []Range, r2 []Range) bool {
if len(r1) != len(r2) {
return false
}
for idx := range r1 {
if r1[idx] != r2[idx] {
return false
}
}
return true
}
func RangesToString(ranges []Range) string {
strs := []string{}
for _, r := range ranges {
s := ""
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
s = ".."
} else if r.begin == r.end {
s = strconv.Itoa(r.begin)
} else {
if r.begin != rangeEllipsis {
s += strconv.Itoa(r.begin)
}
if r.begin != -1 {
s += ".."
if r.end != rangeEllipsis {
s += strconv.Itoa(r.end)
}
}
}
strs = append(strs, s)
}
return strings.Join(strs, ",")
}
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text *util.Chars
@@ -35,13 +78,18 @@ type Delimiter struct {
str *string
}
// IsAwk returns true if the delimiter is an AWK-style delimiter
func (d Delimiter) IsAwk() bool {
return d.regex == nil && d.str == nil
}
// String returns the string representation of a Delimiter.
func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
}
func newRange(begin int, end int) Range {
if begin == 1 {
if begin == 1 && end != 1 {
begin = rangeEllipsis
}
if end == -1 {
@@ -73,7 +121,7 @@ func ParseRange(str *string) (Range, bool) {
}
begin, err1 := strconv.Atoi(ns[0])
end, err2 := strconv.Atoi(ns[1])
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
if err1 != nil || err2 != nil || begin == 0 || end == 0 || begin < 0 && end > 0 {
return Range{}, false
}
return newRange(begin, end), true
@@ -158,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) {
@@ -169,7 +218,41 @@ func Tokenize(text string, delimiter Delimiter) []Token {
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens []Token) string {
// StripLastDelimiter removes the trailing delimiter and whitespaces
func StripLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil {
str = strings.TrimSuffix(str, *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) {
str = str[:lastLoc[0]]
}
}
}
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
for _, token := range tokens {
output.WriteString(token.text.ToString())
@@ -187,7 +270,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
chars := util.ToChars(stringBytes(joinTokens(tokens)))
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
parts = append(parts, &chars)
} else {
if idx < 0 {
@@ -219,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

@@ -40,6 +40,18 @@ func TestParseRange(t *testing.T) {
t.Errorf("%v", r)
}
}
{
i := "1..3..5"
if r, ok := ParseRange(&i); ok {
t.Errorf("%v", r)
}
}
{
i := "-3..3"
if r, ok := ParseRange(&i); ok {
t.Errorf("%v", r)
}
}
}
func TestTokenize(t *testing.T) {
@@ -73,14 +85,14 @@ func TestTransform(t *testing.T) {
{
ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " {
if JoinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
{
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
@@ -95,7 +107,7 @@ func TestTransform(t *testing.T) {
{
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||

View File

@@ -2,23 +2,7 @@
package tui
type Attr int32
func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
}
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
Bold = Attr(1)
Dim = Attr(1 << 1)
Italic = Attr(1 << 2)
@@ -29,7 +13,14 @@ 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) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}
@@ -37,17 +28,20 @@ func (r *FullscreenRenderer) PassThrough(string) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Bell() {}
func (r *FullscreenRenderer) HideCursor() {}
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) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
return nil
}

View File

@@ -21,7 +21,7 @@ func _() {
_ = x[CtrlJ-10]
_ = x[CtrlK-11]
_ = x[CtrlL-12]
_ = x[CtrlM-13]
_ = x[Enter-13]
_ = x[CtrlN-14]
_ = x[CtrlO-15]
_ = x[CtrlP-16]
@@ -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 = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
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) {

File diff suppressed because it is too large Load Diff

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

@@ -18,7 +18,7 @@ func IsLightRendererSupported() bool {
return true
}
func (r *LightRenderer) defaultTheme() *ColorTheme {
func (r *LightRenderer) DefaultTheme() *ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return Dark256
}
@@ -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

@@ -18,6 +18,7 @@ const (
var (
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
counter = uint64(0)
)
// IsLightRendererSupported checks to see if the Light renderer is supported
@@ -39,7 +40,7 @@ func IsLightRendererSupported() bool {
return canSetVt100
}
func (r *LightRenderer) defaultTheme() *ColorTheme {
func (r *LightRenderer) DefaultTheme() *ColorTheme {
// the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178:
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
return Default16
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
}
r.inHandle = uintptr(inHandle)
r.setupTerminal()
// channel for non-blocking reads. Buffer to make sure
// we get the ESC sets:
r.ttyinChannel = make(chan byte, 1024)
// the following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows.
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
_, err := util.Read(fd, b)
if err == nil {
r.ttyinChannel <- b[0]
}
}
}()
r.setupTerminal()
return nil
}
@@ -91,27 +76,51 @@ 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
}
func (r *LightRenderer) setupTerminal() error {
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
return err
}
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
func (r *LightRenderer) setupTerminal() {
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
// The following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows.
current := counter
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
if _, err := util.Read(fd, b); err == nil {
r.mutex.Lock()
// This condition prevents the goroutine from running after the renderer
// has been closed or paused.
if current != counter {
r.mutex.Unlock()
break
}
r.ttyinChannel <- b[0]
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
r.mutex.Unlock()
}
}
}()
}
func (r *LightRenderer) restoreTerminal() error {
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
return err
}
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
func (r *LightRenderer) restoreTerminal() {
r.mutex.Lock()
counter++
// We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
// e.g. fzf --bind 'enter:execute:less {}'
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
r.mutex.Unlock()
}
func (r *LightRenderer) Size() TermSize {
@@ -142,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,22 +37,24 @@ func (p ColorPair) style() tcell.Style {
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
}
type Attr int32
type TcellWindow struct {
color bool
preview bool
top int
left int
width int
height int
normal ColorPair
lastX int
lastY int
moveCursor bool
borderStyle BorderStyle
uri *string
params *string
color bool
windowType WindowType
top int
left int
width int
height int
normal ColorPair
lastX int
lastY int
moveCursor bool
borderStyle BorderStyle
uri *string
params *string
showCursor bool
wrapSign string
wrapSignWidth int
tabstop int
}
func (w *TcellWindow) Top() int {
@@ -72,7 +75,9 @@ func (w *TcellWindow) Height() int {
func (w *TcellWindow) Refresh() {
if w.moveCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
if w.showCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
}
w.moveCursor = false
}
w.lastX = 0
@@ -93,11 +98,17 @@ const (
Italic = Attr(tcell.AttrItalic)
)
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 7)
AttrClear = Attr(1 << 8)
)
func (r *FullscreenRenderer) Bell() {
_screen.Beep()
}
func (r *FullscreenRenderer) HideCursor() {
r.showCursor = false
}
func (r *FullscreenRenderer) ShowCursor() {
r.showCursor = true
}
func (r *FullscreenRenderer) PassThrough(str string) {
// No-op
@@ -106,8 +117,12 @@ func (r *FullscreenRenderer) PassThrough(str string) {
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme {
s, e := r.getScreen()
if e != nil {
return Default16
}
if s.Colors() >= 256 {
return Dark256
}
return Default16
@@ -136,10 +151,6 @@ func (c Color) Style() tcell.Color {
}
}
func (a Attr) Merge(b Attr) Attr {
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 (
@@ -148,20 +159,34 @@ var (
_initialResize bool = true
)
func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
if _screen == nil {
s, e := tcell.NewScreen()
if e != nil {
return nil, e
}
if !r.showCursor {
s.HideCursor()
}
_screen = s
}
return _screen, nil
}
func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
s, e := r.getScreen()
if e != nil {
return e
}
if e = s.Init(); e != nil {
return e
}
s.EnablePaste()
if r.mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
return nil
}
@@ -174,7 +199,6 @@ func (r *FullscreenRenderer) Init() error {
if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
@@ -224,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
@@ -243,7 +272,11 @@ func (r *FullscreenRenderer) GetChar() Event {
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
// dragging has same structure, it only repeats the middle (main) event appropriately
x, y := ev.Position()
mod := ev.Modifiers() != 0
mod := ev.Modifiers()
ctrl := (mod & tcell.ModCtrl) > 0
alt := (mod & tcell.ModAlt) > 0
shift := (mod & tcell.ModShift) > 0
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
prevButton, button := _prevMouseButton, ev.Buttons()
@@ -252,9 +285,9 @@ func (r *FullscreenRenderer) GetChar() Event {
switch {
case button&tcell.WheelDown != 0:
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
case button&tcell.Button1 != 0:
double := false
if !drag {
@@ -277,9 +310,9 @@ func (r *FullscreenRenderer) GetChar() Event {
}
}
// fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
case button&tcell.Button2 != 0:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
default:
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
@@ -288,7 +321,11 @@ func (r *FullscreenRenderer) GetChar() Event {
down := left || button&tcell.Button3 != 0
double := false
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
// No need to report mouse movement events when no button is pressed
if drag {
return Event{Invalid, 0, nil}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
}
// process keyboard:
@@ -300,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 {
@@ -326,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 {
@@ -388,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}
}
@@ -395,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}
}
@@ -406,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}
}
@@ -417,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}
}
@@ -428,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}
}
@@ -443,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}
@@ -513,20 +705,25 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) CancelGetChar() {
// TODO
}
func (r *FullscreenRenderer) Pause(clear bool) {
if clear {
_screen.Fini()
_screen.Suspend()
}
}
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
if clear {
r.initScreen()
_screen.Resume()
}
}
func (r *FullscreenRenderer) Close() {
_screen.Fini()
_screen = nil
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
@@ -537,28 +734,37 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
_screen.Show()
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
normal := ColNormal
if preview {
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
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:
normal = ColPreview
}
w := &TcellWindow{
color: r.theme.Colored,
preview: preview,
windowType: windowType,
top: top,
left: left,
width: width,
height: height,
normal: normal,
borderStyle: borderStyle}
borderStyle: borderStyle,
showCursor: r.showCursor,
tabstop: r.tabstop}
w.Erase()
return w
}
func (w *TcellWindow) Close() {
// TODO
}
func fill(x, y, w, h int, n ColorPair, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
@@ -568,11 +774,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
w.drawBorder(false)
}
@@ -581,9 +783,21 @@ func (w *TcellWindow) EraseMaybe() bool {
return true
}
func (w *TcellWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (w *TcellWindow) EncloseX(x int) bool {
return x >= w.left && x < (w.left+w.width)
}
func (w *TcellWindow) EncloseY(y int) bool {
return y >= w.top && y < (w.top+w.height)
}
func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
return w.EncloseX(x) && w.EncloseY(y)
}
func (w *TcellWindow) Move(y int, x int) {
@@ -614,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()
@@ -622,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)
@@ -661,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()
@@ -673,52 +907,76 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
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':
if len(rs) == 1 && rs[0] == '\r' {
st = style.Dim(true)
rs[0] = '␍'
}
xPos := w.left + w.lastX
yPos := w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
}
w.lastX += util.StringWidth(string(rs))
}
}
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
lx = 0
continue Loop
if j > 0 {
w.renderWrapSign(style)
}
}
if w.lastX < w.width {
w.renderGraphemes(wl.Text, style)
}
}
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
yPos := w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
lx += util.StringWidth(string(rs))
}
w.lastX += lx
if w.lastX == w.width {
if w.lastX >= w.width {
w.lastY++
w.lastX = 0
return FillNextLine
@@ -741,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() {
@@ -760,6 +1018,9 @@ func (w *TcellWindow) DrawHBorder() {
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 {
return
}
shape := w.borderStyle.shape
if shape == BorderNone {
return
@@ -772,10 +1033,19 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
var style tcell.Style
if w.color {
if w.preview {
style = ColPreviewBorder.style()
} else {
switch w.windowType {
case WindowBase:
style = ColBorder.style()
case WindowList:
style = ColListBorder.style()
case WindowHeader:
style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput:
style = ColInputBorder.style()
case WindowPreview:
style = ColPreviewBorder.style()
}
} else {
style = w.normal.style()

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 {
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
// KeyEnter is alias for KeyCR
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
// Ctrl+Alt keys
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
@@ -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()
@@ -233,7 +311,7 @@ Quick reference
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
11 1 KeyCtrlK KeyVT = ^K CtrlK
12 1 KeyCtrlL KeyFF = ^L CtrlL
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
14 1 KeyCtrlN KeySO = ^N CtrlN
15 1 KeyCtrlO KeySI = ^O CtrlO
16 1 KeyCtrlP KeyDLE = ^P CtrlP

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
}
// TtyIn on Windows returns nil
func TtyOut() (*os.File, error) {
// TtyOut on Windows returns nil
func TtyOut(ttyDefault string) (*os.File, error) {
return nil, nil
}

File diff suppressed because it is too large Load Diff

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,31 @@ 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) {
lastIdx := len(chars.slice)
firstIdx := lastIdx - len(runes)
if firstIdx < 0 {
return
}
for i := firstIdx; i < lastIdx; i++ {
char := chars.Get(i)
if char != runes[i-firstIdx] {
return
}
}
chars.slice = chars.slice[0:firstIdx]
}
func (chars *Chars) SliceRight(last int) {
chars.slice = chars.slice[:last]
}
func (chars *Chars) ToString() string {
@@ -227,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())
@@ -237,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
@@ -273,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)
@@ -284,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"

251
test/lib/common.rb Normal file
View File

@@ -0,0 +1,251 @@
# frozen_string_literal: true
require 'bundler/setup'
require 'minitest/autorun'
require 'fileutils'
require 'English'
require 'shellwords'
require 'erb'
require 'tempfile'
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
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
FZF_ALT_C_COMMAND
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
FZF_API_KEY
].freeze
DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__)
BASE = File.expand_path('../..', __dir__)
Dir.chdir(BASE)
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
def wait(timeout = DEFAULT_TIMEOUT)
since = Time.now
begin
yield or raise Minitest::Assertion, 'Assertion failure'
rescue Minitest::Assertion
raise if Time.now - since > timeout
sleep(0.05)
retry
end
end
class Shell
class << self
def bash
@bash ||=
begin
bashrc = '/tmp/fzf.bash'
File.open(bashrc, 'w') do |f|
f.puts ERB.new(TEMPLATE).result(binding)
end
"bash --rcfile #{bashrc}"
end
end
def zsh
@zsh ||=
begin
zdotdir = '/tmp/fzf-zsh'
FileUtils.rm_rf(zdotdir)
FileUtils.mkdir_p(zdotdir)
File.open("#{zdotdir}/.zshrc", 'w') do |f|
f.puts ERB.new(TEMPLATE).result(binding)
end
"ZDOTDIR=#{zdotdir} zsh"
end
end
def 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
class Tmux
attr_reader :win
def initialize(shell = :bash)
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
go(%W[set-window-option -t #{@win} pane-base-index 0])
return unless shell == :fish
send_keys 'function fish_prompt; end; clear', :Enter
self.until(&:empty?)
end
def kill
go(%W[kill-window -t #{win}])
end
def focus
go(%W[select-window -t #{win}])
end
def send_keys(*args)
go(%W[send-keys -t #{win}] + args.map(&:to_s))
end
def paste(str)
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
end
def capture
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
end
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
lines = nil
begin
wait(timeout) do
lines = capture
class << lines
def counts
lazy
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
.reject(&:empty?)
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
end
def match_count
counts[0]
end
def item_count
counts[1]
end
def select_count
counts[2]
end
def any_include?(val)
method = val.is_a?(Regexp) ? :match : :include?
find { |line| line.send(method, val) }
end
end
yield(lines).tap do |ok|
send_keys 'C-l' if refresh && !ok
end
end
rescue Minitest::Assertion
puts $ERROR_INFO.backtrace
puts '>' * 80
puts lines
puts '<' * 80
raise
end
lines
end
def prepare
tries = 0
begin
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
(tries += 1) < 5 ? retry : raise
end
send_keys 'C-u', 'C-l'
end
private
def go(args)
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
end
end
class TestBase < Minitest::Test
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
def writelines(lines)
File.write(TEMPNAME, lines.join("\n"))
end
def tempname
TEMPNAME
end
def fzf_output
@thread.join.value.chomp.tap { @thread = nil }
end
def fzf_output_lines
fzf_output.lines(chomp: true)
end
def setup
File.mkfifo(FIFONAME)
end
def teardown
FileUtils.rm_f([TEMPNAME, FIFONAME])
end
alias assert_equal_org assert_equal
def assert_equal(expected, actual)
# Ignore info separator
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
assert_equal_org(expected, actual)
end
# Run fzf with its output piped to a fifo
def fzf(*opts)
raise 'fzf_output not taken' if @thread
@thread = Thread.new { File.read(FIFONAME) }
fzf!(*opts) + " > #{FIFONAME.shellescape}"
end
def fzf!(*opts)
opts = opts.filter_map do |o|
case o
when Symbol
o = o.to_s
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
when String, Numeric
o.to_s
end
end
"#{FZF} #{opts.join(' ')}"
end
end
class TestInteractive < TestBase
attr_reader :tmux
def setup
super
@tmux = Tmux.new
end
def teardown
super
@tmux.kill
end
end

59
test/lib/common.sh Normal file
View File

@@ -0,0 +1,59 @@
set -u
PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
unset <%= UNSETS.join(' ') %>
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
# Setup fzf
# ---------
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
fi
# Auto-completion
# ---------------
[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
# Key bindings
# ------------
source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
# Old API
_fzf_complete_f() {
_fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <(
echo foo
echo bar
)
}
# New API
_fzf_complete_g() {
_fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <(
echo foo
echo bar
)
}
_fzf_complete_f_post() {
awk '{print "f" $0 $0}'
}
_fzf_complete_g_post() {
awk '{print "g" $0 $0}'
}
[ -n "${BASH-}" ] && complete -F _fzf_complete_f -o default -o bashdefault f
[ -n "${BASH-}" ] && complete -F _fzf_complete_g -o default -o bashdefault g
_comprun() {
local command=$1
shift
case "$command" in
f) fzf "$@" --preview 'echo preview-f-{}' ;;
g) fzf "$@" --preview 'echo preview-g-{}' ;;
*) fzf "$@" ;;
esac
}

5
test/runner.rb Normal file
View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
Dir[File.join(__dir__, 'test_*.rb')].each { require it }
require 'minitest/autorun'

2261
test/test_core.rb Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More