Compare commits

...

281 Commits

Author SHA1 Message Date
Junegunn Choi
38040d43e4 Leverage existing bash completion
This is a PoC implementation for leveraging existing bash completion

git **<tab>
kubectl **<tab>
2024-09-30 19:08:28 +09:00
Junegunn Choi
4161403a1d --tmux: Export bash functions
Fix #4001
2024-09-29 19:33:42 +09:00
junegunn
53bcdc4294 Deploying to master from @ junegunn/fzf@30a8ef28cd 🚀 2024-09-29 00:02:07 +00:00
Koichi Murase
30a8ef28cd [bash] Fix infinite retry loop for completion setting without "-F func" (#4004) 2024-09-23 19:16:59 +09:00
junegunn
855f90727a Deploying to master from @ junegunn/fzf@2191a44e36 🚀 2024-09-15 00:02:03 +00:00
Junegunn Choi
2191a44e36 Redraw/hide scroll offset when 'info' property is changed 2024-09-12 22:04:19 +09:00
Junegunn Choi
952276dc2d Add 'noinfo' option to hide scroll offset information in preview window
fzf --preview 'seq 1000' --preview-window noinfo

Close #2525
2024-09-12 18:31:14 +09:00
dependabot[bot]
2286edb329 Bump golang.org/x/term from 0.23.0 to 0.24.0 (#3991)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/term/compare/v0.23.0...v0.24.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-09-10 21:25:57 +09:00
junegunn
a0f28583e7 Deploying to master from @ junegunn/fzf@8af0af3400 🚀 2024-09-08 00:02:05 +00:00
Junegunn Choi
8af0af3400 Highlights
Close #3942
2024-09-01 16:20:25 +09:00
junegunn
769e5cbb2d Deploying to master from @ junegunn/fzf@fc69308057 🚀 2024-09-01 00:02:21 +00:00
Junegunn Choi
fc69308057 0.55.0 2024-08-29 17:10:58 +09:00
Junegunn Choi
c6d620c99e Add to CHANGELOG 2024-08-29 17:08:23 +09:00
Junegunn Choi
f510a4def6 Test cases for non-default --scheme options 2024-08-29 17:08:23 +09:00
Junegunn Choi
4ae3069c6f Underscore boundaries should be ranked lower 2024-08-29 17:08:23 +09:00
Junegunn Choi
c0f27751d3 Add exact-boundary-match to man page 2024-08-29 17:08:23 +09:00
Junegunn Choi
efbcd5a683 Require quotes on both sides for boundary matching even in --exact mode
Only requiring '-suffix in --exact mode is confusing and not
straightforward. Requiring '-prefix in --exact mode means that
the users can experience unintended mode switches while typing.

e.g.
     'it   -> fuzzy (many results)
     'it'  -> boundary (few results)
     'it's -> fuzzy (many results)

However, user who intends to input a boundary query should not be
interested in the intermediate results, and the number of matches
decreases as she types, so it should be okay.

On the other hand, user who does intend to type "it's" will be surprised
by the sudden decrease of the match count, but eventually get the right
result.
2024-08-29 17:08:23 +09:00
Junegunn Choi
6a67712944 Implement exact-boundary match type
Close #3963
2024-08-29 17:08:23 +09:00
dependabot[bot]
e8a690928d Bump crate-ci/typos from 1.23.6 to 1.24.1 (#3978)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.23.6 to 1.24.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.23.6...v1.24.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-08-29 17:04:44 +09:00
Junegunn Choi
0eee95af57 Fix CTRL-Z handling
Fix #3802

This fixes `xterm -e fzf` hangs on CTRL-Z

* Replace SIGSTOP with SIGTSTP
* Do not rely on SIGCONT
2024-08-29 16:41:52 +09:00
Junegunn Choi
a09c6e991a [vim] Ignore argument to fzf#exec if it's lower than minimum requirement 2024-08-28 13:49:41 +09:00
Junegunn Choi
d06c5ab990 [vim] Require fzf 0.53.0 or above (with --tmux)
Close #3980
2024-08-28 13:41:46 +09:00
Junegunn Choi
e0924d27b8 Change default --ellipsis to '··' 2024-08-27 19:41:39 +09:00
polluks2
2775b771f2 [man] Add italics (#3097) 2024-08-25 22:39:20 +09:00
junegunn
cf7a3c6a0e Deploying to master from @ junegunn/fzf@230cc6acc3 🚀 2024-08-25 00:01:56 +00:00
Junegunn Choi
230cc6acc3 Fix fzf-tmux on tmux 3.0
* Fix #3959
* https://github.com/junegunn/fzf/issues/3635#issuecomment-2085988777
2024-08-24 22:54:29 +09:00
dependabot[bot]
626a23a585 Bump crate-ci/typos from 1.23.1 to 1.23.6 (#3956)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.23.1 to 1.23.6.
- [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.23.1...v1.23.6)

---
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-08-24 16:12:01 +09:00
Junegunn Choi
74f196eebb [vim] Fix callback not run on exit code 1 2024-08-23 19:35:25 +09:00
Junegunn Choi
cf2242aea3 [bash] Revert skipping setting up fuzzy path completion
This partially reverts a2d0e8f not to break backward compatibility.
2024-08-22 19:39:22 +09:00
Junegunn Choi
8cb59e6fca [vim] Add 'exit' callback
A spec can have `exit` callback that is called with the exit status of fzf.
This can be used to clean up temporary resources or restore the original
state when fzf is closed without a selection.
2024-08-19 20:51:26 +09:00
Junegunn Choi
5cce17e80a Fix preview window incorrectly rendering empty line at the bottom 2024-08-18 11:38:37 +09:00
junegunn
ee5302fb2d Deploying to master from @ junegunn/fzf@387c6ef664 🚀 2024-08-18 00:02:08 +00:00
Junegunn Choi
387c6ef664 Support hyperlinks (OSC 8) in the main window
Close #2557
2024-08-14 23:04:05 +09:00
Junegunn Choi
581734c369 Fix OSC 8 parser 2024-08-14 19:19:28 +09:00
Junegunn Choi
d90a969c00 Add support for hyperlinks in preview window
Close #2165
2024-08-14 08:51:34 +09:00
dependabot[bot]
2c8a96bb27 Bump golang.org/x/sys from 0.22.0 to 0.24.0 (#3968)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.22.0 to 0.24.0.
- [Commits](https://github.com/golang/sys/compare/v0.22.0...v0.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 23:22:55 +09:00
dependabot[bot]
5da168db30 Bump golang.org/x/term from 0.22.0 to 0.23.0 (#3966)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/term/compare/v0.22.0...v0.23.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-08-13 22:37:21 +09:00
Junegunn Choi
e215e2daf3 Allow comments in $FZF_DEFAULT_OPTS and $FZF_DEFAULT_OPTS_FILE
Close #3961
2024-08-13 18:51:02 +09:00
Junegunn Choi
e28f5aa45b Make sure preview command is not run before Terminal is ready 2024-08-11 14:48:52 +09:00
Junegunn Choi
a2d0e8f233 [bash] Enable fuzzy path completion for all commands (#3958)
All commands with no custom completion defined.

Close #3957
2024-08-11 14:22:21 +09:00
junegunn
303d04106a Deploying to master from @ junegunn/fzf@c423c496a1 🚀 2024-08-11 00:01:54 +00:00
Eduardo D Sanchez
c423c496a1 fix: Add fallback for cygwin ps (#3955) 2024-08-07 14:42:27 +09:00
Junegunn Choi
4e85f72f0e Fix extra scroll offset in multi-line mode (--read0 or --wrap)
Fix #3950
2024-08-04 10:52:17 +09:00
junegunn
dd0737aac0 Deploying to master from @ junegunn/fzf@f90985845d 🚀 2024-08-04 00:02:03 +00:00
Junegunn Choi
f90985845d Fix '--tmux bottom' when the status line is not at the bottom
Fix #3948
2024-08-02 23:11:20 +09:00
Junegunn Choi
af4917dbb6 0.54.3 2024-07-31 21:51:54 +09:00
Junegunn Choi
d9404fcce4 Remove stale comment 2024-07-28 10:13:41 +09:00
junegunn
5c01fee5a9 Deploying to master from @ junegunn/fzf@9b27d68a37 🚀 2024-07-28 00:02:00 +00:00
Junegunn Choi
9b27d68a37 Fix build error 2024-07-27 19:13:24 +09:00
Junegunn Choi
b99d884e57 Minor refactoring 2024-07-27 18:59:50 +09:00
Junegunn Choi
587df594b8 Fix incompatibility of adaptive height and 'start:reload'
This command would cause a deadlock and make fzf crash:

  fzf --bind 'start:reload:ls' --height ~100%

Because,

1. 'start' event is handled by Terminal
2. When 'reload' is bound to 'start', fzf avoids starting the initial reader
3. Terminal waits for the initial input to find the right height when
   adaptive height is used
4. Because the initial reader is not started, Terminal never gets the
   initial list
5. No chance to trigger 'start:reload', hence deadlock

This commit fixes the above problem by extracting the reload command
bound to 'start' event and starting the initial reader with that command
instead of letting Terminal start it.

This commit also makes the environment variables available to
$FZF_DEFAULT_COMMAND.

  FZF_DEFAULT_COMMAND='echo $FZF_QUERY' fzf --query foo

Fix #3944
2024-07-27 11:30:25 +09:00
Junegunn Choi
b896e0d314 0.54.2 2024-07-26 17:44:09 +09:00
Junegunn Choi
559fb7ee45 Revert "Prefer LightRenderer over tcell on Windows"
This reverts commit dca2262fe6.

> For mouse support on mintty
> Fix #3847

The current implementation LightRenderer for Windows is unable to accept
non-ASCII input unlike the tcell renderer. So even though it supports
mouse on mintty, we shouldn't use it as the default.

* #3799
* #3847
2024-07-26 01:43:21 +09:00
Junegunn Choi
62545cd983 Display repology image in 3 columns 2024-07-25 12:49:02 +09:00
junegunn
c50812518e Deploying to master from @ junegunn/fzf@4cc5609d8b 🚀 2024-07-21 00:02:12 +00:00
Junegunn Choi
4cc5609d8b Fix invalid highlighting of truncated multi-line items 2024-07-21 01:09:39 +09:00
Junegunn Choi
50fa90dfb8 0.54.1 2024-07-19 17:10:49 +09:00
Charlie Vieth
a2c365e710 Update fastwalk to v1.0.8 for better MSYS detection and sorting (#3930)
This commit updates github.com/charlievieth/fastwalk to v1.0.8 which
improves MSYS detection and adds optional sorting of directory entries.
It also updates fzf to use the SortFilesFirst sort mode which improves
the output by making it a bit more sorted and grouped by directory
previously entries were visited in directory order (which is basically
random).

PRs Included:

  * https://github.com/charlievieth/fastwalk/pull/25
  * https://github.com/charlievieth/fastwalk/pull/27
  * https://github.com/charlievieth/fastwalk/pull/28
2024-07-19 13:17:41 +09:00
LangLangBart
b4ddffdc61 fix(fish): partially revert dbe8dc3 by removing the 'builtin' for cd 2024-07-17 22:13:01 +09:00
Junegunn Choi
8d4d184fc6 Change WinGet workflow to drop 'v' prefix from tag (#3927)
https://github.com/vedantmgoyal9/winget-releaser?tab=readme-ov-file#configuration-options-%EF%B8%8F
2024-07-17 12:38:10 +09:00
junegunn
ea23539b59 Deploying to master from @ junegunn/fzf@9e92b6f11e 🚀 2024-07-14 00:02:04 +00:00
Junegunn Choi
9e92b6f11e 0.54.0
New tags will have `v` prefix.

* https://github.com/junegunn/fzf/issues/2879
* https://github.com/golang/go/issues/32945

Close #2879
2024-07-08 22:51:48 +09:00
Junegunn Choi
6cbde812f6 [bash] Add code to the default list of programs to support completion
Close #3843
2024-07-08 22:51:47 +09:00
Junegunn Choi
3b2e932c13 Bind CTRL-/ and ALT-/ to toggle-wrap by default 2024-07-08 22:51:47 +09:00
dependabot[bot]
8ff4e52641 Bump golang.org/x/term from 0.21.0 to 0.22.0 (#3913)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/term/compare/v0.21.0...v0.22.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-07-08 22:51:27 +09:00
Charlie Vieth
2dbc874e3d Update charlievieth/fastwalk to use forward-slashes on WSL and MSYS (#3907)
This commit changes FZF to enforce that all paths are joined with
forward-slashes when running on WSL or MSYS
even when the FZF binary was compiled for Windows.

Update: github.com/charlievieth/fastwalk
Fixes:  https://github.com/junegunn/fzf/issues/3859

---------

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-07-08 22:21:37 +09:00
dependabot[bot]
039a2f1d04 Bump crate-ci/typos from 1.22.9 to 1.23.1 (#3912)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.22.9 to 1.23.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.22.9...v1.23.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-07-08 22:20:30 +09:00
junegunn
4ef1cf5b35 Deploying to master from @ junegunn/fzf@b44ab9e33c 🚀 2024-07-07 00:01:56 +00:00
Junegunn Choi
b44ab9e33c [completion] Use --wrap option in process completion
And remove the short preview window for showing the whole command.

Because it is important to be able to see the whole command before
deciding to kill it.
2024-07-06 10:20:58 +09:00
Junegunn Choi
8f4c23f1c4 Remove --walker-path-sep
Related: #3859 #3907 #3909
2024-07-05 20:15:03 +09:00
Junegunn Choi
23a391e715 [zsh] Fix backslash escaping (#3909)
Fix #3859

To test:

  FZF_CTRL_T_COMMAND="echo -E 'foo\bar\baz'; echo -E 'hello\world'"

  _fzf_compgen_path() {
    eval $FZF_CTRL_T_COMMAND
  }

  source shell/key-bindings.zsh
  source shell/completion.zsh
2024-07-05 01:46:36 +09:00
Junegunn Choi
035b0be29f Adjust offset immediately after 'first', 'last', and 'pos'
seq 100 | fzf --multi --sync --bind 'result:last+transform:for _ in $(seq 10); do echo -n "+select+down"; done'
2024-07-03 22:06:17 +09:00
LangLangBart
e1fcdbc337 fix(zsh): use the '=~' operator instead of grep (#3906) 2024-07-03 11:03:21 +09:00
Junegunn Choi
cfc149e994 Avoid printing ANSI reset codes
fzf --border --bind 'start:transform-border-label:echo -e "\x1b[0mfoo"'
2024-07-02 09:20:10 +09:00
Junegunn Choi
2faffbd1b7 Fill background color in padding area
fzf --color bg:blue --border --padding 1,2
2024-07-01 21:39:09 +09:00
junegunn
8db65704b9 Deploying to master from @ junegunn/fzf@797a01aed4 🚀 2024-06-30 00:01:54 +00:00
Junegunn Choi
797a01aed4 [man] Clarify --walker-path-sep=CHAR 2024-06-29 18:44:28 +09:00
Junegunn Choi
bf515a3d32 Add --walker-path-sep=CHAR to use a different path separator
This is needed when you run a Windows binary on WSL or zsh on Windows
where forward slashes are expected.

  export FZF_DEFAULT_OPTS='--walker-path-sep /'

Close #3859
2024-06-29 17:13:31 +09:00
Junegunn Choi
a06745826a [zsh] Fix completion error on openSUSE Tumbleweed
Fix suggested by @LangLangBart

Fix #3890
2024-06-28 16:59:56 +09:00
Junegunn Choi
0420ed4f2a Empty --marker-multi-line if --marker is empty 2024-06-25 20:49:42 +09:00
Junegunn Choi
3b944addd4 Allow removing header line with change-header and transform-header
If the new header is an empty string.

  fzf --header loading --bind 'start:reload:sleep 3; ls' --bind 'load:change-header:'
  fzf --header loading --bind 'start:reload:sleep 3; ls' --bind 'load:transform-header:'
2024-06-25 17:14:11 +09:00
Junegunn Choi
70bf8bc35d Add --wrap option and 'toggle-wrap' action (#3887)
* `--wrap`
* `--wrap-sign`
* `toggle-wrap`

Close #3619
Close #2236
Close #577
Close #461
2024-06-25 17:08:47 +09:00
dependabot[bot]
724f8a1d45 Bump crate-ci/typos from 1.22.7 to 1.22.9 (#3894)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.22.7 to 1.22.9.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.22.7...v1.22.9)

---
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-06-24 23:55:53 +09:00
Junegunn Choi
cc2b2146ee ADVANCED.md: Remove unnecessary : | fzf
See 5b52833
2024-06-24 17:23:14 +09:00
Junegunn Choi
8689f5f230 Fix rubocop warning 2024-06-24 17:16:56 +09:00
林千里
e9e0011f1d fix zsh ${(@)history} syntax does not work with ksh_arrays (#3893)
When `setopt ksh_arrays` in zsh, `${(@kv)history}` gives only the first entry rather than all.
2024-06-24 17:07:55 +09:00
Junegunn Choi
5b52833785 Do not start the initial reader if 'reload*' is bound to 'start' 2024-06-24 17:05:53 +09:00
Koichi Murase
1525768094 [man] Escape hyphens to prevent conversion to Unicode hyphens (#3885)
ASCII hyphens (U+002D HYPHEN-MINUS) in the option names (e.g. -x and
--extended) and the code examples in the man pages should be escaped
as \- (e.g. \-x and \-\-extended) to prevent them being converted to
Unicode hyphens in some environments.

For example, in openSUSE Tumbleweed, the raw ASCII hyphens in the
man-page sources are configured to be the Unicode hyphen (U+2010
HYPHEN).  This makes it impossible to search the option name in the
man page by e.g. /--extended[RET].  A problem also arises in copying
and pasting option names and code examples from the man page.  It
appears to be the normal ASCII hyphens by appearance (in typical
terminal fonts) but are not recognized as the ASCII hyphens by the
`fzf` command.
2024-06-24 09:32:35 +09:00
Junegunn Choi
a70ea4654e Fix regression in separator display 2024-06-23 18:23:46 +09:00
Junegunn Choi
b02bf9b6bb Fix panic on extremely short terminals
Fix #3889
2024-06-23 11:27:03 +09:00
junegunn
bee7bc5324 Deploying to master from @ junegunn/fzf@7c2ffd3fef 🚀 2024-06-23 00:01:48 +00:00
Junegunn Choi
7c2ffd3fef Make transform*, --info-command, and execute-silent cancellable
Users can press CTRL-C after 1 second to terminate the command.

Close #3883
2024-06-22 17:24:47 +09:00
LangLangBart
db01e7dab6 fix(zsh): add (s) modifier to perl command (#3882) 2024-06-20 17:51:52 +09:00
Junegunn Choi
2326c74eb2 Code cleanup 2024-06-20 17:06:44 +09:00
Junegunn Choi
b9d15569e8 Fix test case for validateSign 2024-06-20 01:48:24 +09:00
Junegunn Choi
c3cc378d89 Allow empty pointer and marker
Close #3879
2024-06-20 01:45:06 +09:00
Junegunn Choi
27d1f5e0a8 Fix typos 2024-06-20 00:58:51 +09:00
Junegunn Choi
540632bb9e Add --info-command for customizing the input text
Close #3866
2024-06-20 00:53:18 +09:00
Junegunn Choi
d9c028c934 fzf-preview.sh: Let chafa decide the right format
Close #3822

  Output encoding:
    -f, --format=FORMAT  Set output format; one of [iterm, kitty, sixels,
                       symbols]. Iterm, kitty and sixels yield much higher
                       quality but enjoy limited support. Symbols mode yields
                       beautiful character art.
2024-06-19 19:25:46 +09:00
Junegunn Choi
c54ad82e8d Clarify that --nth applies after --with-nth transformation
Close #3873
2024-06-19 17:01:35 +09:00
bsdmp
295b89631b Add wpath and dpath pledges on OpenBSD to make --tmux work (#3877) 2024-06-19 11:26:08 +09:00
Jan Palus
6179faf778 mod: upgrade fastwalk to 1.0.4 (#3878)
Fixes #3832
2024-06-19 11:23:43 +09:00
dependabot[bot]
16dc236a25 Bump crate-ci/typos from 1.22.3 to 1.22.7 (#3871)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.22.3 to 1.22.7.
- [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.22.3...v1.22.7)

---
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-06-19 11:23:30 +09:00
Hexin
61ae9d75b6 Remove comment in command substitution (#3875) 2024-06-19 11:22:57 +09:00
Junegunn Choi
e2401aca68 Add 'offset-middle' action 2024-06-17 18:34:10 +09:00
Junegunn Choi
59943cbb48 Fire 'result' even when input stream is not complete
Related: #3866
2024-06-17 17:54:52 +09:00
Junegunn Choi
02634d404d Remove {fzf:query} from man page 2024-06-17 17:53:02 +09:00
Junegunn Choi
ed12925f7d --sync: Suppress initial render also when focus event is bound 2024-06-17 17:00:49 +09:00
Junegunn Choi
e0ddb97ab4 Improved --sync behavior
When --sync is provided, fzf will not render the interface until the
initial filtering and associated actions (bound to any of 'start',
'load', or 'result') are complete.
2024-06-17 00:11:57 +09:00
junegunn
b8c01af0fc Deploying to master from @ junegunn/fzf@6de0a7ddc1 🚀 2024-06-16 00:01:59 +00:00
Junegunn Choi
6de0a7ddc1 --sync: Do not start TUI until initial filtering is complete 2024-06-15 10:46:28 +09:00
Junegunn Choi
79196c025d Clean up GitHub Actions workflow
fzf does not uses tcell-based renderer on systems where light renderer
can be used since dca2262. So this has become meaningless.
2024-06-15 10:27:54 +09:00
Junegunn Choi
94c33ac020 Fix panic when parent process is killed
Fix #3863
2024-06-15 10:23:03 +09:00
Junegunn Choi
b2ecb6352c Make GET endpoint available from 'execute' and 'transform' actions 2024-06-14 21:33:42 +09:00
Junegunn Choi
9dc3ed638a --walker-skip should also handle symlinks to directories
Fix #3858
2024-06-13 22:55:31 +09:00
dependabot[bot]
0acace1ace Bump crate-ci/typos from 1.21.0 to 1.22.3 (#3850)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.21.0 to 1.22.3.
- [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.21.0...v1.22.3)

---
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-06-13 20:51:13 +09:00
dependabot[bot]
1a2d37e1e6 Bump golang.org/x/term from 0.20.0 to 0.21.0 (#3849)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/term/compare/v0.20.0...v0.21.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-06-13 20:50:57 +09:00
LangLangBart
22adb6494f chore(shell): Separate declaration and assignment for zsh legacy versions (#3856) 2024-06-13 17:33:23 +09:00
Samara Jinnah
e023736c30 [zsh] Prevent glob expansion in history widget (#3855) 2024-06-13 10:43:33 +09:00
Junegunn Choi
dca2262fe6 Prefer LightRenderer over tcell on Windows
For mouse support on mintty

Fix #3847
2024-06-12 21:53:18 +09:00
Junegunn Choi
0684a20ea3 Fix invalid mouse offset for --height on Windows 2024-06-12 21:18:02 +09:00
Junegunn Choi
a1a72bb8d1 Do not open tmux or winpty in --filter mode 2024-06-12 21:02:48 +09:00
ismay
144d55a5be [fish] Merge history before searching (#3852)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-06-12 13:56:20 +09:00
Junegunn Choi
7fc13c5cfd Less aggressive chunk cache invalidation for --tail 2024-06-10 20:33:55 +09:00
Junegunn Choi
dfee7af57b Fix divide by zero error with --tiebreak=end for long items
Fix #3846
2024-06-10 08:26:53 +09:00
junegunn
9b0e2daf02 Deploying to master from @ junegunn/fzf@590060a16b 🚀 2024-06-09 00:02:07 +00:00
Junegunn Choi
590060a16b Remove unused field 2024-06-07 17:05:33 +09:00
Junegunn Choi
368294edf6 Reduce flickering of the list when the list is truncated by --tail 2024-06-07 16:59:09 +09:00
Junegunn Choi
c4a9ccd6af 0.53.0 2024-06-06 22:03:26 +09:00
Junegunn Choi
cbf91f2ed3 ADVANCED.md: /dev/tty redirection no longer required 2024-06-06 21:58:02 +09:00
Junegunn Choi
b1460d4787 hasPreviewFlags should ignore escaped placeholder
This reload command wouldn't run before the fix:

  : | fzf --bind 'start:reload:echo \{}'
2024-06-06 17:40:15 +09:00
Junegunn Choi
7dc9e14874 Update docs 2024-06-06 17:40:15 +09:00
Junegunn Choi
1616ed543d Fix index out of bounds error caused by outdated offset 2024-06-06 00:23:58 +09:00
Junegunn Choi
dc73fba188 [man] Clarification on --scheme options 2024-06-05 14:29:50 +09:00
Junegunn Choi
ef148dfd37 Handle int32 overflow
yes | fzf --tail=10 --preview 'echo "{n}"'
2024-06-05 14:29:50 +09:00
Junegunn Choi
93bbb3032d Add --tail=NUM to limit the number of items to keep in memory 2024-06-04 17:50:46 +09:00
Junegunn Choi
4c83d8596d Add new options to bash completion 2024-06-03 09:45:20 +09:00
Junegunn Choi
d453e6d7db Update ADVANCED.md: Use --tmux instead of fzf-tmux 2024-06-03 09:41:40 +09:00
Junegunn Choi
c29533994f Fix invalid default of selected-hl (--color)
It should default to 'hl' instead of 'current-hl'
2024-06-02 18:09:41 +09:00
Junegunn Choi
1afe13b5b5 Merge remote-tracking branch 'origin/master' into devel 2024-06-02 17:59:04 +09:00
Junegunn Choi
36600eaaa9 Update CHANGELOG: clarification 2024-06-02 17:58:44 +09:00
junegunn
3ee1fc2034 Deploying to master from @ junegunn/fzf@124cd70710 🚀 2024-06-02 00:01:52 +00:00
Junegunn Choi
e2f93e5a2d --tmux vs. --height: Last one wins 2024-06-01 22:11:15 +09:00
Junegunn Choi
cfdf2f1153 Update README 2024-06-01 16:20:03 +09:00
Junegunn Choi
e042143e3f Immediately close standard output of the child process
Fix #3828
2024-06-01 15:22:05 +09:00
Junegunn Choi
7c613d0d9b Do not disable --height on mintty (because it works) 2024-06-01 14:45:54 +09:00
Junegunn Choi
b00d46bc14 Fix --height on Windows 2024-06-01 14:36:41 +09:00
Junegunn Choi
555b0d235b Ignore --height option if it's not supported on the platform
This is to make shell integration work out of the box on Git bash.

  eval "$(fzf --bash)"
  vim <CTRL-T>
    # would print '--height option is currently not supported on this platform'
2024-06-01 14:35:45 +09:00
Junegunn Choi
564daf9a7d Set standard input of 'man' process to os.Stdin 2024-06-01 13:23:46 +09:00
Junegunn Choi
41bcbe342f Revert "An '--expect' key should execute actions bound to the key"
To be backward compatible.

Close #3829
2024-06-01 13:21:59 +09:00
LangLangBart
dbe8dc344e [fish] Use builtins for cd and history (#3830)
Close #3826
2024-06-01 11:28:02 +09:00
Junegunn Choi
e33fb59da1 Update CHANGELOG 2024-05-31 16:57:35 +09:00
Junegunn Choi
7aa88aa115 Fix error message on invalid --tmux option
fzf --tmux foobar
  # not a valid integer: foobar
  # ->
  # invalid tmux option: foobar (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])
2024-05-31 16:57:35 +09:00
LangLangBart
2b6d600879 [zsh] Enhance CTRL-R to display multi-line entires (#3823)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-31 16:57:35 +09:00
Junegunn Choi
05c765d442 [fish] Add --nth 2..,.. to allow anchored search against command 2024-05-31 16:57:35 +09:00
Junegunn Choi
49b496269c Fix index out of bounds error on scroll-down action 2024-05-31 16:57:35 +09:00
Junegunn Choi
7405925952 [bash] Indent multi-line history entries 2024-05-31 16:57:35 +09:00
Junegunn Choi
3afd543a7e [fish] Use perl instead of sed to strip leading tabs
https://github.com/junegunn/fzf/pull/3807#discussion_r1619520105
2024-05-30 10:23:20 +09:00
Junegunn Choi
b4f2cde5ac [fish] Better multi-line support for CTRL-R
Prepend each entry with an index number so that multi-line entries can
be clearly distinguished.
2024-05-29 20:16:49 +09:00
Junegunn Choi
ed53ef7cee [shell] Add --highlight-line to CTRL-R bindings 2024-05-29 20:13:41 +09:00
Junegunn Choi
12630b124d Make --tmux argument optional 2024-05-29 02:16:18 +09:00
Junegunn Choi
1d59ac09d2 Pass-through error message from 'tmux display-popup'
fzf --tmux 9999
    # height too large
2024-05-29 02:07:56 +09:00
Junegunn Choi
a8f3a0dd59 Merge branch 'master' into devel 2024-05-28 23:19:26 +09:00
Konstantin-Glukhov
124cd70710 [vim] Do not prepend CWD to path starting with a backslash on Windows (#3820)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-28 23:15:14 +09:00
Junegunn Choi
782de139c8 [vim] Native --tmux fix for Neovim 2024-05-28 19:27:31 +09:00
Junegunn Choi
32eb32ee5e Add multi-line example to CHANGELOG 2024-05-27 01:48:46 +09:00
Junegunn Choi
2f51eb2b41 Different marker for the first and last line of multi-line entries
Can be configured via `--marker-multi-line`
2024-05-27 01:35:05 +09:00
Junegunn Choi
0ccbd79e10 Fix --help output: marker default
Co-authored-by: LangLangBart <92653266+LangLangBart@users.noreply.github.com>
2024-05-26 09:24:30 +09:00
junegunn
99bd6de541 Deploying to master from @ junegunn/fzf@daa602422d 🚀 2024-05-26 00:01:51 +00:00
Junegunn Choi
1fef36e4bc Do not allow tabs in pointer and marker 2024-05-25 16:31:34 +09:00
Junegunn Choi
89375005b5 Fix option validation order 2024-05-25 16:23:13 +09:00
Junegunn Choi
88e78c9193 Update integration test to use named pipes 2024-05-25 12:03:20 +09:00
Junegunn Choi
29a19ad080 Update CHANGELOG 2024-05-25 09:40:17 +09:00
Junegunn Choi
2a039ab746 Describe exit code 126 2024-05-24 19:32:44 +09:00
Junegunn Choi
7e9a0fcdbd Change default --scroll-off to 3 2024-05-24 19:25:50 +09:00
Junegunn Choi
7a97532547 Fix --scroll-off for multi-line mode 2024-05-24 19:23:36 +09:00
Junegunn Choi
996abb2831 Fix incorrect colors for selected-{fg,bg,hl}
When a non-default base color scheme is specified, fzf would choose incorrect
colors for selected-*.

  fzf --color 'light,fg:238,bg:255,bg+:253' -m
2024-05-24 00:46:01 +09:00
Junegunn Choi
da500a358f Use bold bar as the default marker 2024-05-24 00:31:20 +09:00
Junegunn Choi
c36b846acc [vim] Open cmd.exe window only on mintty < 3.4.5 without winpty 2024-05-23 21:27:29 +09:00
Junegunn Choi
d9b5c9b2be Address review comments by @Konfekt
d4216b0dcc
2024-05-23 21:14:08 +09:00
Junegunn Choi
3dee8778d0 execute: Open separate handles to /dev/tty (in, out, err)
# This will no longer cause 'Vim: Warning: Output is not to a terminal'
  fzf --bind 'enter:execute:vim {}' > /tmp/foo
2024-05-23 21:11:12 +09:00
Junegunn Choi
d4216b0dcc Use MSYS=enable_pcon instead of winpty on mintty 3.4.5 or later 2024-05-23 18:42:54 +09:00
Enno
bfe2bf4dce [vim] Git Bash Mintty: only use cmd.exe if winpty missing (#3811)
* Git Bash Mintty: only use cmd.exe if winpty missing

Addresses https://github.com/junegunn/fzf/issues/3809

* preferably use term in Git Bash for popup window

See https://github.com/junegunn/fzf/pull/3811#issuecomment-2124241321
2024-05-23 09:07:54 +09:00
Junegunn Choi
561f9291fd [vim] Replace backslashes with forward slashes on win32unix 2024-05-23 09:03:43 +09:00
Junegunn Choi
b5b0d6b3ea Do not run as winpty proxy if winpty is not available 2024-05-23 08:47:38 +09:00
Junegunn Choi
a90426b7ca Add print(...) action 2024-05-22 22:18:24 +09:00
Junegunn Choi
303c3bae7f proxy: Pass SIGINT to the child fzf 2024-05-22 22:14:00 +09:00
Junegunn Choi
6b4358f641 An '--expect' key should execute actions bound to the key
Fix #3810
2024-05-22 20:39:09 +09:00
Junegunn Choi
552158f3ad Ignore SIGINT when running as proxy 2024-05-22 20:01:37 +09:00
Junegunn Choi
7205203dc8 Update CHANGELOG 2024-05-21 02:07:49 +09:00
Junegunn Choi
0cadf70072 Update the summary 2024-05-21 01:57:22 +09:00
Junegunn Choi
076b3d0a9a Embed man page in the binary and show it on 'fzf --man' 2024-05-21 01:06:10 +09:00
Junegunn Choi
7b0c9e04d3 Change default marker 2024-05-20 18:51:52 +09:00
Junegunn Choi
573df524fe Use winpty to launch fzf in Git bash (mintty)
Close #3806

Known limitation:
* --height cannot be used
2024-05-20 18:24:14 +09:00
Junegunn Choi
aee417c46a Respect $NO_COLOR environment variable
Close #1762
2024-05-20 10:50:00 +09:00
Junegunn Choi
04db44067d Implement multi-line display of multi-line items 2024-05-20 09:25:30 +09:00
Junegunn Choi
5b204c54f9 Change default pointer and marker character
* Pointer: '▌'
* Marker: '▏'

They will still be set to '>' if `--no-unicode` is given.

Reasons:
* They look okay
* They work better with multi-line items (WIP)
2024-05-19 15:51:32 +09:00
junegunn
daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 2024-05-19 00:01:47 +00:00
Junegunn Choi
04dfb14e32 Do not 'become' inside a tmux popup
fzf --tmux center --bind 'enter:become:vim {}'
2024-05-18 17:08:36 +09:00
Junegunn Choi
c24256cba3 Update README
* Tidy up
* Mention `--tmux`
2024-05-18 17:08:36 +09:00
Junegunn Choi
685fb71d89 [vim] Use native --tmux option instead of fzf-tmux when possible 2024-05-18 17:08:36 +09:00
Junegunn Choi
83b6033906 Add --tmux option to replace fzf-tmux script 2024-05-18 17:08:36 +09:00
Zhizhen He
01e7668915 chore: use strings.ReplaceAll (#3801) 2024-05-18 17:06:33 +09:00
Enno
0994d9c881 Make :FZF work in Vim from Git Bash (#3798)
* make :FZF work in Vim from Git Bash

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

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

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

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

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

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

* prefer doubling slashed instead of generic env. var

If MSYS_NO_PATHCONV=1 is used, then all arguments are preserved, in particular possibly paths passed in s:command.
Therefore, only avoid converting `/C` from `cmd` to a path.
2024-05-15 17:26:49 +09:00
LangLangBart
030428ba43 docs: update zsh integration instructions (#3794) 2024-05-15 01:59:43 +09:00
Junegunn Choi
8a110e02b9 Fix tcell test case 2024-05-15 00:45:23 +09:00
Junegunn Choi
86d92c17c4 Refactor tui.TtyIn() 2024-05-15 00:28:56 +09:00
Junegunn Choi
c4cc7891b4 Revert "Close handles to /dev/tty", instead reuse handles 2024-05-15 00:15:29 +09:00
Junegunn Choi
218843b9f1 Close handles to /dev/tty 2024-05-14 21:49:47 +09:00
Junegunn Choi
d274d093af Render UI directly to /dev/tty
See https://github.com/junegunn/fzf/discussions/3792

This allows us to separately capture the standard error from fzf and its
child processes, and there's less chance of user errors of redirecting
the error stream and hiding fzf.
2024-05-14 16:32:26 +09:00
Junegunn Choi
6432f00f0d 0.52.1 2024-05-14 01:54:30 +09:00
junegunn
4e9e842aa4 Deploying to master from @ junegunn/fzf@07880ca441 🚀 2024-05-12 00:01:52 +00:00
LangLangBart
07880ca441 chore: Update flags to include long-form options for case (#3785) 2024-05-09 20:39:21 +09:00
Junegunn Choi
bcda25a513 0.52.0 2024-05-08 00:15:30 +09:00
Junegunn Choi
8256fcde15 Minor fixup 2024-05-07 23:49:30 +09:00
Junegunn Choi
af65aa298a Add color names: selected-{fg,bg,hl} 2024-05-07 23:38:06 +09:00
Junegunn Choi
6834d17844 [vim] Revert 7411da8d5a
Fix #3777
2024-05-07 20:16:18 +09:00
Junegunn Choi
ed511d7867 [install] tar --no-same-owner
Close #3776
2024-05-07 20:00:13 +09:00
Junegunn Choi
cd8d736a9f [shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2024-05-07 19:31:56 +09:00
Junegunn Choi
0952b2dfd4 Rename --cursor-line to --highlight-line 2024-05-07 19:22:39 +09:00
Junegunn Choi
4bedd33c59 Refactor the code to remove global variables 2024-05-07 16:58:17 +09:00
Junegunn Choi
c5fb0c43f9 Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2024-05-07 01:34:35 +09:00
Junegunn Choi
9e4780510e Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2024-05-07 01:26:25 +09:00
Junegunn Choi
e8405f40fe Refactor the code so that fzf can be used as a library (#3769) 2024-05-07 01:06:42 +09:00
dependabot[bot]
065b9e6fb2 Bump golang.org/x/term from 0.19.0 to 0.20.0 (#3774)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 22:51:10 +09:00
dependabot[bot]
98141ca7d8 Bump crate-ci/typos from 1.20.10 to 1.21.0 (#3772)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.10...v1.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 22:50:50 +09:00
Junegunn Choi
501577ab28 Fix flaky test case 2024-05-06 13:59:16 +09:00
Junegunn Choi
5669f48343 Do not enable delayed expansion mode when running cmd.exe
And simplify the argument escaping code. Fix #3764.

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

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

  # No sudden expansion of the arguments on '!'
  fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!" --prompt "&|<>()@^%!"
2024-05-06 13:46:06 +09:00
Junegunn Choi
24ff66d4a9 Fix change-preview reset by change-preview-window
Fix #3770
2024-05-06 09:40:02 +09:00
Junegunn Choi
bf184449bc Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2024-05-06 09:27:58 +09:00
Kuremu
7b98c2c653 Add click-header event for reporting clicks within header (#3768)
Sets $FZF_CLICK_HEADER_LINE and $FZF_CLICK_HEADER_COLUMN env vars with
coordinates of the last click inside and relative to the header and
fires click-header event.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-05 18:56:43 +09:00
Junegunn Choi
b6add2a257 Fix rendering of preview window border of tcell renderer
(sleep 1; find .) |
    go run -tags tcell main.go --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 17:09:00 +09:00
Junegunn Choi
2bd41f1330 Reduce flicking when changing the size of the preview window with --border
(sleep 1; find .) |
    fzf --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 16:49:30 +09:00
Junegunn Choi
c37cd11ca5 Remove unnecessary flicking when changing the size of the preview window
fzf --bind 'space:change-preview-window(60%|70%|80%|90%|hidden|)' --preview 'cat {}'
2024-05-05 11:10:54 +09:00
Junegunn Choi
9dee8edc0c Clear characters on 1-column margin after the preview window on the left 2024-05-05 11:06:50 +09:00
Junegunn Choi
f6aa28c380 Fix --info inline-right not properly clearing the previous output
(seq 100000; sleep 1) | fzf --info inline-right --bind load:change-query:x
2024-05-03 12:18:34 +09:00
cyqsimon
dba1644518 Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-02 20:52:27 +09:00
Junegunn Choi
260a65b0fb 0.51.0 2024-05-01 14:14:06 +09:00
Junegunn Choi
835d2fb98c [vim] Fix argument escaping for Windows batch file
Fix #3620
2024-05-01 10:02:26 +09:00
Charlie Vieth
a9811addaa Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
2024-05-01 09:15:47 +09:00
dependabot[bot]
ee9d88b637 Bump crate-ci/typos from 1.20.9 to 1.20.10 (#3757)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.9 to 1.20.10.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.9...v1.20.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 23:10:50 +09:00
Junegunn Choi
194a763c46 Escaping for cmd.exe: always use double quotes 2024-04-28 23:30:44 +09:00
Junegunn Choi
8d74446bef Fix escaping for cmd.exe
Close #3651
Close #2609
2024-04-28 22:03:00 +09:00
Junegunn Choi
7ed6c7905c Determine shell type once by the basename 2024-04-28 20:11:05 +09:00
Junegunn Choi
159a37fa37 Restore CmdLine parameter when running commands using cmd.exe 2024-04-28 16:01:19 +09:00
junegunn
f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 2024-04-28 00:01:30 +00:00
Junegunn Choi
4a68eac99b Suggest using toggle+up instead of toggle-up 2024-04-27 19:04:30 +09:00
Junegunn Choi
2665580120 Add $FZF_POS environment variable
Close #2175
Close #3753
2024-04-27 18:57:22 +09:00
Junegunn Choi
a4391aeedd Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
2024-04-27 18:36:37 +09:00
dependabot[bot]
b86a967ee2 Bump crate-ci/typos from 1.19.0 to 1.20.9 (#3749)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.19.0 to 1.20.9.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.19.0...v1.20.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-27 17:57:11 +09:00
Junegunn Choi
608232568b Add 'change-multi' action
Close #3754
2024-04-27 17:38:06 +09:00
Junegunn Choi
7f85beccb5 [completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
2024-04-27 14:14:30 +09:00
Junegunn Choi
767f1255ab Make completion.bash load faster
* Reduce number of `__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)`s
* Clean up options in fzf completion
* Remove telnet completion
2024-04-25 16:54:51 +09:00
Junegunn Choi
fddbfe7b0e Fix 'reload' not terminating closed standard input stream
Fix #3750
2024-04-25 16:49:06 +09:00
Junegunn Choi
4ab7fdc28e Merge identical case clauses 2024-04-25 16:34:05 +09:00
Junegunn Choi
e352b68878 Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
2024-04-24 18:20:30 +09:00
Junegunn Choi
207deeadba Add -trimpath to build command 2024-04-23 17:24:50 +09:00
Cheng
d18d92f925 Replace fmt.Errorf with no parameters with errors.New (#3747) 2024-04-22 09:35:33 +09:00
junegunn
af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 2024-04-21 00:01:49 +00:00
Junegunn Choi
d8bfb6712d Remove invalid 'result' event when using --sync option
When the search for the initial query doesn't finish immediately
fzf would trigger an invalid 'result' event for an empty query.

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

  seq 1000000 | fzf --query 99 --bind result:accept --sync
    # Should print 99, but fzf would print 1
2024-04-20 14:42:43 +09:00
Junegunn Choi
f864f8b5f7 Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
2024-04-19 22:40:38 +09:00
Junegunn Choi
31d72efba7 Describe how to build fzf from the latest source using brew 2024-04-18 23:37:12 +09:00
LangLangBart
d169c951f3 fix: Move 'emulate' command outside interactive check (#3736) 2024-04-17 18:03:12 +09:00
Junegunn Choi
90d7e38909 [fzf-tmux] Replace command -v with which
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

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

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

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

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

    # Default behavior
    fzf --bind space:jump

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

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

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

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

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

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

* https://github.com/junegunn/fzf/pull/3675#issuecomment-2044860901
* f103aa4753
2024-04-10 00:46:09 +09:00
Junegunn Choi
4cd37fc02b Disable line wrapping during rendering
Prevent unwanted line wraps that break the layout when the actual
display width of a character is different than expected.
2024-04-09 00:26:25 +09:00
LangLangBart
69b9d674a3 chore: Add new option in issue checklist and modify requirements (#3715) 2024-04-07 10:25:12 +09:00
junegunn
bad8061547 Deploying to master from @ junegunn/fzf@62963dcefd 🚀 2024-04-07 00:01:37 +00:00
90 changed files with 6918 additions and 2941 deletions

View File

@@ -15,6 +15,8 @@ body:
required: true
- label: I have searched through the existing issues
required: true
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
required: false
- type: input
attributes:

View File

@@ -36,7 +36,7 @@ jobs:
run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
@@ -46,6 +46,3 @@ jobs:
- name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
- name: Integration test (tcell)
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose

View File

@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@v1.19.0
- uses: crate-ci/typos@v1.24.1

View File

@@ -10,6 +10,5 @@ jobs:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: junegunn.fzf
version: ${{ github.event.release.tag_name }}
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,4 +1,5 @@
---
version: 2
project_name: fzf
before:
@@ -6,56 +7,9 @@ before:
- go mod download
builds:
- id: fzf-macos
binary: fzf
goos:
- darwin
goarch:
- amd64
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
post: |
sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
}
EOF
gon /tmp/fzf-gon-amd64.hcl
'
- id: fzf-macos-arm
binary: fzf
goos:
- darwin
goarch:
- arm64
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
post: |
sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf"
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
}
EOF
gon /tmp/fzf-gon-arm64.hcl
'
- id: fzf
goos:
- darwin
- linux
- windows
- freebsd
@@ -71,6 +25,8 @@ builds:
- 5
- 6
- 7
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
ignore:
@@ -83,6 +39,42 @@ builds:
- goos: openbsd
goarch: arm64
# .goreleaser.yaml
notarize:
macos:
- # Whether this configuration is enabled or not.
#
# Default: false.
# Templates: allowed.
enabled: "{{ not .IsSnapshot }}"
# Before notarizing, we need to sign the binary.
# This blocks defines the configuration for doing so.
sign:
# The .p12 certificate file path or its base64'd contents.
certificate: "{{.Env.MACOS_SIGN_P12}}"
# The password to be used to open the certificate.
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
# Then, we notarize the binaries.
notarize:
# The issuer ID.
# Its the UUID you see when creating the App Store Connect key.
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
# Key ID.
# You can see it in the list of App Store Connect Keys.
# It will also be in the ApiKey filename.
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
# The .p8 key file path or its base64'd contents.
key: "{{.Env.MACOS_NOTARY_KEY}}"
# Whether to wait for the notarization to finish.
# Not recommended, as it could take a really long time.
wait: true
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
builds:
@@ -94,21 +86,15 @@ archives:
files:
- non-existent*
checksum:
extra_files:
- glob: ./dist/fzf-*darwin*.zip
release:
github:
owner: junegunn
name: fzf
prerelease: auto
name_template: '{{ .Tag }}'
extra_files:
- glob: ./dist/fzf-*darwin*.zip
name_template: '{{ .Version }}'
snapshot:
name_template: "{{ .Tag }}-devel"
name_template: "{{ .Version }}-devel"
changelog:
sort: asc

View File

@@ -6,7 +6,7 @@ Lint/ShadowingOuterLocalVariable:
Enabled: false
Style/MethodCallWithArgsParentheses:
Enabled: true
IgnoredMethods:
AllowedMethods:
- assert
- exit
- paste
@@ -15,7 +15,7 @@ Style/MethodCallWithArgsParentheses:
- refute
- require
- send_keys
IgnoredPatterns:
AllowedPatterns:
- ^assert_
- ^refute_
Style/NumericPredicate:

View File

@@ -1,18 +1,17 @@
Advanced fzf examples
======================
* *Last update: 2024/01/20*
* *Requires fzf 0.46.0 or above*
* *Last update: 2024/06/24*
* *Requires fzf 0.54.0 or later*
---
<!-- vim-markdown-toc GFM -->
* [Introduction](#introduction)
* [Screen Layout](#screen-layout)
* [Display modes](#display-modes)
* [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux)
* [Popup window support](#popup-window-support)
* [`--tmux`](#--tmux)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources)
@@ -63,7 +62,7 @@ learn its wide variety of features.
This document will guide you through some examples that will familiarize you
with the advanced features of fzf.
Screen Layout
Display modes
-------------
### `--height`
@@ -104,56 +103,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
```
### `fzf-tmux`
### `--tmux`
Before fzf had `--height` option, we would open fzf in a tmux split pane not
to take up the whole screen. This is done using `fzf-tmux` script.
(Requires tmux 3.3 or later)
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
```sh
# Open fzf on a tmux split pane below the current pane.
# Takes the same set of options.
fzf-tmux --layout=reverse
# Open fzf in a tmux popup at the center of the screen with 70% width and height
fzf --tmux 70%
```
![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png)
![image](https://github.com/junegunn/fzf/assets/700826/9c365405-c700-49b2-8985-60d822ed4cff)
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
`--height` option. But the advantage of it is that it's more flexible.
(See `man fzf-tmux` for available options.)
`--tmux` option is silently ignored if you're not on tmux. So if you're trying
to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
```sh
# On the right (50%)
fzf-tmux -r
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
# --tmux is specified later so it takes precedence over --height when on tmux.
# If you're not on tmux, --tmux is ignored and --height is used instead.
fzf --height 70% --tmux 70%
```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png)
You can also specify the position, width, and height of the popup window in
the following format:
![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png)
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
```sh
# Open tmux in a tmux popup window (default size: 50% of the screen)
fzf-tmux -p
# 80% width, 60% height
fzf-tmux -p 80%,60%
# 100% width and 60% height
fzf --tmux 100%,60% --border horizontal
```
![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png)
![image](https://github.com/junegunn/fzf/assets/700826/f80d3514-d69f-42f2-a8de-a392a562bfcf)
```sh
# On the right (50% width)
fzf --tmux right
```
![image](https://github.com/junegunn/fzf/assets/700826/4033ade4-7efa-421b-a3fb-a430d197098a)
```sh
# On the left (40% width and 70% height)
fzf --tmux left,40%,70%
```
![image](https://github.com/junegunn/fzf/assets/700826/efe43881-2bf0-49ea-ab2e-1377f778cd52)
> [!TIP]
> You might also want to check out my tmux plugins which support this popup
> window layout.
>
@@ -363,7 +361,7 @@ projects, and it will free up memory as you narrow down the results.
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
@@ -374,11 +372,9 @@ INITIAL_QUERY="${*:-}"
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
an empty input (`: | fzf`), then we make it start the initial Ripgrep
process immediately via `start:reload` binding. This way, fzf owns the
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
the process will keep running in the background.
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
the initial Ripgrep process immediately via `start:reload` binding for the
consistency of the code.
- Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt.
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
@@ -402,7 +398,7 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
@@ -446,7 +442,7 @@ CTRL-F.
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
@@ -489,7 +485,7 @@ prevent immediate evaluation.
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
@@ -529,15 +525,15 @@ Kubernetes pods.
```bash
pods() {
: | command='kubectl get pods --all-namespaces' fzf \
command='kubectl get pods --all-namespaces' fzf \
--info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}

View File

@@ -24,13 +24,23 @@ make build
make release
```
> :warning: Makefile uses git commands to determine the version and the
> revision information for `fzf --version`. So if you're building fzf from an
> [!WARNING]
> Makefile uses git commands to determine the version and the revision
> information for `fzf --version`. So if you're building fzf from an
> environment where its git information is not available, you have to manually
> set `$FZF_VERSION` and `$FZF_REVISION`.
>
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
> [!TIP]
> To build fzf with profiling options enabled, set `TAGS=pprof`
>
> ```sh
> TAGS=pprof make clean install
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
> ```
Third-party libraries used
--------------------------

View File

@@ -1,6 +1,306 @@
CHANGELOG
=========
0.55.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
- Added `exact-boundary-match` type to the search syntax. When a search term is single-quoted, fzf will search for the exact occurrences of the string with both ends at word boundaries.
```sh
fzf --query "'here'" << EOF
come here
not there
EOF
```
- [bash] Fuzzy path completion is enabled for all commands
- 1. If the default completion is not already set
- 2. And if the current bash supports `complete -D` option
- However, fuzzy completion for some commands can be "dynamically" disabled by the dynamic completion loader
- See the comment in `__fzf_default_completion` function for more information
- Comments are now allowed in `$FZF_DEFAULT_OPTS` and `$FZF_DEFAULT_OPTS_FILE`
```sh
export FZF_DEFAULT_OPTS='
# Layout options
--layout=reverse
--info=inline-right # Show info on the right side of the prompt line
# ...
'
```
- Hyperlinks (OSC 8) are now supported in the preview window and in the main window
```sh
printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>' | fzf --ansi
fzf --preview "printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>'"
```
- The default `--ellipsis` is now `··` instead of `..`.
- [vim] A spec can have `exit` callback that is called with the exit status of fzf
- This can be used to clean up temporary resources or restore the original state when fzf is closed without a selection
- Fixed `--tmux bottom` when the status line is not at the bottom
- Fixed extra scroll offset in multi-line mode (`--read0` or `--wrap`)
- Added fallback `ps` command for `kill` completion on Cygwin
0.54.3
------
- Fixed incompatibility of adaptive height specification and 'start:reload'
```sh
# A regression in 0.54.0 would cause this to fail
fzf --height '~100%' --bind 'start:reload:seq 10'
```
- Environment variables are now available to `$FZF_DEFAULT_COMMAND`
```sh
FZF_DEFAULT_COMMAND='echo $FZF_QUERY' fzf --query foo
```
0.54.2
------
- Fixed incorrect syntax highlighting of truncated multi-line entries
- Updated GoReleaser to 2.1.0 to simplify notarization of macOS binaries
- macOS archives will be in `tar.gz` format instead of `zip` format since we no longer notarize the zip files but binaries
- (Windows) Reverted a mintty fix in 0.54.0
- As a result, mouse may not work on mintty in fullscreen mode. However, fzf will correctly read non-ASCII input in fullscreen mode (`--no-height`).
- fzf unfortunately cannot read non-ASCII input when not in fullscreen mode on Windows. So if you need to input non-ASCII characters, add `--no-height` to your `$FZF_DEFAULT_OPTS`.
- Any help in fixing this issue will be appreciated (#3799, #3847).
0.54.1
------
- Updated [fastwalk](https://github.com/charlievieth/fastwalk) dependency for built-in directory walker
- [fastwalk: add optional sorting and improve documentation](https://github.com/charlievieth/fastwalk/pull/27)
- [fastwalk: only check if MSYSTEM is set during MSYS/MSYS2](https://github.com/charlievieth/fastwalk/pull/28)
- Thanks to @charlievieth
- Reverted ALT-C binding of fish to use `cd` instead of `builtin cd`
- `builtin cd` was introduced to work around a bug of `cd` coming from `zoxide init --cmd cd fish` where it cannot handle `--` argument.
- However, the default `cd` of fish is actually a wrapper function for supporting `cd -`, so we want to use it instead.
- See [#3928](https://github.com/junegunn/fzf/pull/3928) for more information and consider helping zoxide fix the bug.
0.54.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
- Implemented line wrap of long items
- `--wrap` option enables line wrap
- `--wrap-sign` customizes the sign for wrapped lines (default: ``)
- `toggle-wrap` action toggles line wrap
```sh
history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap' --wrap-sign $'\t↳ '
```
- fzf by default binds `CTRL-/` and `ALT-/` to `toggle-wrap`
- Updated shell integration scripts to leverage line wrap
- CTRL-R binding includes `--wrap-sign $'\t↳ '` to indent wrapped lines
- `kill **` completion uses `--wrap` to show the whole line by default
instead of showing it in the preview window
- Added `--info-command` option for customizing the info line
```sh
# Prepend the current cursor position in yellow
fzf --info-command='echo -e "\x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO 💛"'
```
- `$FZF_INFO` is set to the original info text
- ANSI color codes are supported
- Pointer and marker signs can be set to empty strings
```sh
# Minimal style
fzf --pointer '' --marker '' --prompt '' --info hidden
```
- Better cache management and improved rendering for `--tail`
- Improved `--sync` behavior
- When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete.
```sh
# fzf will not render intermediate states
(sleep 1; seq 1000000; sleep 1) |
fzf --sync --query 5 --listen --bind start:up,load:up,result:up,focus:change-header:Ready
```
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
```sh
fzf --listen --sync --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .'
```
- Added `offset-middle` action to place the current item is in the middle of the screen
- 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
# `load` event would fire and the header would be prematurely updated.
fzf --header 'Loading ...' --header-lines 1 \
--bind 'start:reload:sleep 1; ps -ef' \
--bind 'load:change-header:Loaded!'
```
- Fixed mouse support on Windows
- Fixed crash when using `--tiebreak=end` with very long items
- zsh 5.0 compatibility (thanks to @LangLangBart)
- Fixed `--walker-skip` to also skip symlinks to directories
- Fixed `result` event not fired when input stream is not complete
- New tags will have `v` prefix so that they are available on https://proxy.golang.org/
0.53.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.53.0/_
- Multi-line display
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
- fzf can now display multi-line items
```sh
# All bash functions, highlighted
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi --highlight-line
# Ripgrep multi-line output
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
```
- To disable multi-line display, use `--no-multi-line`
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
```
╻First line
┃...
╹Last line
```
- Native tmux integration
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
```sh
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
# Center, 100% width and 70% height
fzf --tmux 100%,70% --border horizontal --padding 1,2
# Left, 30% width
fzf --tmux left,30%
# Bottom, 50% height
fzf --tmux bottom,50%
```
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
- To use `--tmux` in Vim plugin:
```vim
let g:fzf_layout = { 'tmux': '100%,70%' }
```
- Added support for endless input streams
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
```sh
# Interactive filtering of a log stream
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
```
- Better Windows Support
- fzf now works on Git bash (mintty) out of the box via winpty integration
- Many fixes and improvements for Windows
- man page is now embedded in the binary; `fzf --man` to see it
- Changed the default `--scroll-off` to 3, as we think it's a better default
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
```sh
# Vim will work fine without /dev/tty redirection
ls | fzf --bind 'space:execute:vim {}' > selected
```
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
```sh
# This doesn't work as expected because --expect is not compatible with --bind
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
# This is something you can do instead
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
```
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
0.52.1
------
- Fixed a critical bug in the Windows version
- Windows users are strongly encouraged to upgrade to this version
0.52.0
------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
- Added color names for selected lines: `selected-fg`, `selected-bg`, and `selected-hl`
```sh
fzf --border --multi --info inline-right --layout reverse --marker ▏ --pointer ▌ --prompt '▌ ' \
--highlight-line --color gutter:-1,selected-bg:238,selected-fg:146,current-fg:189
```
- Added `click-header` event that is triggered when the header section is clicked. When the event is triggered, `$FZF_CLICK_HEADER_COLUMN` and `$FZF_CLICK_HEADER_LINE` are set.
```sh
fd --type f |
fzf --header $'[Files] [Directories]' --header-first \
--bind 'click-header:transform:
(( FZF_CLICK_HEADER_COLUMN <= 7 )) && echo "reload(fd --type f)"
(( FZF_CLICK_HEADER_COLUMN >= 9 )) && echo "reload(fd --type d)"
'
```
- Add `$FZF_COMPLETION_{DIR,PATH}_OPTS` for separately customizing the behavior of fuzzy completion
```sh
# Set --walker options without 'follow' not to follow symbolic links
FZF_COMPLETION_PATH_OPTS="--walker=file,dir,hidden"
FZF_COMPLETION_DIR_OPTS="--walker=dir,hidden"
```
- Fixed Windows argument escaping
- Bug fixes and improvements
- The code was heavily refactored to allow using fzf as a library in Go programs. The API is still experimental and subject to change.
- https://gist.github.com/junegunn/193990b65be48a38aac6ac49d5669170
0.51.0
------
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.
```sh
# Toggle selection to the top or to the bottom
seq 30 | fzf --multi --bind 'load:pos(10)' \
--bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \
--bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done'
```
- Added `--with-shell` option to start child processes with a custom shell command and flags
```sh
gem list | fzf --with-shell 'ruby -e' \
--preview 'pp Gem::Specification.find_by_name({1})' \
--bind 'ctrl-o:execute-silent:
spec = Gem::Specification.find_by_name({1})
[spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
system "open", _1
end
'
```
- Added `change-multi` action for dynamically changing `--multi` option
- `change-multi` - enable multi-select mode with no limit
- `change-multi(NUM)` - enable multi-select mode with a limit
- `change-multi(0)` - disable multi-select mode
- Windows improvements
- `become` action is now supported on Windows
- Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
- Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes.
- Bug fixes and improvements
0.50.0
------
- Search performance optimization. You can observe 50%+ improvement in some scenarios.
```
$ rg --line-number --no-heading --smart-case . > $DATA
$ wc < $DATA
5520118 26862362 897487793
$ hyperfine -w 1 -L bin fzf-0.49.0,fzf-7ce6452,fzf-a5447b8,fzf '{bin} --filter "///" < $DATA | head -30'
Summary
fzf --filter "///" < $DATA | head -30 ran
1.16 ± 0.03 times faster than fzf-a5447b8 --filter "///" < $DATA | head -30
1.23 ± 0.03 times faster than fzf-7ce6452 --filter "///" < $DATA | head -30
1.52 ± 0.03 times faster than fzf-0.49.0 --filter "///" < $DATA | head -30
```
- Added `jump` and `jump-cancel` events that are triggered when leaving `jump` mode
```sh
# Default behavior
fzf --bind space:jump
# Same as jump-accept action
fzf --bind space:jump,jump:accept
# Accept on jump, abort on cancel
fzf --bind space:jump,jump:accept,jump-cancel:abort
# Change header on jump-cancel
fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
```
- Added a new environment variable `$FZF_KEY` exported to the child processes. It's the name of the last key pressed.
```sh
fzf --bind 'space:jump,jump:accept,jump-cancel:transform:[[ $FZF_KEY =~ ctrl-c ]] && echo abort'
```
- fzf can be built with profiling options. See [BUILD.md](BUILD.md) for more information.
- Bug fixes
0.49.0
------
- Ingestion performance improved by around 40% (more or less depending on options)

View File

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

View File

@@ -1,20 +1,20 @@
SHELL := bash
GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
ifdef FZF_VERSION
VERSION := $(FZF_VERSION)
else
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
endif
ifeq ($(VERSION),)
$(error Not on git repository; cannot determine $$FZF_VERSION)
endif
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION
@@ -25,7 +25,7 @@ endif
ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION)
endif
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64
@@ -77,7 +77,6 @@ endif
all: target/$(BINARY)
test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \
@@ -87,6 +86,10 @@ test: $(SOURCES)
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
lint: $(SOURCES) test/test_go.rb
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
rubocop --require rubocop-minitest --require rubocop-performance
install: bin/fzf
generate:
@@ -173,15 +176,15 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
docker:
docker build -t fzf-arch .
docker run -it fzf-arch tmux
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu tmux
docker-test:
docker build -t fzf-arch .
docker run -it fzf-arch
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu
update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all generate build release test bench install clean docker docker-test update
.PHONY: all generate build release test bench lint install clean docker docker-test update

View File

@@ -289,12 +289,13 @@ The following table summarizes the available options.
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink` | funcref | Function to be called with each selected item |
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
@@ -457,12 +458,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
```
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key.
or above) by putting `--tmux` option value in `tmux` key.
```vim
" See `man fzf-tmux` for available options
" See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' }
let g:fzf_layout = { 'tmux': '90%,70%' }
else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif

242
README.md

File diff suppressed because one or more lines are too long

View File

@@ -57,7 +57,7 @@ if [[ $KITTY_WINDOW_ID ]]; then
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then
chafa -f sixel -s "$dim" "$file"
chafa -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo

View File

@@ -7,7 +7,7 @@ fail() {
exit 2
}
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
args=()
@@ -19,6 +19,9 @@ 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}")
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")
help() {
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
@@ -94,10 +97,18 @@ while [[ $# -gt 0 ]]; do
opt="$opt ${arg:0:2}$size"
elif [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))}
if [[ -n "$swap" ]]; then
opt="$opt -l $(( 100 - size ))%"
if [[ $tmux_32 = 1 ]]; then
if [[ -n "$swap" ]]; then
opt="$opt -l $(( 100 - size ))%"
else
opt="$opt -l $size%"
fi
else
opt="$opt -l $size%"
if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))"
else
opt="$opt -p $size"
fi
fi
else
if [[ -n "$swap" ]]; then
@@ -132,8 +143,10 @@ if [[ -z "$TMUX" ]]; then
exit $?
fi
# --height option is not allowed. CTRL-Z is also disabled.
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
# * --height option is not allowed
# * CTRL-Z is also disabled
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
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
@@ -185,12 +198,11 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
if [[ $tmux_version = 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
elif [[ $tmux_32 = 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
elif [[ $tmux_version = 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
exit 2

View File

@@ -306,12 +306,13 @@ The following table summarizes the available options.
`source` | string | External command to generate input to fzf (e.g. `find .` )
`source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item
`sink` | funcref | Function to be called with each selected item
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
`options` | string/list | Options to fzf
`dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+----------------------------------------------------------------------
@@ -469,11 +470,12 @@ in Neovim.
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
<
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key.
or above) by putting `--tmux` options in `tmux` key.
>
" See `man fzf-tmux` for available options
" See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' }
let g:fzf_layout = { 'tmux': '90%,70%' }
else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif

8
go.mod
View File

@@ -1,13 +1,13 @@
module github.com/junegunn/fzf
require (
github.com/charlievieth/fastwalk v1.0.3
github.com/charlievieth/fastwalk v1.0.8
github.com/gdamore/tcell/v2 v2.7.4
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/sys v0.25.0
golang.org/x/term v0.24.0
)
require (

16
go.sum
View File

@@ -1,17 +1,17 @@
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
github.com/charlievieth/fastwalk v1.0.8 h1:uaoH6cAKSk73aK7aKXqs0+bL+J3Txzd3NGH8tRXgHko=
github.com/charlievieth/fastwalk v1.0.8/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/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-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
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=

18
install
View File

@@ -2,7 +2,7 @@
set -u
version=0.49.0
version=0.55.0
auto_completion=
key_bindings=
update_config=2
@@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() {
command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf -
curl -fL $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -125,7 +125,7 @@ try_curl() {
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf -
wget -O - $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -146,7 +146,7 @@ download() {
fi
local url
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
set -o pipefail
if ! (try_curl $url || try_wget $url); then
set +o pipefail
@@ -168,8 +168,8 @@ archi=$(uname -sm)
binary_available=1
binary_error=""
case "$archi" in
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
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 ;;
@@ -265,7 +265,11 @@ fi
EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
echo "eval \"\$(fzf --$shell)\"" >> "$src"
if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else
cat >> "$src" << EOF
# Auto-completion

View File

@@ -1,4 +1,4 @@
$version="0.49.0"
$version="0.55.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
@@ -40,7 +40,7 @@ function download {
return
}
cd "$fzf_base\bin"
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
$temp=$env:TMP + "\fzf.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if ($PSVersionTable.PSVersion.Major -ge 3) {

54
main.go
View File

@@ -3,14 +3,16 @@ package main
import (
_ "embed"
"fmt"
"os"
"os/exec"
"strings"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.49"
var revision string = "devel"
var version = "0.55"
var revision = "devel"
//go:embed shell/key-bindings.bash
var bashKeyBindings []byte
@@ -27,15 +29,30 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish
var fishKeyBindings []byte
//go:embed man/man1/fzf.1
var manPage []byte
func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content)))
fmt.Println("### end: " + label + " ###")
}
func exit(code int, err error) {
if code == fzf.ExitError && err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() {
protector.Protect()
options := fzf.ParseOptions()
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash {
printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion)
@@ -51,5 +68,34 @@ func main() {
fmt.Println("fzf_key_bindings")
return
}
fzf.Run(options, version, revision)
if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
if options.Man {
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
if len(file) == 0 {
fmt.Print(string(manPage))
return
}
defer os.Remove(file)
cmd := exec.Command("man", file)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Print(string(manPage))
}
return
}
code, err := fzf.Run(options)
exit(code, err)
}

View File

@@ -21,48 +21,48 @@ 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 "Apr 2024" "fzf 0.49.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "Aug 2024" "fzf 0.55.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
fzf\-tmux - open fzf in tmux split pane
.SH SYNOPSIS
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
.SH DESCRIPTION
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
a tmux popup window. It is designed to work just like fzf except that it does
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
your scripts as the extra options will be silently ignored if you're not on
tmux.
.SH LAYOUT OPTIONS
(default layout: \fB-d 50%\fR)
(default layout: \fB\-d 50%\fR)
.SS Popup window
(requires tmux 3.2 or above)
.TP
.B "-p [WIDTH[%][,HEIGHT[%]]]"
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
.TP
.B "-w WIDTH[%]"
.B "\-w WIDTH[%]"
.TP
.B "-h WIDTH[%]"
.B "\-h WIDTH[%]"
.TP
.B "-x COL"
.B "\-x COL"
.TP
.B "-y ROW"
.B "\-y ROW"
.SS Split pane
.TP
.B "-u [height[%]]"
.B "\-u [height[%]]"
Split above (up)
.TP
.B "-d [height[%]]"
.B "\-d [height[%]]"
Split below (down)
.TP
.B "-l [width[%]]"
.B "\-l [width[%]]"
Split left
.TP
.B "-r [width[%]]"
.B "\-r [width[%]]"
Split right

File diff suppressed because it is too large Load Diff

View File

@@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction
function! s:wrap_cmds(cmds)
return map([
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
return map(['@echo off']
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
\ + ['endlocal'],
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ '<SID>enc_to_cp(v:val."\r")')
endfunction
else
@@ -84,11 +81,21 @@ else
endif
function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
let e = '"'
let slashes = 0
for c in split(a:arg, '\zs')
if c ==# '\'
let slashes += 1
elseif c ==# '"'
let e .= repeat('\', slashes + 1)
let slashes = 0
else
let slashes = 0
endif
let e .= c
endfor
let e .= repeat('\', slashes) .'"'
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
endfunction
function! fzf#shellescape(arg, ...)
@@ -191,6 +198,7 @@ function! s:compare_binary_versions(a, b)
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
endfunction
let s:min_version = '0.53.0'
let s:checked = {}
function! fzf#exec(...)
if !exists('s:exec')
@@ -218,7 +226,11 @@ function! fzf#exec(...)
let s:exec = binaries[-1]
endif
if a:0 && !has_key(s:checked, a:1)
let min_version = s:min_version
if a:0 && s:compare_versions(a:1, min_version) > 0
let min_version = a:1
endif
if !has_key(s:checked, min_version)
let fzf_version = s:get_version(s:exec)
if empty(fzf_version)
let message = printf('Failed to run "%s --version"', s:exec)
@@ -226,17 +238,17 @@ function! fzf#exec(...)
throw message
end
if s:compare_versions(fzf_version, a:1) >= 0
let s:checked[a:1] = 1
if s:compare_versions(fzf_version, min_version) >= 0
let s:checked[min_version] = 1
return s:exec
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
let s:versions = {}
unlet s:exec
redraw
call fzf#install()
return fzf#exec(a:1, 1)
return fzf#exec(min_version, 1)
else
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
endif
endif
@@ -320,7 +332,10 @@ function! s:common_sink(action, lines) abort
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
for item in a:lines
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
if has('win32unix') && item !~ '/'
let item = substitute(item, '\', '/', 'g')
end
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
let sep = s:is_win ? '\' : '/'
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
endif
@@ -480,6 +495,8 @@ function! s:extract_option(opts, name)
return opt
endfunction
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
function! fzf#run(...) abort
try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -501,19 +518,19 @@ try
endif
if has_key(dict, 'source')
let source = remove(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
let source_command = source
let prefix = '('.source.')|'
elseif type == 3
let temps.input = s:fzf_tempname()
call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else
throw 'Invalid source type'
endif
else
let source_command = ''
let prefix = ''
endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@@ -522,26 +539,23 @@ try
\ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_term = has_nvim_term || has_vim8_term
\ && !s:need_cmd_window
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
if use_term
let optstr .= ' --no-height'
let optstr .= ' --no-height --no-tmux'
elseif use_height
let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height
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])
let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term
return s:execute_term(dict, command, temps)
@@ -552,14 +566,6 @@ try
call s:callback(dict, lines)
return lines
finally
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry
endfunction
@@ -578,19 +584,21 @@ function! s:fzf_tmux(dict)
if empty(size)
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif
let size = o . ',' . a:dict[o]
break
endif
endfor
endif
return printf('LINES=%d COLUMNS=%d %s %s - --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
" Legacy fzf-tmux options
if size =~ '-'
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
end
" Using native --tmux option
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
endfunction
function! s:splittable(dict)
@@ -662,21 +670,17 @@ else
let s:launcher = function('s:xterm_launcher')
endif
function! s:exit_handler(code, command, ...)
if a:code == 130
return 0
elseif has('nvim') && a:code == 129
" When deleting the terminal buffer while fzf is still running,
" Nvim sends SIGHUP.
return 0
elseif a:code > 1
function! s:exit_handler(dict, code, command, ...)
if has_key(a:dict, 'exit')
call a:dict.exit(a:code)
endif
if a:code == 2
call s:error('Error running ' . a:command)
if !empty(a:000)
sleep
endif
return 0
endif
return 1
return a:code
endfunction
function! s:execute(dict, command, use_height, temps) abort
@@ -713,21 +717,22 @@ function! s:execute(dict, command, use_height, temps) abort
call jobstart(cmd, fzf)
return []
endif
elseif has('win32unix') && $TERM !=# 'cygwin'
elseif s:need_cmd_window
let shellscript = s:fzf_tempname()
call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let command = 'start //WAIT sh -c '.shellscript
let a:temps.shellscript = shellscript
endif
if a:use_height
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else
execute 'silent !'.command
endif
let exit_status = v:shell_error
redraw!
let lines = s:collect(a:temps)
return s:exit_handler(exit_status, command) ? lines : []
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
endfunction
function! s:execute_tmux(dict, command, temps) abort
@@ -742,7 +747,7 @@ function! s:execute_tmux(dict, command, temps) abort
let exit_status = v:shell_error
redraw!
let lines = s:collect(a:temps)
return s:exit_handler(exit_status, command) ? lines : []
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
endfunction
function! s:calc_size(max, val, dict)
@@ -908,7 +913,7 @@ function! s:execute_term(dict, command, temps) abort
endif
let lines = s:collect(self.temps)
if !s:exit_handler(a:code, self.command, 1)
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
return
endif

View File

@@ -4,12 +4,14 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.bash
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
[[ $- =~ i ]] || return 0
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
@@ -32,6 +34,14 @@
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@"
@@ -91,129 +101,87 @@ _fzf_opts_completion() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-h --help
-x --extended
-e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo
--scheme
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth
--with-nth
-s --sort
+s --no-sort
--track
--no-track
--tac
--no-tac
-i
+i
-m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse
+c --no-color
+2 --no-256
--black
--no-black
--bold
--no-bold
--layout
--reverse
--no-reverse
--cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels
-1 --select-1
+1 --no-select-1
-0 --exit-0
+0 --no-exit-0
--read0
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt
--pointer
--marker
--sync
--no-sync
--async
--no-history
--history
--history-size
--no-header
--no-header-lines
--header
--header-lines
--header-first
--no-header-first
--ellipsis
--preview
--no-preview
--preview-window
--height
--min-height
--no-height
--no-margin
--no-padding
--no-border
+i --no-ignore-case
+s --no-sort
+x --no-extended
--ansi
--bash
--bind
--border
--no-border-label
--border-label
--border-label-pos
--no-preview-label
--color
--cycle
--disabled
--ellipsis
--expect
--filepath-word
--fish
--header
--header-first
--header-lines
--height
--highlight-line
--history
--history-size
--hscroll-off
--info
--jump-labels
--keep-right
--layout
--listen
--listen-unsafe
--literal
--man
--margin
--marker
--min-height
--no-bold
--no-clear
--no-hscroll
--no-mouse
--no-scrollbar
--no-separator
--no-unicode
--padding
--pointer
--preview
--preview-label
--preview-label-pos
--no-unicode
--unicode
--margin
--padding
--preview-window
--print-query
--print0
--prompt
--read0
--reverse
--scheme
--scroll-off
--separator
--sync
--tabstop
--listen
--no-listen
--clear
--no-clear
--tac
--tiebreak
--tmux
--track
--version
--with-nth
--with-shell
--wrap
--zsh
-0 --exit-0
-1 --select-1
-d --delimiter
-e --exact
-f --filter
-h --help
-i --ignore-case
-m --multi
-n --nth
-q --query
--"
case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
@@ -296,6 +264,7 @@ _fzf_handle_dynamic_completion() {
# _completion_loader may not have updated completion for the command
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
# Update orig_complete by _fzf_orig_completion entry
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
@@ -314,18 +283,28 @@ _fzf_handle_dynamic_completion() {
}
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd
local cur base dir leftover matches trigger cmd rest
cmd="${COMP_WORDS[0]}"
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
fi
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" 2> /dev/null || return
# Try to leverage existing completion
rest=("${@:4}")
unset 'rest[${#rest[@]}-2]'
COMP_LINE=${COMP_LINE:0:${#COMP_LINE}-${#trigger}}
COMP_POINT=$((COMP_POINT-${#trigger}))
COMP_WORDS[$COMP_CWORD]=$base
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
[[ $? -ne 0 ]] &&
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
dir=
[[ $base = *"/"* ]] && dir="$base"
while true; do
@@ -335,13 +314,23 @@ __fzf_generic_path_completion() {
[[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2"
if declare -F "$1" > /dev/null; then
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if [[ ${#COMPREPLY[@]} -gt 0 ]]; then
for h in "${COMPREPLY[@]}"; do
echo "$h"
done | command sort -u | __fzf_comprun "$4" -q "$leftover"
elif declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir"
if [[ $1 =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
@@ -399,7 +388,23 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
# Try to leverage existing completion
COMP_LINE=${COMP_LINE:0:${#COMP_LINE}-${#trigger}}
COMP_POINT=$((COMP_POINT-${#trigger}))
unset 'rest[${#rest[@]}-2]'
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
[[ $? -ne 0 ]] &&
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
selected=$(
(if [[ ${#COMPREPLY[@]} -gt 0 ]]; then
for h in "${COMPREPLY[@]}"; do
echo "$h"
done
fi; cat) | command sort -u |
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")
@@ -431,9 +436,10 @@ _fzf_complete_kill() {
}
_fzf_proc_completion() {
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
_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 # For BusyBox
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin
)
}
@@ -503,19 +509,49 @@ complete -o default -F _fzf_opts_completion fzf
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
awk bat cat diff diff3
# Default path completion
__fzf_default_completion() {
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
# Dynamic completion loader has updated the completion for the command
if [[ $? -eq 124 ]]; then
# We trigger _fzf_setup_completion so that fuzzy completion for the command
# still works. However, loader can update the completion for multiple
# commands at once, and fuzzy completion will no longer work for those
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
_fzf_setup_completion path "$1"
return 124
fi
}
# Set fuzzy path completion as the default completion for all commands.
# We can't set up default completion,
# 1. if it's already set up by another script
# 2. or if the current version of bash doesn't support -D option
complete | command grep -q __fzf_default_completion ||
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
# undocumented and subject to change in the future.
#
# NOTE: Although we have default completion, we still need to set up completion
# for each command in case they already have completion set up by another script.
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
awk bat cat code diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp
svn tar unzip zip"
svn tar unzip zip"}"
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
# Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
if type _comp_load > /dev/null 2>&1; then
# _comp_load was added in bash-completion 2.12 to replace _completion_loader.
@@ -551,10 +587,21 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
done
# Variables
for cmd in $v_cmds; do
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
done
# Aliases
__fzf_defc unalias _fzf_alias_completion "-a"
# Processes
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
# ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds
unset cmd d_cmds a_cmds v_cmds
_fzf_setup_completion() {
local kind fn cmd
@@ -576,8 +623,4 @@ _fzf_setup_completion() {
done
}
# Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill
fi

View File

@@ -4,12 +4,12 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.zsh
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
# Both branches of the following `if` do the same thing -- define
@@ -75,6 +75,9 @@ fi
# This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options.
{
# The 'emulate' command should not be placed inside the interactive if check;
# placing it there fails to disable alias expansion. See #3731.
if [[ -o interactive ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
#
@@ -93,6 +96,14 @@ fi
###########################################################
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@"
@@ -146,16 +157,23 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}"
export FZF_DEFAULT_OPTS
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty
fi | while read item; do
if [[ $compgen =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read -r item; do
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
echo -n -E "${(q)item} "
done
)
matches=${matches% }
@@ -180,11 +198,11 @@ _fzf_dir_completion() {
"" "/" ""
}
_fzf_feed_fifo() (
_fzf_feed_fifo() {
command rm -f "$1"
mkfifo "$1"
cat <&0 > "$1" &
)
cat <&0 > "$1" &|
}
_fzf_complete() {
setopt localoptions ksh_arrays
@@ -217,7 +235,10 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo"
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
@@ -244,13 +265,14 @@ _fzf_complete_telnet() {
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
# The current word without the trigger is in the $prefix variable passed from the caller.
_fzf_complete_ssh() {
local tokens=(${(z)1})
local -a tokens
tokens=(${(z)1})
case ${tokens[-1]} in
-i|-F|-E)
_fzf_path_completion "$prefix" "$1"
;;
*)
local user=
local user
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
;;
@@ -276,9 +298,10 @@ _fzf_complete_unalias() {
}
_fzf_complete_kill() {
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
_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 # For BusyBox
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin
)
}
@@ -315,7 +338,7 @@ fzf-completion() {
# Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@@ -345,6 +368,7 @@ fzf-completion() {
zle -N fzf-completion
bindkey '^I' fzf-completion
fi
} always {
# Restore the original options.

View File

@@ -11,15 +11,24 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0
if [[ $- =~ i ]]; then
# Key bindings
# ------------
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_select__() {
local opts
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
while read -r item; do
printf '%q ' "$item" # escape special chars
done
@@ -37,25 +46,26 @@ fzf-file-widget() {
}
__fzf_cd__() {
local opts dir
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
local dir
dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
}
if command -v perl > /dev/null; then
__fzf_history__() {
local output opts script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE"
else
@@ -64,14 +74,13 @@ if command -v perl > /dev/null; then
}
else # awk - fallback for POSIX systems
__fzf_history__() {
local output opts script n x y z d
local output script n x y z d
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next }
@@ -82,7 +91,8 @@ else # awk - fallback for POSIX systems
set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --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
@@ -132,3 +142,5 @@ if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
fi

View File

@@ -18,6 +18,15 @@ status is-interactive; or exit 0
# ------------
function fzf_key_bindings
function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
end
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline)
@@ -27,8 +36,9 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
@@ -49,19 +59,31 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
# merge history from other sessions before searching
if test -z "$fish_private_mode"
builtin history merge
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 \) ];
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
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
else
history | eval (__fzfcmd) -q '(commandline)' | read -l result
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
end
end
@@ -76,7 +98,8 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result

View File

@@ -11,8 +11,6 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0
# Key bindings
# ------------
@@ -38,13 +36,24 @@ fi
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{
if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
local item
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do
echo -n "${(q)item} "
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
echo -n -E "${(q)item} "
done
local ret=$?
echo
@@ -57,7 +66,7 @@ __fzfcmd() {
}
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$?
zle reset-prompt
return $ret
@@ -72,7 +81,10 @@ fi
# ALT-C - cd into the selected directory
fzf-cd-widget() {
setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)"
local dir="$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
if [[ -z "$dir" ]]; then
zle redisplay
return 0
@@ -94,15 +106,24 @@ fi
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))"
local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
# Ensure the associative history array, which maps event numbers to the full
# history lines, is loaded, and that Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
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") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi
local ret=$?
if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected")
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
zle vi-fetch-history -n ${num%\*}
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
zle vi-fetch-history -n $MATCH
else # selected is a custom query, not from history
LBUFFER="$selected"
fi
@@ -114,6 +135,7 @@ zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
fi
} always {
eval $__fzf_key_bindings_options

View File

@@ -26,102 +26,106 @@ func _() {
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleTrackCurrent-46]
_ = x[actToggleHeader-47]
_ = x[actTrackCurrent-48]
_ = x[actUntrackCurrent-49]
_ = x[actDown-50]
_ = x[actUp-51]
_ = x[actPageUp-52]
_ = x[actPageDown-53]
_ = x[actPosition-54]
_ = x[actHalfPageUp-55]
_ = x[actHalfPageDown-56]
_ = x[actOffsetUp-57]
_ = x[actOffsetDown-58]
_ = x[actJump-59]
_ = x[actJumpAccept-60]
_ = x[actPrintQuery-61]
_ = x[actRefreshPreview-62]
_ = x[actReplaceQuery-63]
_ = x[actToggleSort-64]
_ = x[actShowPreview-65]
_ = x[actHidePreview-66]
_ = x[actTogglePreview-67]
_ = x[actTogglePreviewWrap-68]
_ = x[actTransform-69]
_ = x[actTransformBorderLabel-70]
_ = x[actTransformHeader-71]
_ = x[actTransformPreviewLabel-72]
_ = x[actTransformPrompt-73]
_ = x[actTransformQuery-74]
_ = x[actPreview-75]
_ = x[actChangePreview-76]
_ = x[actChangePreviewWindow-77]
_ = x[actPreviewTop-78]
_ = x[actPreviewBottom-79]
_ = x[actPreviewUp-80]
_ = x[actPreviewDown-81]
_ = x[actPreviewPageUp-82]
_ = x[actPreviewPageDown-83]
_ = x[actPreviewHalfPageUp-84]
_ = x[actPreviewHalfPageDown-85]
_ = x[actPrevHistory-86]
_ = x[actPrevSelected-87]
_ = x[actPut-88]
_ = x[actNextHistory-89]
_ = x[actNextSelected-90]
_ = x[actExecute-91]
_ = x[actExecuteSilent-92]
_ = x[actExecuteMulti-93]
_ = x[actSigStop-94]
_ = x[actFirst-95]
_ = x[actLast-96]
_ = x[actReload-97]
_ = x[actReloadSync-98]
_ = x[actDisableSearch-99]
_ = x[actEnableSearch-100]
_ = x[actSelect-101]
_ = x[actDeselect-102]
_ = x[actUnbind-103]
_ = x[actRebind-104]
_ = x[actBecome-105]
_ = x[actResponse-106]
_ = x[actShowHeader-107]
_ = x[actHideHeader-108]
_ = 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]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 640, 655, 670, 687, 694, 699, 708, 719, 730, 743, 758, 769, 782, 789, 802, 815, 832, 847, 860, 874, 888, 904, 924, 936, 959, 977, 1001, 1019, 1036, 1046, 1062, 1084, 1097, 1113, 1125, 1139, 1155, 1173, 1193, 1215, 1229, 1244, 1250, 1264, 1279, 1289, 1305, 1320, 1330, 1338, 1345, 1354, 1367, 1383, 1398, 1407, 1418, 1427, 1436, 1445, 1456, 1469, 1482}
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}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -152,7 +152,13 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite
initialCharClass = charWhite
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass
// A minor optimization that can give yet another 5% performance boost
bonusMatrix [charNumber + 1][charNumber + 1]int16
)
type charClass int
@@ -187,6 +193,27 @@ func Init(scheme string) bool {
default:
return false
}
for i := 0; i <= unicode.MaxASCII; i++ {
char := rune(i)
c := charNonWord
if char >= 'a' && char <= 'z' {
c = charLower
} else if char >= 'A' && char <= 'Z' {
c = charUpper
} else if char >= '0' && char <= '9' {
c = charNumber
} else if strings.ContainsRune(whiteChars, char) {
c = charWhite
} else if strings.ContainsRune(delimiterChars, char) {
c = charDelimiter
}
asciiCharClasses[i] = c
}
for i := 0; i <= int(charNumber); i++ {
for j := 0; j <= int(charNumber); j++ {
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
}
}
return true
}
@@ -214,21 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
return offset, make([]int32, size)
}
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
} else if strings.ContainsRune(whiteChars, char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) {
return charLower
@@ -248,7 +260,7 @@ func charClassOfNonAscii(char rune) charClass {
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return charClassOfAscii(char)
return asciiCharClasses[char]
}
return charClassOfNonAscii(char)
}
@@ -287,7 +299,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 {
return bonusBoundaryWhite
}
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
}
func normalizeRune(r rune) rune {
@@ -340,30 +352,45 @@ func isAscii(runes []rune) bool {
return true
}
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
// Can't determine
if !input.IsBytes() {
return 0
return 0, input.Length()
}
// Not possible
if !isAscii(pattern) {
return -1
return -1, -1
}
firstIdx, idx := 0, 0
firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 {
return -1
return -1, -1
}
if pidx == 0 && idx > 0 {
// Step back to find the right bonus point
firstIdx = idx - 1
}
lastIdx = idx
idx++
}
return firstIdx
// Find the last appearance of the last character of the pattern to limit the search scope
bu := b
if !caseSensitive && b >= 'a' && b <= 'z' {
bu = b - 32
}
scope := input.Bytes()[lastIdx:]
for offset := len(scope) - 1; offset > 0; offset-- {
if scope[offset] == b || scope[offset] == bu {
return firstIdx, lastIdx + offset + 1
}
}
return firstIdx, lastIdx + 1
}
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
@@ -412,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return Result{0, 0, 0}, posArray(withPos, M)
}
N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
@@ -420,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
}
// Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 {
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if minIdx < 0 {
return Result{-1, -1, 0}, nil
}
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
N = maxIdx - minIdx
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0
@@ -436,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
offset32, F := alloc32(offset32, slab, M)
// Rune array
_, T := alloc32(offset32, slab, N)
input.CopyRunes(T)
input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
for off, char := range T {
var class charClass
if char <= unicode.MaxASCII {
class = charClassOfAscii(char)
class = asciiCharClasses[char]
if !caseSensitive && class == charUpper {
char += 32
T[off] = char
}
} else {
class = charClassOfNonAscii(char)
@@ -459,28 +490,28 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if normalize {
char = normalizeRune(char)
}
T[off] = char
}
Tsub[off] = char
bonus := bonusFor(prevClass, class)
Bsub[off] = bonus
bonus := bonusMatrix[prevClass][class]
B[off] = bonus
prevClass = class
if char == pchar {
if pidx < M {
F[pidx] = int32(idx + off)
F[pidx] = int32(off)
pidx++
pchar = pattern[util.Min(pidx, M-1)]
}
lastIdx = idx + off
lastIdx = off
}
if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score
C0sub[off] = 1
H0[off] = score
C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off
maxScore, maxScorePos = score, off
if forward && bonus >= bonusBoundary {
break
}
@@ -488,24 +519,24 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false
} else {
if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
}
C0sub[off] = 0
C0[off] = 0
inGap = true
}
prevH0 = H0sub[off]
prevH0 = H0[off]
}
if pidx != M {
return Result{-1, -1, 0}, nil
}
if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
if !withPos {
return result, nil
}
pos := []int{maxScorePos}
pos := []int{minIdx + maxScorePos}
return result, &pos
}
@@ -602,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
}
if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j)
*pos = append(*pos, j+minIdx)
if i == 0 {
break
}
@@ -615,7 +646,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos
return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
}
// Implement the same sorting criteria as V2
@@ -645,7 +676,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
*pos = append(*pos, idx)
}
score += scoreMatch
bonus := bonusFor(prevClass, class)
bonus := bonusMatrix[prevClass][class]
if consecutive == 0 {
firstBonus = bonus
} else {
@@ -683,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
if len(pattern) == 0 {
return Result{0, 0, 0}, nil
}
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil
}
@@ -766,6 +798,14 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
// The solution is much cheaper since there is only one possible alignment of
// the pattern.
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
}
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
}
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 {
return Result{0, 0, 0}, nil
}
@@ -777,7 +817,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
return Result{-1, -1, 0}, nil
}
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil
}
@@ -799,10 +840,22 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
}
pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
if pchar == char {
ok := pchar == char
if ok {
if pidx_ == 0 {
bonus = bonusAt(text, index_)
}
if boundaryCheck {
ok = bonus >= bonusBoundary
if ok && pidx_ == 0 {
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
}
if ok && pidx_ == len(pattern)-1 {
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
}
}
}
if ok {
pidx++
if pidx == lenPattern {
if bonus > bestBonus {
@@ -828,7 +881,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
sidx = lenRunes - (bestPos + 1)
eidx = lenRunes - (bestPos - lenPattern + 1)
}
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
var score int
if boundaryCheck {
// Underscore boundaries should be ranked lower than the other types of boundaries
score = int(bonus)
deduct := int(bonus-bonusBoundary) + 1
if sidx > 0 && text.Get(sidx-1) == '_' {
score -= deduct + 1
deduct = 1
}
if eidx < lenRunes && text.Get(eidx) == '_' {
score -= deduct
}
// Add base score so that this can compete with other match types e.g. 'foo' | bar
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
} else {
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
}
return Result{sidx, eidx, score}, nil
}
return Result{-1, -1, 0}, nil

View File

@@ -9,6 +9,10 @@ import (
"github.com/junegunn/fzf/src/util"
)
func init() {
Init("default")
}
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
}

View File

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

View File

@@ -1,6 +1,7 @@
package fzf
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
@@ -13,22 +14,28 @@ type ansiOffset struct {
color ansiState
}
type url struct {
uri string
params string
}
type ansiState struct {
fg tui.Color
bg 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
return s.fg != -1 || s.bg != -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
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
}
func (s *ansiState) ToString() string {
@@ -60,7 +67,11 @@ func (s *ansiState) ToString() string {
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
if s.url != nil {
ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
}
return ret
}
func toAnsiString(color tui.Color, offset int) string {
@@ -98,10 +109,19 @@ func matchOperatingSystemCommand(s string) int {
if s[i] == '\x07' {
return i + 1
}
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
// ------
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
return i + 2
}
}
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
// ------------
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
return i + 1
}
return -1
}
@@ -292,7 +312,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string
i := -1
var i int
if delimiter == 0 {
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
@@ -312,7 +332,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range sbytes(s) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
@@ -328,13 +348,21 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
var state ansiState
if prevState == nil {
state = ansiState{-1, -1, 0, -1}
state = ansiState{-1, -1, 0, -1, nil}
} else {
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
}
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
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 {
params := ansiCode[4 : 4+paramsEnd]
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
state.url = &url{uri: uri, params: params}
}
}
return state
}
@@ -350,7 +378,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0
ptr := &state.fg
var delimiter byte = 0
var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int

View File

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

View File

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

View File

@@ -16,14 +16,16 @@ type ChunkList struct {
chunks []*Chunk
mutex sync.Mutex
trans ItemBuilder
cache *ChunkCache
}
// NewChunkList returns a new ChunkList
func NewChunkList(trans ItemBuilder) *ChunkList {
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
return &ChunkList{
chunks: []*Chunk{},
mutex: sync.Mutex{},
trans: trans}
trans: trans,
cache: cache}
}
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
@@ -48,7 +50,12 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 {
return 0
}
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
if len(cs) == 1 {
return cs[0].count
}
// First chunk might not be full due to --tail=N
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
}
// Push adds the item to the list
@@ -72,18 +79,56 @@ func (cl *ChunkList) Clear() {
}
// Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
cl.mutex.Lock()
changed := false
if tail > 0 && CountItems(cl.chunks) > tail {
changed = true
// Find the number of chunks to keep
numChunks := 0
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
numChunks++
left -= cl.chunks[i].count
}
// Copy the chunks to keep
ret := make([]*Chunk, numChunks)
minIndex := len(cl.chunks) - numChunks
cl.cache.retire(cl.chunks[:minIndex]...)
copy(ret, cl.chunks[minIndex:])
for left, i := tail, len(ret)-1; i >= 0; i-- {
chunk := ret[i]
if chunk.count > left {
newChunk := *chunk
newChunk.count = left
oldCount := chunk.count
for i := 0; i < left; i++ {
newChunk.items[i] = chunk.items[oldCount-left+i]
}
ret[i] = &newChunk
cl.cache.retire(chunk)
break
}
left -= chunk.count
}
cl.chunks = ret
}
ret := make([]*Chunk, len(cl.chunks))
copy(ret, cl.chunks)
// Duplicate the last chunk
// Duplicate the first and the last chunk
if cnt := len(ret); cnt > 0 {
if tail > 0 && cnt > 1 {
newChunk := *ret[0]
ret[0] = &newChunk
}
newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk
}
cl.mutex.Unlock()
return ret, CountItems(ret)
return ret, CountItems(ret), changed
}

View File

@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byLength}
cl := NewChunkList(func(item *Item, s []byte) bool {
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
item.text = util.ToChars(s)
return true
})
// Snapshot
snapshot, count := cl.Snapshot()
snapshot, count, _ := cl.Snapshot(0)
if len(snapshot) > 0 || count > 0 {
t.Error("Snapshot should be empty now")
}
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
}
// But the new snapshot should contain the added items
snapshot, count = cl.Snapshot()
snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 1 && count != 2 {
t.Error("Snapshot should not be empty now")
}
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
}
// New snapshot
snapshot, count = cl.Snapshot()
snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk")
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items:", lastChunkCount)
}
}
func TestChunkListTail(t *testing.T) {
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
item.text = util.ToChars(s)
return true
})
total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i)))
}
snapshot, count, changed := cl.Snapshot(0)
assertCount := func(expected int, shouldChange bool) {
if count != expected || CountItems(snapshot) != expected {
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
}
if changed != shouldChange {
t.Error("Unexpected change status")
}
}
assertCount(total, false)
tail := chunkSize + chunkSize/2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, false)
snapshot, count, changed = cl.Snapshot(0)
assertCount(tail, false)
tail = chunkSize / 2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
}

View File

@@ -67,9 +67,9 @@ const (
)
const (
exitCancel = -1
exitOk = 0
exitNoMatch = 1
exitError = 2
exitInterrupt = 130
ExitOk = 0
ExitNoMatch = 1
ExitError = 2
ExitBecome = 126
ExitInterrupt = 130
)

View File

@@ -2,10 +2,9 @@
package fzf
import (
"fmt"
"os"
"sync"
"time"
"unsafe"
"github.com/junegunn/fzf/src/util"
)
@@ -19,28 +18,52 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/
func ustring(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
type revision struct {
major int
minor int
}
func sbytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
func (r *revision) bumpMajor() {
r.major++
r.minor = 0
}
func (r *revision) bumpMinor() {
r.minor++
}
func (r revision) compatible(other revision) bool {
return r.major == other.major
}
// Run starts fzf
func Run(opts *Options, version string, revision string) {
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 {
return runTmux(os.Args, opts)
}
if needWinpty(opts) {
return runWinpty(os.Args, opts)
}
}
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
}
sort := opts.Sort > 0
sortCriteria = opts.Criteria
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
os.Exit(exitOk)
}
// Event channel
eventBox := util.NewEventBox()
@@ -54,28 +77,29 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil)
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars(sbytes(trimmed)), offsets
return util.ToChars(stringBytes(trimmed)), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(ustring(data), nil, nil)
return util.ToChars(sbytes(trimmed)), nil
trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(stringBytes(trimmed)), nil
}
}
}
// Chunk list
cache := NewChunkCache()
var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, ustring(data))
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
@@ -85,8 +109,8 @@ func Run(opts *Options, version string, revision string) {
return true
})
} else {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(ustring(data), 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 {
var ansiState *ansiState
if prevLineAnsiState != nil {
@@ -110,7 +134,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(sbytes(transformed))
item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
@@ -119,14 +143,36 @@ func Run(opts *Options, version string, revision string) {
})
}
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Terminal I/O
var terminal *Terminal
var err error
var initialEnv []string
initialReload := opts.extractReloadOnStart()
if opts.Filter == nil {
terminal, err = NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
if len(initialReload) > 0 {
var temps []string
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
initialEnv = terminal.environ()
defer removeFiles(temps)
}
}
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
}
// Matcher
@@ -142,14 +188,15 @@ func Run(opts *Options, version string, revision string) {
forward = true
}
}
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
inputRevision := revision{}
snapshotRevision := revision{}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode
if opts.Filter != nil {
@@ -163,23 +210,27 @@ func Run(opts *Options, version string, revision string) {
found := false
if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size)
mutex := sync.Mutex{}
reader := NewReader(
func(runes []byte) bool {
item := Item{}
if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
found = true
}
mutex.Unlock()
}
return false
}, eventBox, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot()
// NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
@@ -189,9 +240,9 @@ func Run(opts *Options, version string, revision string) {
}
}
if found {
os.Exit(exitOk)
return ExitOk, nil
}
os.Exit(exitNoMatch)
return ExitNoMatch, nil
}
// Synchronous search
@@ -202,16 +253,16 @@ func Run(opts *Options, version string, revision string) {
// Go interactive
go matcher.Loop()
defer matcher.Stop()
// Terminal I/O
terminal := NewTerminal(opts, eventBox)
// Handling adaptive height
maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0
heightUnknown := opts.Height.auto
if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad()
}
deferred := opts.Select1 || opts.Exit0
deferred := opts.Select1 || opts.Exit0 || opts.Sync
go terminal.Loop()
if !deferred && !heightUnknown {
// Start right away
@@ -221,7 +272,7 @@ func Run(opts *Options, version string, revision string) {
// Event coordination
reading := true
ticks := 0
var nextCommand *string
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
total := 0
@@ -242,14 +293,17 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string, environ []string) {
restart := func(command commandSpec, environ []string) {
reading = true
chunkList.Clear()
itemIndex = 0
inputRevision++
inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ)
}
exitCode := ExitOk
stop := false
for {
delay := true
ticks++
@@ -270,7 +324,11 @@ func Run(opts *Options, version string, revision string) {
if reading {
reader.terminate()
}
os.Exit(value.(int))
quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron)
@@ -284,25 +342,25 @@ func Run(opts *Options, version string, revision string) {
useSnapshot = false
}
if !useSnapshot {
if snapshotRevision != inputRevision {
if !snapshotRevision.compatible(inputRevision) {
query = []rune{}
}
snapshot, count = chunkList.Snapshot()
var changed bool
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
}
if heightUnknown && !deferred {
determine(!reading)
}
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew:
var command *string
var command *commandSpec
var environ []string
var changed bool
switch val := value.(type) {
@@ -328,7 +386,10 @@ func Run(opts *Options, version string, revision string) {
break
}
if !useSnapshot {
newSnapshot, newCount := chunkList.Snapshot()
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
// We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed
if command == nil || newCount > 0 {
@@ -371,10 +432,11 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
}
if count > 0 {
os.Exit(exitOk)
if count == 0 {
exitCode = ExitNoMatch
}
os.Exit(exitNoMatch)
stop = true
return
}
determine(val.final)
}
@@ -385,6 +447,9 @@ func Run(opts *Options, version string, revision string) {
}
events.Clear()
})
if stop {
break
}
if delay && reading {
dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep,
@@ -392,4 +457,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur)
}
}
return exitCode, err
}

35
src/functions.go Normal file
View File

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

View File

@@ -1,6 +1,8 @@
package fzf
import (
"math"
"github.com/junegunn/fzf/src/util"
)
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
return item.text.Index
}
var minItem = Item{text: util.Chars{Index: -1}}
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
func (item *Item) TrimLength() uint16 {
return item.text.TrimLength()

View File

@@ -16,11 +16,12 @@ type MatchRequest struct {
pattern *Pattern
final bool
sort bool
revision int
revision revision
}
// Matcher is responsible for performing search
type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern
sort bool
tac bool
@@ -29,7 +30,7 @@ type Matcher struct {
partitions int
slab []*util.Slab
mergerCache map[string]*Merger
revision int
revision revision
}
const (
@@ -38,10 +39,11 @@ const (
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *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)
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
sort: sort,
tac: tac,
@@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for {
var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events {
for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) {
case MatchRequest:
request = val
@@ -71,12 +78,19 @@ func (m *Matcher) Loop() {
}
events.Clear()
})
if stop {
break
}
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)
clearChunkCache()
if !request.revision.compatible(m.revision) {
m.cache.Clear()
}
cacheCleared = true
}
// Restart search
@@ -85,20 +99,20 @@ func (m *Matcher) Loop() {
cancelled := false
count := CountItems(request.chunks)
foundCache := false
if count == prevCount {
// Look up mergerCache
if cached, found := m.mergerCache[patternString]; found {
foundCache = true
merger = cached
if !cacheCleared {
if count == prevCount {
// Look up mergerCache
if cached, found := m.mergerCache[patternString]; found {
merger = cached
}
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
}
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
}
if !foundCache {
if merger == nil {
merger, cancelled = m.scan(request)
}
@@ -150,6 +164,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return PassMerger(&request.chunks, m.tac, request.revision), false
}
minIndex := request.chunks[0].items[0].Index()
cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks)
@@ -180,7 +195,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort {
if m.sort && request.pattern.sortable {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
@@ -221,11 +236,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
}
// Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
pattern := m.patternBuilder(patternRunes)
var event util.EventType
@@ -234,5 +249,9 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else {
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
}
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}

View File

@@ -3,8 +3,8 @@ package fzf
import "fmt"
// EmptyMerger is a Merger with no data
func EmptyMerger(revision int) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision)
func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
}
// Merger holds a set of locally sorted lists of items and provides the view of
@@ -20,19 +20,25 @@ type Merger struct {
final bool
count int
pass bool
revision int
revision revision
minIndex int32
}
// PassMerger returns a new Merger that simply returns the items in the
// original order
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
}
mg := Merger{
pattern: nil,
chunks: chunks,
tac: tac,
count: 0,
pass: true,
revision: revision}
revision: revision,
minIndex: minIndex}
for _, chunk := range *mg.chunks {
mg.count += chunk.count
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
}
// NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
mg := Merger{
pattern: pattern,
lists: lists,
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
tac: tac,
final: false,
count: 0,
revision: revision}
revision: revision,
minIndex: minIndex}
for _, list := range mg.lists {
mg.count += len(list)
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
}
// Revision returns revision number
func (mg *Merger) Revision() int {
func (mg *Merger) Revision() revision {
return mg.revision
}
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1
if mg.pass {
index = int(itemIndex)
index = int(itemIndex - mg.minIndex)
if mg.tac {
index = mg.count - index - 1
}
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac {
idx = mg.count - idx - 1
}
firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count
chunk := (*mg.chunks)[idx/chunkSize+1]
return Result{item: &chunk.items[idx%chunkSize]}
}
chunk := (*mg.chunks)[idx/chunkSize]
return Result{item: &chunk.items[idx%chunkSize]}
}

View File

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

13
src/options_no_pprof.go Normal file
View File

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

73
src/options_pprof.go Normal file
View File

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

89
src/options_pprof_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
// Protect calls OS specific protections like pledge on OpenBSD
func Protect() {
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
}

158
src/proxy.go Normal file
View File

@@ -0,0 +1,158 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
const becomeSuffix = ".become"
func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}
func fifo(name string) (string, error) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
output, err := mkfifo(output, 0600)
if err != nil {
return output, err
}
return output, nil
}
func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool) (*exec.Cmd, error), opts *Options, withExports bool) (int, error) {
output, err := fifo("proxy-output")
if err != nil {
return ExitError, err
}
defer os.Remove(output)
// Take the output
go func() {
withOutputPipe(output, func(outputFile io.ReadCloser) {
if opts.Output == nil {
io.Copy(os.Stdout, outputFile)
} else {
reader := bufio.NewReader(outputFile)
sep := opts.PrintSep[0]
for {
item, err := reader.ReadString(sep)
if err != nil {
break
}
opts.Output <- item
}
}
})
}()
var command 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")
if err != nil {
return ExitError, err
}
defer os.Remove(input)
go func() {
withInputPipe(input, func(inputFile io.WriteCloser) {
if opts.Input == nil {
io.Copy(inputFile, os.Stdin)
} else {
for item := range opts.Input {
fmt.Fprint(inputFile, item+opts.PrintSep)
}
}
})
}()
if withExports {
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
} else {
// For mintty: cannot directly read named pipe from Go code
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
}
}
// 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.
var exports []string
needBash := false
if withExports {
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
for _, pairStr := range os.Environ() {
pair := strings.SplitN(pairStr, "=", 2)
if validIdentifier.MatchString(pair[0]) {
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
name := pair[0][10 : len(pair[0])-2]
exports = append(exports, name+pair[1])
exports = append(exports, "export -f "+name)
needBash = true
}
}
}
temp := WriteTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
cmd, err := cmdBuilder(temp, needBash)
if err != nil {
return ExitError, err
}
cmd.Stderr = os.Stderr
intChan := make(chan os.Signal, 1)
defer close(intChan)
go func() {
if sig, valid := <-intChan; valid {
cmd.Process.Signal(sig)
}
}()
signal.Notify(intChan, os.Interrupt)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
code := exitError.ExitCode()
if code == ExitBecome {
becomeFile := temp + becomeSuffix
data, err := os.ReadFile(becomeFile)
os.Remove(becomeFile)
if err != nil {
return ExitError, err
}
elems := strings.Split(string(data), "\x00")
if len(elems) < 1 {
return ExitError, errors.New("invalid become command")
}
command := elems[0]
env := []string{}
if len(elems) > 1 {
env = elems[1:]
}
executor := util.NewExecutor(opts.WithShell)
ttyin, err := tui.TtyIn()
if err != nil {
return ExitError, err
}
executor.Become(ttyin, env, command)
}
return code, err
}
}
return ExitOk, nil
}

41
src/proxy_unix.go Normal file
View File

@@ -0,0 +1,41 @@
//go:build !windows
package fzf
import (
"io"
"os"
"golang.org/x/sys/unix"
)
func sh(bash bool) (string, error) {
if bash {
return "bash", nil
}
return "sh", nil
}
func mkfifo(path string, mode uint32) (string, error) {
return path, unix.Mkfifo(path, mode)
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return err
}
task(outputFile)
outputFile.Close()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
if err != nil {
return err
}
task(inputFile)
inputFile.Close()
return nil
}

85
src/proxy_windows.go Normal file
View File

@@ -0,0 +1,85 @@
//go:build windows
package fzf
import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync/atomic"
)
var shPath atomic.Value
func sh(bash bool) (string, error) {
if cached := shPath.Load(); cached != nil {
return cached.(string), nil
}
name := "sh"
if bash {
name = "bash"
}
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
bytes, err := cmd.Output()
if err != nil {
return "", err
}
sh := strings.TrimSpace(string(bytes))
shPath.Store(sh)
return sh, nil
}
func mkfifo(path string, mode uint32) (string, error) {
m := strconv.FormatUint(uint64(mode), 8)
sh, err := sh(false)
if err != nil {
return path, err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
if err := cmd.Run(); err != nil {
return path, err
}
return path + ".lnk", nil
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
sh, err := sh(false)
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
outputFile, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(outputFile)
cmd.Wait()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
sh, err := sh(false)
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
inputFile, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(inputFile)
inputFile.Close()
cmd.Wait()
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
@@ -18,20 +19,22 @@ import (
// Reader reads from command or standard input
type Reader struct {
pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox
delimNil bool
event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
execOut io.ReadCloser
command *string
killed bool
wait bool
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@@ -78,6 +81,7 @@ func (r *Reader) terminate() {
r.mutex.Lock()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
@@ -85,24 +89,41 @@ func (r *Reader) terminate() {
r.mutex.Unlock()
}
func (r *Reader) restart(command string, environ []string) {
func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(command, environ)
success := r.readFromCommand(command.command, environ)
r.fin(success)
removeFiles(command.tempFiles)
}
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string) {
r.startEventPoller()
var success bool
if util.IsTty() {
if inputChan != nil {
success = r.readChannel(inputChan)
} else if len(initCmd) > 0 {
success = r.readFromCommand(initCmd, initEnv)
} else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores)
} else {
// We can't export FZF_* environment variables to the default command
success = r.readFromCommand(cmd, nil)
success = r.readFromCommand(cmd, initEnv)
}
} else {
success = r.readFromStdin()
@@ -147,7 +168,7 @@ func (r *Reader) feed(src io.Reader) {
}
// We're not making any progress after 100 tries. Stop.
if n == 0 && err == nil {
if n == 0 {
break
}
@@ -173,6 +194,12 @@ func (r *Reader) feed(src io.Reader) {
}
} else {
// Could not find the delimiter in the buffer
// NOTE: We can further optimize this by keeping track of the cursor
// position in the slab so that a straddling item that doesn't go
// beyond the boundary of a slab doesn't need to be copied to
// another buffer. However, the performance gain is negligible in
// practice (< 0.1%) and is not
// worth the added complexity.
leftover = append(leftover, buf...)
break
}
@@ -197,17 +224,46 @@ func (r *Reader) readFromStdin() bool {
return true
}
func isSymlinkToDir(path string, de os.DirEntry) bool {
if de.Type()&fs.ModeSymlink == 0 {
return false
}
if s, err := os.Stat(path); err == nil {
return s.IsDir()
}
return false
}
func trimPath(path string) string {
bytes := stringBytes(path)
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
bytes = bytes[2:]
}
if len(bytes) == 0 {
return "."
}
return byteString(bytes)
}
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
r.killed = false
conf := fastwalk.Config{Follow: opts.follow}
conf := fastwalk.Config{
Follow: opts.follow,
// Use forward slashes when running a Windows binary under WSL or MSYS
ToSlash: fastwalk.DefaultToSlash(),
Sort: fastwalk.SortFilesFirst,
}
fn := func(path string, de os.DirEntry, err error) error {
if err != nil {
return nil
}
path = filepath.Clean(path)
path = trimPath(path)
if path != "." {
isDir := de.IsDir()
if isDir {
if isDir || opts.follow && isSymlinkToDir(path, de) {
base := filepath.Base(path)
if !opts.hidden && base[0] == '.' {
return filepath.SkipDir
@@ -218,7 +274,7 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
}
}
}
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
@@ -236,20 +292,27 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
r.exec = util.ExecCommand(command, true)
r.exec = r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
}
out, err := r.exec.StdoutPipe()
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil {
r.exec = nil
r.mutex.Unlock()
return false
}
err = r.exec.Start()
r.mutex.Unlock()
if err != nil {
r.exec = nil
r.mutex.Unlock()
return false
}
r.feed(out)
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil
}

View File

@@ -10,9 +10,10 @@ import (
func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
exec := util.NewExecutor("")
reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true },
eb, false, true)
eb, exec, false, true)
reader.startEventPoller()

View File

@@ -15,6 +15,8 @@ type Offset [2]int32
type colorOffset struct {
offset [2]int32
color tui.ColorPair
match bool
url *url
}
type Result struct {
@@ -80,7 +82,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen)
} else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
}
}
}
@@ -109,7 +111,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
}
return offsets
}
@@ -176,8 +178,11 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
if curr != 0 && idx > start {
if curr < 0 {
color := colMatch
var url *url
if curr < -1 && theme.Colored {
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
ansi := itemColors[-curr-2]
url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch)
// hl or hl+ only sets the foreground color, so colMatch is the
// combination of either [hl and bg] or [hl+ and bg+].
//
@@ -193,12 +198,14 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color})
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else {
ansi := itemColors[curr-1]
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase)})
color: ansiToColorPair(ansi, colBase),
match: false,
url: ansi.color.url})
}
}
}

View File

@@ -124,10 +124,10 @@ func TestColorOffset(t *testing.T) {
item := Result{
item: &Item{
colors: &[]ansiOffset{
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
{[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}}}}}
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)

View File

@@ -38,9 +38,9 @@ const (
)
type httpServer struct {
apiKey []byte
actionChannel chan []*action
responseChannel chan string
apiKey []byte
actionChannel chan []*action
getHandler func(getParams) string
}
type listenAddress struct {
@@ -73,35 +73,35 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil
}
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
host := address.host
port := address.port
apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 {
return port, fmt.Errorf("FZF_API_KEY is required to allow remote access")
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
}
addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr)
if err != nil {
return port, fmt.Errorf("failed to listen on %s", addrStr)
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return port, fmt.Errorf("cannot extract port: %s", addr)
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return port, err
return nil, port, err
}
}
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
responseChannel: responseChannel,
apiKey: []byte(apiKey),
actionChannel: actionChannel,
getHandler: getHandler,
}
go func() {
@@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
return
}
continue
}
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
listener.Close()
}()
return port, nil
return listener, port, nil
}
// Here we are writing a simplistic HTTP server without using net/http
@@ -167,17 +165,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
case 0:
getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
select {
case response := <-server.responseChannel:
response := server.getHandler(parseGetParams(getMatch[1]))
if len(response) > 0 {
return good(response)
case <-time.After(channelTimeout):
go func() {
// Drain the channel
<-server.responseChannel
}()
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
@@ -217,12 +209,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
if err != nil {
return bad(err.Error())
}
if len(actions) == 0 {
return bad("no action specified")

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

60
src/tmux.go Normal file
View File

@@ -0,0 +1,60 @@
package fzf
import (
"os"
"os/exec"
"github.com/junegunn/fzf/src/tui"
)
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")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
// Get current directory
dir, err := os.Getwd()
if err != nil {
dir = "."
}
// Set tmux options for popup placement
// C Both The centre of the terminal
// R -x The right side of the terminal
// P Both The bottom left of the pane
// 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}
switch opts.Tmux.position {
case posUp:
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
case posDown:
tmuxArgs = append(tmuxArgs, "-xC", "-y9999")
case posLeft:
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
case posRight:
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
case posCenter:
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
}
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
sh, err := sh(needBash)
if err != nil {
return nil, err
}
tmuxArgs = append(tmuxArgs, sh, temp)
return exec.Command("tmux", tmuxArgs...), nil
}, opts, true)
}

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape BorderShape = BorderRounded
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
@@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}

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

@@ -0,0 +1,122 @@
// Code generated by "stringer -type=EventType"; DO NOT EDIT.
package tui
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Rune-0]
_ = x[CtrlA-1]
_ = x[CtrlB-2]
_ = x[CtrlC-3]
_ = x[CtrlD-4]
_ = x[CtrlE-5]
_ = x[CtrlF-6]
_ = x[CtrlG-7]
_ = x[CtrlH-8]
_ = x[Tab-9]
_ = x[CtrlJ-10]
_ = x[CtrlK-11]
_ = x[CtrlL-12]
_ = x[CtrlM-13]
_ = x[CtrlN-14]
_ = x[CtrlO-15]
_ = x[CtrlP-16]
_ = x[CtrlQ-17]
_ = x[CtrlR-18]
_ = x[CtrlS-19]
_ = x[CtrlT-20]
_ = x[CtrlU-21]
_ = x[CtrlV-22]
_ = x[CtrlW-23]
_ = x[CtrlX-24]
_ = x[CtrlY-25]
_ = x[CtrlZ-26]
_ = x[Esc-27]
_ = x[CtrlSpace-28]
_ = x[CtrlDelete-29]
_ = x[CtrlBackSlash-30]
_ = x[CtrlRightBracket-31]
_ = x[CtrlCaret-32]
_ = x[CtrlSlash-33]
_ = x[ShiftTab-34]
_ = x[Backspace-35]
_ = x[Delete-36]
_ = x[PageUp-37]
_ = x[PageDown-38]
_ = x[Up-39]
_ = x[Down-40]
_ = x[Left-41]
_ = x[Right-42]
_ = x[Home-43]
_ = x[End-44]
_ = x[Insert-45]
_ = x[ShiftUp-46]
_ = x[ShiftDown-47]
_ = x[ShiftLeft-48]
_ = x[ShiftRight-49]
_ = x[ShiftDelete-50]
_ = x[F1-51]
_ = x[F2-52]
_ = x[F3-53]
_ = x[F4-54]
_ = x[F5-55]
_ = x[F6-56]
_ = x[F7-57]
_ = x[F8-58]
_ = x[F9-59]
_ = x[F10-60]
_ = x[F11-61]
_ = x[F12-62]
_ = x[AltBackspace-63]
_ = x[AltUp-64]
_ = x[AltDown-65]
_ = x[AltLeft-66]
_ = x[AltRight-67]
_ = x[AltShiftUp-68]
_ = x[AltShiftDown-69]
_ = x[AltShiftLeft-70]
_ = x[AltShiftRight-71]
_ = x[Alt-72]
_ = x[CtrlAlt-73]
_ = x[Invalid-74]
_ = x[Fatal-75]
_ = x[Mouse-76]
_ = x[DoubleClick-77]
_ = x[LeftClick-78]
_ = x[RightClick-79]
_ = x[SLeftClick-80]
_ = x[SRightClick-81]
_ = x[ScrollUp-82]
_ = x[ScrollDown-83]
_ = x[SScrollUp-84]
_ = x[SScrollDown-85]
_ = x[PreviewScrollUp-86]
_ = x[PreviewScrollDown-87]
_ = x[Resize-88]
_ = x[Change-89]
_ = x[BackwardEOF-90]
_ = x[Start-91]
_ = x[Load-92]
_ = x[Focus-93]
_ = x[One-94]
_ = x[Zero-95]
_ = x[Result-96]
_ = x[Jump-97]
_ = x[JumpCancel-98]
_ = x[ClickHeader-99]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
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}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {
return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
}

View File

@@ -2,6 +2,7 @@ package tui
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
@@ -10,6 +11,7 @@ import (
"time"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
@@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -71,13 +73,14 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() {
if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset()
}
}
// Light renderer
type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme
mouse bool
forceBlack bool
@@ -85,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time
clicks [][2]int
ttyin *os.File
ttyout *os.File
buffer []byte
origState *term.State
width int
@@ -123,19 +127,25 @@ type LightWindow struct {
bg Color
}
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
out, err := openTtyOut()
if err != nil {
out = os.Stderr
}
r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
clearOnExit: clearOnExit,
ttyin: openTtyIn(),
ttyin: ttyin,
ttyout: out,
yoffset: 0,
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
return &r
return &r, nil
}
func repeat(r rune, times int) string {
@@ -153,11 +163,11 @@ func atoi(s string, defaultValue int) int {
return value
}
func (r *LightRenderer) Init() {
func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil {
errorExit(err.Error())
return err
}
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +205,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
return nil
}
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,19 +244,20 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer, false)
func (r *LightRenderer) getBytes() ([]byte, error) {
bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
}
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
return nil, errors.New("failed to read " + consoleDevice)
}
retries := 0
if c == ESC.Int() || nonblock {
if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
@@ -260,7 +272,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue
}
break
} else if c == ESC.Int() && pc != c {
} else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval
} else {
retries = 0
@@ -272,19 +284,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
return buffer
return buffer, nil
}
func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 {
r.buffer = r.getBytes()
r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
}
if len(r.buffer) == 0 {
panic("Empty buffer")
return Event{Fatal, 0, nil}
}
sz := 1
@@ -300,7 +316,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte():
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case 28:
@@ -311,11 +327,13 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
case ESC.Byte():
case Esc.Byte():
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer = r.getBytes()
if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz)
}
return ev
@@ -327,7 +345,7 @@ func (r *LightRenderer) GetChar() Event {
}
char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError {
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
sz = rsz
return Event{Rune, char, nil}
@@ -335,7 +353,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
loc := offsetRegexpBegin.FindIndex(r.buffer)
@@ -349,15 +367,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
}
alt := false
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() {
if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
r.buffer = r.buffer[1:]
alt = true
}
switch r.buffer[1] {
case ESC.Byte():
return Event{ESC, 0, nil}
case Esc.Byte():
return Event{Esc, 0, nil}
case 127:
return Event{AltBS, 0, nil}
return Event{AltBackspace, 0, nil}
case '[', 'O':
if len(r.buffer) < 3 {
return Event{Invalid, 0, nil}
@@ -386,7 +404,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
return Event{Up, 0, nil}
case 'Z':
return Event{BTab, 0, nil}
return Event{ShiftTab, 0, nil}
case 'H':
return Event{Home, 0, nil}
case 'F':
@@ -434,7 +452,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} // INS
case '3':
if r.buffer[3] == '~' {
return Event{Del, 0, nil}
return Event{Delete, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
@@ -442,16 +460,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{SDelete, 0, nil}
return Event{ShiftDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4':
return Event{End, 0, nil}
case '5':
return Event{PgUp, 0, nil}
return Event{PageUp, 0, nil}
case '6':
return Event{PgDn, 0, nil}
return Event{PageDown, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
@@ -489,16 +507,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
*sz = 6
switch r.buffer[4] {
case '1', '2', '3', '5':
case '1', '2', '3', '4', '5':
// Kitty iTerm2 WezTerm
// SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3'
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5]
if altShift {
altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
}
*sz = 7
char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if len(r.buffer) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
char = r.buffer[5]
}
switch char {
case 'A':
@@ -506,33 +537,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil}
}
if altShift {
return Event{AltSUp, 0, nil}
return Event{AltShiftUp, 0, nil}
}
return Event{SUp, 0, nil}
return Event{ShiftUp, 0, nil}
case 'B':
if alt {
return Event{AltDown, 0, nil}
}
if altShift {
return Event{AltSDown, 0, nil}
return Event{AltShiftDown, 0, nil}
}
return Event{SDown, 0, nil}
return Event{ShiftDown, 0, nil}
case 'C':
if alt {
return Event{AltRight, 0, nil}
}
if altShift {
return Event{AltSRight, 0, nil}
return Event{AltShiftRight, 0, nil}
}
return Event{SRight, 0, nil}
return Event{ShiftRight, 0, nil}
case 'D':
if alt {
return Event{AltLeft, 0, nil}
}
if altShift {
return Event{AltSLeft, 0, nil}
return Event{AltShiftLeft, 0, nil}
}
return Event{SLeft, 0, nil}
return Event{ShiftLeft, 0, nil}
}
} // r.buffer[4]
} // r.buffer[3]
@@ -725,6 +756,7 @@ func (r *LightRenderer) Close() {
r.flush()
r.closePlatform()
r.restoreTerminal()
r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
@@ -762,6 +794,9 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color
}
if !w.bg.IsDefault() && w.border.shape != BorderNone {
w.Erase()
}
w.drawBorder(false)
return w
}
@@ -808,44 +843,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
}
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
}
}
func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2
if !left || !right {
width++
}
vw := runeWidth(w.border.left)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
}
w.CPrint(color, repeat(' ', width))
if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@@ -867,7 +890,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@@ -998,19 +1024,19 @@ func (w *LightWindow) Print(text string) {
}
func cleanse(str string) string {
return strings.Replace(str, "\x1b", "", -1)
return strings.ReplaceAll(str, "\x1b", "")
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false, code)
w.csi("m")
w.csi("0m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, attr)
if hasColors {
defer w.csi("m")
defer w.csi("0m")
}
w.stderrInternal(cleanse(text), false, code)
}
@@ -1092,6 +1118,14 @@ func (w *LightWindow) setBg() string {
return "\x1b[m"
}
func (w *LightWindow) LinkBegin(uri string, params string) {
w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
}
func (w *LightWindow) LinkEnd() {
w.renderer.queued.WriteString("\x1b]8;;\x1b\\")
}
func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx)
code := w.setBg()
@@ -1107,7 +1141,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
bg = w.bg
}
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
defer w.csi("m")
defer w.csi("0m")
return w.fill(text, resetCode)
}
return w.fill(text, w.setBg())

View File

@@ -3,7 +3,7 @@
package tui
import (
"fmt"
"errors"
"os"
"os/exec"
"strings"
@@ -33,34 +33,35 @@ func (r *LightRenderer) fd() int {
return int(r.ttyin.Fd())
}
func (r *LightRenderer) initPlatform() error {
fd := r.fd()
origState, err := term.GetState(fd)
if err != nil {
return err
}
r.origState = origState
term.MakeRaw(fd)
return nil
func (r *LightRenderer) initPlatform() (err error) {
r.origState, err = term.MakeRaw(r.fd())
return err
}
func (r *LightRenderer) closePlatform() {
// NOOP
r.ttyout.Close()
}
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil
}
}
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
return nil, errors.New("failed to open " + consoleDevice)
}
return in
return in, nil
}
func openTtyIn() (*os.File, error) {
return openTty(syscall.O_RDONLY)
}
func openTtyOut() (*os.File, error) {
return openTty(syscall.O_WRONLY)
}
func (r *LightRenderer) setupTerminal() {
@@ -86,9 +87,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
var err error
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0)
bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 {
// Add anything we skipped over to the input buffer

View File

@@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@@ -91,9 +91,13 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
}
func openTtyIn() *os.File {
func openTtyIn() (*os.File, error) {
// not used
return nil
return nil, nil
}
func openTtyOut() (*os.File, error) {
return os.Stderr, nil
}
func (r *LightRenderer) setupTerminal() error {
@@ -135,7 +139,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
return -1, -1
}
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
}
func (r *LightRenderer) getch(nonblock bool) (int, bool) {

View File

@@ -4,10 +4,10 @@ package tui
import (
"os"
"regexp"
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
@@ -50,6 +50,8 @@ type TcellWindow struct {
lastY int
moveCursor bool
borderStyle BorderStyle
uri *string
params *string
}
func (w *TcellWindow) Top() int {
@@ -146,13 +148,13 @@ var (
_initialResize bool = true
)
func (r *FullscreenRenderer) initScreen() {
func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
if e != nil {
errorExit(e.Error())
return e
}
if e = s.Init(); e != nil {
errorExit(e.Error())
return e
}
if r.mouse {
s.EnableMouse()
@@ -160,16 +162,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse()
}
_screen = s
return nil
}
func (r *FullscreenRenderer) Init() {
func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "")
}
encoding.Register()
r.initScreen()
if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
func (r *FullscreenRenderer) Top() int {
@@ -320,16 +327,16 @@ func (r *FullscreenRenderer) GetChar() Event {
switch ev.Rune() {
case 0:
if ctrl {
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
case ctrl:
return keyfn('h')
case alt:
return Event{AltBS, 0, nil}
return Event{AltBackspace, 0, nil}
case none, shift:
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
}
}
case tcell.KeyCtrlI:
@@ -382,17 +389,17 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
return Event{AltBackspace, 0, nil}
}
return Event{BSpace, 0, nil}
return Event{Backspace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp:
if altShift {
return Event{AltSUp, 0, nil}
return Event{AltShiftUp, 0, nil}
}
if shift {
return Event{SUp, 0, nil}
return Event{ShiftUp, 0, nil}
}
if alt {
return Event{AltUp, 0, nil}
@@ -400,10 +407,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil}
case tcell.KeyDown:
if altShift {
return Event{AltSDown, 0, nil}
return Event{AltShiftDown, 0, nil}
}
if shift {
return Event{SDown, 0, nil}
return Event{ShiftDown, 0, nil}
}
if alt {
return Event{AltDown, 0, nil}
@@ -411,10 +418,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil}
case tcell.KeyLeft:
if altShift {
return Event{AltSLeft, 0, nil}
return Event{AltShiftLeft, 0, nil}
}
if shift {
return Event{SLeft, 0, nil}
return Event{ShiftLeft, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
@@ -422,10 +429,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil}
case tcell.KeyRight:
if altShift {
return Event{AltSRight, 0, nil}
return Event{AltShiftRight, 0, nil}
}
if shift {
return Event{SRight, 0, nil}
return Event{ShiftRight, 0, nil}
}
if alt {
return Event{AltRight, 0, nil}
@@ -442,17 +449,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlDelete, 0, nil}
}
if shift {
return Event{SDelete, 0, nil}
return Event{ShiftDelete, 0, nil}
}
return Event{Del, 0, nil}
return Event{Delete, 0, nil}
case tcell.KeyEnd:
return Event{End, 0, nil}
case tcell.KeyPgUp:
return Event{PgUp, 0, nil}
return Event{PageUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
return Event{PageDown, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
return Event{ShiftTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
@@ -498,7 +505,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc
case tcell.KeyEsc:
return Event{ESC, 0, nil}
return Event{Esc, 0, nil}
}
}
@@ -561,7 +568,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
w.drawBorder(false)
}
@@ -593,6 +604,16 @@ func (w *TcellWindow) Print(text string) {
w.printString(text, w.normal)
}
func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
if w.uri != nil {
style = style.Url(*w.uri)
if md := regexp.MustCompile(`id=([^:]+)`).FindStringSubmatch(*w.params); len(md) > 1 {
style = style.UrlId(md[1])
}
}
return style
}
func (w *TcellWindow) printString(text string, pair ColorPair) {
lx := 0
a := pair.Attr()
@@ -607,6 +628,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
}
style = w.withUrl(style)
gr := uniseg.NewGraphemes(text)
for gr.Next() {
@@ -657,6 +679,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
style = w.withUrl(style)
gr := uniseg.NewGraphemes(text)
Loop:
@@ -708,6 +731,16 @@ func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, w.normal)
}
func (w *TcellWindow) LinkBegin(uri string, params string) {
w.uri = &uri
w.params = &params
}
func (w *TcellWindow) LinkEnd() {
w.uri = nil
w.params = nil
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = w.normal.Fg()

View File

@@ -3,6 +3,7 @@
package tui
import (
"os"
"testing"
"github.com/gdamore/tcell/v2"
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
// Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) {
if util.ToTty() {
if util.IsTty(os.Stdout) {
// This test is skipped when output goes to terminal, because it causes
// some glitches:
// - output lines may not start at the beginning of a row which makes
@@ -102,22 +103,22 @@ func TestGetCharEventKey(t *testing.T) {
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,8 +127,8 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
@@ -161,11 +162,11 @@ func TestGetCharEventKey(t *testing.T) {
// section 7: Esc
// KeyEsc and KeyEscape are aliases for KeyESC
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
// section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
@@ -259,7 +260,7 @@ Quick reference
37 LeftClick
38 RightClick
39 BTab
40 BSpace
40 Backspace
41 Del
42 PgUp
43 PgDn
@@ -272,7 +273,7 @@ Quick reference
50 Insert
51 SUp
52 SDown
53 SLeft
53 ShiftLeft
54 SRight
55 F1
56 F2
@@ -288,15 +289,15 @@ Quick reference
66 F12
67 Change
68 BackwardEOF
69 AltBS
69 AltBackspace
70 AltUp
71 AltDown
72 AltLeft
73 AltRight
74 AltSUp
75 AltSDown
76 AltSLeft
77 AltSRight
76 AltShiftLeft
77 AltShiftRight
78 Alt
79 CtrlAlt
..

View File

@@ -4,12 +4,19 @@ package tui
import (
"os"
"sync/atomic"
"syscall"
)
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
var tty atomic.Value
func ttyname() string {
if cached := tty.Load(); cached != nil {
return cached.(string)
}
var stderr syscall.Stat_t
if syscall.Fstat(2, &stderr) != nil {
return ""
@@ -27,24 +34,21 @@ func ttyname() string {
continue
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name()
value := prefix + file.Name()
tty.Store(value)
return value
}
}
}
return ""
}
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
func TtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
}
}
return os.Stdin
}
return in
// TtyIn returns terminal device to read user input
func TtyIn() (*os.File, error) {
return openTtyIn()
}
// TtyIn returns terminal device to write to
func TtyOut() (*os.File, error) {
return openTtyOut()
}

View File

@@ -2,13 +2,20 @@
package tui
import "os"
import (
"os"
)
func ttyname() string {
return ""
}
// TtyIn on Windows returns os.Stdin
func TtyIn() *os.File {
return os.Stdin
func TtyIn() (*os.File, error) {
return os.Stdin, nil
}
// TtyIn on Windows returns nil
func TtyOut() (*os.File, error) {
return nil, nil
}

View File

@@ -1,15 +1,16 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
)
// Types of user action
//
//go:generate stringer -type=EventType
type EventType int
const (
@@ -41,7 +42,7 @@ const (
CtrlX
CtrlY
CtrlZ
ESC
Esc
CtrlSpace
CtrlDelete
@@ -51,27 +52,12 @@ const (
CtrlCaret
CtrlSlash
Invalid
Resize
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
ShiftTab
Backspace
BTab
BSpace
Del
PgUp
PgDn
Delete
PageUp
PageDown
Up
Down
@@ -81,11 +67,11 @@ const (
End
Insert
SUp
SDown
SLeft
SRight
SDelete
ShiftUp
ShiftDown
ShiftLeft
ShiftRight
ShiftDelete
F1
F2
@@ -100,6 +86,39 @@ const (
F11
F12
AltBackspace
AltUp
AltDown
AltLeft
AltRight
AltShiftUp
AltShiftDown
AltShiftLeft
AltShiftRight
Alt
CtrlAlt
Invalid
Fatal
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
// Events
Resize
Change
BackwardEOF
Start
@@ -108,21 +127,9 @@ const (
One
Zero
Result
AltBS
AltUp
AltDown
AltLeft
AltRight
AltSUp
AltSDown
AltSLeft
AltSRight
Alt
CtrlAlt
Jump
JumpCancel
ClickHeader
)
func (t EventType) AsEvent() Event {
@@ -142,6 +149,31 @@ func (e Event) Comparable() Event {
return Event{e.Type, e.Char, nil}
}
func (e Event) KeyName() string {
if e.Type >= Invalid {
return ""
}
switch e.Type {
case Rune:
return string(e.Char)
case Alt:
return "alt-" + string(e.Char)
case CtrlAlt:
return "ctrl-alt-" + string(e.Char)
case CtrlBackSlash:
return "ctrl-\\"
case CtrlRightBracket:
return "ctrl-]"
case CtrlCaret:
return "ctrl-^"
case CtrlSlash:
return "ctrl-/"
}
return util.ToKebabCase(e.Type.String())
}
func Key(r rune) Event {
return Event{Rune, r, nil}
}
@@ -271,6 +303,9 @@ type ColorTheme struct {
Disabled ColorAttr
Fg ColorAttr
Bg ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
PreviewFg ColorAttr
PreviewBg ColorAttr
DarkBg ColorAttr
@@ -282,7 +317,7 @@ type ColorTheme struct {
Spinner ColorAttr
Info ColorAttr
Cursor ColorAttr
Selected ColorAttr
Marker ColorAttr
Header ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
@@ -299,15 +334,6 @@ type Event struct {
MouseEvent *MouseEvent
}
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct {
Y int
X int
@@ -321,7 +347,8 @@ type MouseEvent struct {
type BorderShape int
const (
BorderNone BorderShape = iota
BorderUndefined BorderShape = iota
BorderNone
BorderRounded
BorderSharp
BorderBold
@@ -336,6 +363,14 @@ const (
BorderRight
)
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
}
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
@@ -484,7 +519,7 @@ type TermSize struct {
}
type Renderer interface {
Init()
Init() error
Resize(maxHeightFunc func(int) int)
Pause(clear bool)
Resume(clear bool, sigcont bool)
@@ -529,6 +564,8 @@ type Window interface {
CPrint(color ColorPair, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
LinkBegin(uri string, params string)
LinkEnd()
Erase()
EraseMaybe() bool
}
@@ -563,12 +600,14 @@ var (
ColMatch ColorPair
ColCursor ColorPair
ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair
ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair
ColInfo ColorPair
@@ -590,6 +629,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
@@ -598,7 +640,7 @@ func EmptyTheme() *ColorTheme {
Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
@@ -620,6 +662,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
@@ -628,7 +673,7 @@ func NoColorTheme() *ColorTheme {
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined},
Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
@@ -644,17 +689,15 @@ func NoColorTheme() *ColorTheme {
}
}
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() {
Default16 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined},
@@ -663,7 +706,7 @@ func init() {
Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined},
@@ -682,6 +725,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined},
@@ -690,7 +736,7 @@ func init() {
Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined},
@@ -709,6 +755,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined},
@@ -717,7 +766,7 @@ func init() {
Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined},
@@ -759,12 +808,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
@@ -790,17 +842,23 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter)
if theme.SelectedBg.Color != theme.Bg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
}
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)

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

@@ -0,0 +1,28 @@
package util
import (
"sync"
)
var atExitFuncs []func()
// AtExit registers the function fn to be called on program termination.
// The functions will be called in reverse order they were registered.
func AtExit(fn func()) {
if fn == nil {
panic("AtExit called with nil func")
}
once := &sync.Once{}
atExitFuncs = append(atExitFuncs, func() {
once.Do(fn)
})
}
// RunAtExitFuncs runs any functions registered with AtExit().
func RunAtExitFuncs() {
fns := atExitFuncs
for i := len(fns) - 1; i >= 0; i-- {
fns[i]()
}
atExitFuncs = nil
}

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

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

View File

@@ -1,6 +1,7 @@
package util
import (
"bytes"
"fmt"
"unicode"
"unicode/utf8"
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
return chars.slice
}
func (chars *Chars) NumLines(atMost int) (int, bool) {
lines := 1
if runes := chars.optionalRunes(); runes != nil {
for _, r := range runes {
if r == '\n' {
lines++
}
if lines > atMost {
return atMost, true
}
}
return lines, false
}
for idx := 0; idx < len(chars.slice); idx++ {
found := bytes.IndexByte(chars.slice[idx:], '\n')
if found < 0 {
break
}
idx += found
lines++
if lines > atMost {
return atMost, true
}
}
return lines, false
}
func (chars *Chars) optionalRunes() []rune {
if chars.inBytes {
return nil
@@ -178,12 +208,12 @@ func (chars *Chars) ToRunes() []rune {
return runes
}
func (chars *Chars) CopyRunes(dest []rune) {
func (chars *Chars) CopyRunes(dest []rune, from int) {
if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes)
copy(dest, runes[from:])
return
}
for idx, b := range chars.slice[:len(dest)] {
for idx, b := range chars.slice[from:][:len(dest)] {
dest[idx] = rune(b)
}
}
@@ -196,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
chars.slice = append([]byte(prefix), chars.slice...)
}
}
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
text := make([]rune, chars.Length())
copy(text, chars.ToRunes())
lines := [][]rune{}
overflow := false
if !multiLine {
lines = append(lines, text)
} else {
from := 0
for off := 0; off < len(text); off++ {
if text[off] == '\n' {
lines = append(lines, text[from:off+1]) // Include '\n'
from = off + 1
if len(lines) >= maxLines {
break
}
}
}
var lastLine []rune
if from < len(text) {
lastLine = text[from:]
}
overflow = false
if len(lines) >= maxLines {
overflow = true
} else {
lines = append(lines, lastLine)
}
}
// If wrapping is disabled, we're done
if wrapCols == 0 {
return lines, overflow
}
wrapped := [][]rune{}
for _, line := range lines {
// Remove trailing '\n' and remember if it was there
newline := len(line) > 0 && line[len(line)-1] == '\n'
if newline {
line = line[:len(line)-1]
}
for {
cols := wrapCols
if len(wrapped) > 0 {
cols -= wrapSignWidth
}
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
if overflowIdx >= 0 {
// Might be a wide character
if overflowIdx == 0 {
overflowIdx = 1
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line[:overflowIdx])
line = line[overflowIdx:]
continue
}
// Restore trailing '\n'
if newline {
line = append(line, '\n')
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line)
break
}
}
return wrapped, false
}

View File

@@ -1,6 +1,9 @@
package util
import "testing"
import (
"fmt"
"testing"
)
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
@@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5)
check(" ", 0)
}
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)
fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
}
}
// No wrap
check(true, 1, 0, 0, 8, 1, true)
check(true, 2, 0, 0, 8, 2, true)
check(true, 3, 0, 0, 8, 3, false)
// Wrap (2)
check(true, 4, 2, 0, 8, 4, true)
check(true, 5, 2, 0, 8, 5, true)
check(true, 6, 2, 0, 8, 6, true)
check(true, 7, 2, 0, 8, 7, true)
check(true, 8, 2, 0, 8, 8, true)
check(true, 9, 2, 0, 8, 9, false)
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
// With wrap sign (3 + 1)
check(true, 100, 3, 1, 1, 8, false)
// With wrap sign (3 + 2)
check(true, 100, 3, 2, 1, 12, false)
// With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false)
}

View File

@@ -3,6 +3,7 @@ package util
import (
"math"
"os"
"strconv"
"strings"
"time"
@@ -137,14 +138,20 @@ func DurWithin(
return val
}
// IsTty returns true if stdin is a terminal
func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd())
// IsTty returns true if the file is a terminal
func IsTty(file *os.File) bool {
fd := file.Fd()
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
}
// ToTty returns true if stdout is a terminal
func ToTty() bool {
return isatty.IsTerminal(os.Stdout.Fd())
// RunOnce runs the given function only once
func RunOnce(f func()) func() {
once := Once(true)
return func() {
if once() {
f()
}
}
}
// Once returns a function that returns the specified boolean value only once
@@ -152,7 +159,7 @@ func Once(nextResponse bool) func() bool {
state := nextResponse
return func() bool {
prevState := state
state = false
state = !nextResponse
return prevState
}
}
@@ -176,3 +183,46 @@ func RepeatToFill(str string, length int, limit int) string {
}
return output
}
// ToKebabCase converts the given CamelCase string to kebab-case
func ToKebabCase(s string) string {
name := ""
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
}
// CompareVersions compares two version strings
func CompareVersions(v1, v2 string) int {
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
}
return n
}
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
var p1, p2 int
if i < len(parts1) {
p1 = atoi(parts1[i])
}
if i < len(parts2) {
p2 = atoi(parts2[i])
}
if p1 > p2 {
return 1
} else if p1 < p2 {
return -1
}
}
return 0
}

View File

@@ -137,8 +137,11 @@ func TestOnce(t *testing.T) {
if o() {
t.Error("Expected: false")
}
if o() {
t.Error("Expected: false")
if !o() {
t.Error("Expected: true")
}
if !o() {
t.Error("Expected: true")
}
o = Once(true)
@@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
if o() {
t.Error("Expected: false")
}
if o() {
t.Error("Expected: false")
}
}
func TestRunesWidth(t *testing.T) {
@@ -203,3 +209,34 @@ func TestStringWidth(t *testing.T) {
t.Errorf("Expected: %d, Actual: %d", 1, w)
}
}
func TestCompareVersions(t *testing.T) {
assert := func(a, b string, expected int) {
if result := CompareVersions(a, b); result != expected {
t.Errorf("Expected: %d, Actual: %d", expected, result)
}
}
assert("2", "1", 1)
assert("2", "2", 0)
assert("2", "10", -1)
assert("2.1", "2.2", -1)
assert("2.1", "2.1.1", -1)
assert("1.2.3", "1.2.2", 1)
assert("1.2.3", "1.2.3", 0)
assert("1.2.3", "1.2.3.0", 0)
assert("1.2.3", "1.2.4", -1)
// Different number of parts
assert("1.0.0", "1", 0)
assert("1.0.0", "1.0", 0)
assert("1.0.0", "1.0.0", 0)
assert("1.0", "1.0.0", 0)
assert("1", "1.0.0", 0)
assert("1.0.0", "1.0.0.1", -1)
assert("1.0.0.1.0", "1.0.0.1", 0)
assert("", "3.4.5", -1)
}

View File

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

View File

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

13
src/winpty.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build !windows
package fzf
import "errors"
func needWinpty(_ *Options) bool {
return false
}
func runWinpty(_ []string, _ *Options) (int, error) {
return ExitError, errors.New("Not supported")
}

80
src/winpty_windows.go Normal file
View File

@@ -0,0 +1,80 @@
//go:build windows
package fzf
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/junegunn/fzf/src/util"
)
func isMintty345() bool {
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
}
func needWinpty(opts *Options) bool {
if os.Getenv("TERM_PROGRAM") != "mintty" {
return false
}
if isMintty345() {
/*
See: https://github.com/junegunn/fzf/issues/3809
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
*/
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
return false
}
// Setting the environment variable here unfortunately doesn't help,
// so we need to start a child process with "MSYS=enable_pcon"
// os.Setenv("MSYS", "enable_pcon")
return true
}
if opts.NoWinpty {
return false
}
if _, err := exec.LookPath("winpty"); err != nil {
return false
}
return true
}
func runWinpty(args []string, opts *Options) (int, error) {
argStr := escapeSingleQuote(args[0])
for _, arg := range args[1:] {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-winpty`
if isMintty345() {
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
sh, err := sh(needBash)
if err != nil {
return nil, err
}
cmd := exec.Command(sh, temp)
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd, nil
}, opts, false)
}
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
sh, err := sh(needBash)
if err != nil {
return nil, err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd, nil
}, opts, false)
}

File diff suppressed because it is too large Load Diff