Compare commits

...

152 Commits

Author SHA1 Message Date
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
64 changed files with 3879 additions and 1676 deletions

View File

@@ -46,6 +46,3 @@ jobs:
- name: Integration test - name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose run: make install && ./install --all && tmux new-session -d && ruby test/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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: crate-ci/typos@v1.21.0 - uses: crate-ci/typos@v1.23.1

View File

@@ -109,12 +109,12 @@ release:
owner: junegunn owner: junegunn
name: fzf name: fzf
prerelease: auto prerelease: auto
name_template: '{{ .Tag }}' name_template: '{{ .Version }}'
extra_files: extra_files:
- glob: ./dist/fzf-*darwin*.zip - glob: ./dist/fzf-*darwin*.zip
snapshot: snapshot:
name_template: "{{ .Tag }}-devel" name_template: "{{ .Version }}-devel"
changelog: changelog:
sort: asc sort: asc

View File

@@ -1,18 +1,17 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2024/01/20* * *Last update: 2024/06/24*
* *Requires fzf 0.46.0 or above* * *Requires fzf 0.54.0 or later*
--- ---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Introduction](#introduction) * [Introduction](#introduction)
* [Screen Layout](#screen-layout) * [Display modes](#display-modes)
* [`--height`](#--height) * [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux) * [`--tmux`](#--tmux)
* [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) * [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) * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling 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 This document will guide you through some examples that will familiarize you
with the advanced features of fzf. with the advanced features of fzf.
Screen Layout Display modes
------------- -------------
### `--height` ### `--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" 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 (Requires tmux 3.3 or later)
to take up the whole screen. This is done using `fzf-tmux` script.
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
```sh ```sh
# Open fzf on a tmux split pane below the current pane. # Open fzf in a tmux popup at the center of the screen with 70% width and height
# Takes the same set of options. fzf --tmux 70%
fzf-tmux --layout=reverse
``` ```
![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 `--tmux` option is silently ignored if you're not on tmux. So if you're trying
`--height` option. But the advantage of it is that it's more flexible. to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
(See `man fzf-tmux` for available options.)
```sh ```sh
# On the right (50%) # --tmux is specified later so it takes precedence over --height when on tmux.
fzf-tmux -r # If you're not on tmux, --tmux is ignored and --height is used instead.
fzf --height 70% --tmux 70%
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
``` ```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png) 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) * `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
```sh ```sh
# Open tmux in a tmux popup window (default size: 50% of the screen) # 100% width and 60% height
fzf-tmux -p fzf --tmux 100%,60% --border horizontal
# 80% width, 60% height
fzf-tmux -p 80%,60%
``` ```
![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 > You might also want to check out my tmux plugins which support this popup
> window layout. > 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 # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \ --bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \ --delimiter : \
@@ -374,11 +372,9 @@ INITIAL_QUERY="${*:-}"
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png) ![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 - Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
an empty input (`: | fzf`), then we make it start the initial Ripgrep the initial Ripgrep process immediately via `start:reload` binding for the
process immediately via `start:reload` binding. This way, fzf owns the consistency of the code.
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
the process will keep running in the background.
- Filtering is no longer a responsibility of fzf; hence `--disabled` - Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt. - `{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 - `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 # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \ --bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --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" \ --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} rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \ --bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \ --bind "ctrl-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} rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \ --bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] && --bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
@@ -529,15 +525,15 @@ Kubernetes pods.
```bash ```bash
pods() { pods() {
: | command='kubectl get pods --all-namespaces' fzf \ command='kubectl get pods --all-namespaces' fzf \
--info=inline --layout=reverse --header-lines=1 \ --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \ --prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \ --header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \ --bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \ --bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /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}) > /dev/tty' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
--preview-window up:follow \ --preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }

View File

@@ -1,6 +1,140 @@
CHANGELOG CHANGELOG
========= =========
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 0.52.0
------ ------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim) - Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)

View File

@@ -4,17 +4,17 @@ GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)
else else
VERSION := $(shell git describe --abbrev=0 2> /dev/null) VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
endif endif
ifeq ($(VERSION),) ifeq ($(VERSION),)
$(error Not on git repository; cannot determine $$FZF_VERSION) $(error Not on git repository; cannot determine $$FZF_VERSION)
endif endif
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION)) VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM)) VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION ifdef FZF_REVISION

View File

@@ -294,7 +294,7 @@ The following table summarizes the available options.
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) | | `tmux` | string | (Layout) `--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) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) | | `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
@@ -457,12 +457,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 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 ```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') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif

213
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 # 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then 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 # Add a new line character so that fzf can display multiple images in the preview window
echo echo

View File

@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
exit $? exit $?
fi fi
# --height option is not allowed. CTRL-Z is also disabled. # * --height option is not allowed
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") # * 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 # Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then

View File

@@ -311,7 +311,7 @@ The following table summarizes the available options.
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) `--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) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
@@ -469,11 +469,12 @@ in Neovim.
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } 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 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') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif

6
go.mod
View File

@@ -1,13 +1,13 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/charlievieth/fastwalk v1.0.3 github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815
github.com/gdamore/tcell/v2 v2.7.4 github.com/gdamore/tcell/v2 v2.7.4
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.20.0 golang.org/x/sys v0.22.0
golang.org/x/term v0.20.0 golang.org/x/term v0.22.0
) )
require ( require (

12
go.sum
View File

@@ -1,5 +1,5 @@
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg= github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815 h1:4PRbYm9OMgH0bcdZZqMXA/AoOvpGy4l0H6g9Au/kgGA=
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs= github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815/go.mod h1:rV19+IF9Y2TYQNy4MqEk5M/spNHjKsA0i71yrsv2p4E=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.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-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.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.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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.52.0 version=0.54.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -146,7 +146,7 @@ download() {
fi fi
local url 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 set -o pipefail
if ! (try_curl $url || try_wget $url); then if ! (try_curl $url || try_wget $url); then
set +o pipefail set +o pipefail
@@ -265,7 +265,11 @@ fi
EOF EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src" echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else else
cat >> "$src" << EOF cat >> "$src" << EOF
# Auto-completion # Auto-completion

View File

@@ -1,4 +1,4 @@
$version="0.52.0" $version="0.54.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
@@ -40,7 +40,7 @@ function download {
return return
} }
cd "$fzf_base\bin" 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" $temp=$env:TMP + "\fzf.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if ($PSVersionTable.PSVersion.Major -ge 3) { if ($PSVersionTable.PSVersion.Major -ge 3) {

23
main.go
View File

@@ -4,13 +4,14 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"os" "os"
"os/exec"
"strings" "strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.52" var version = "0.54"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
@@ -28,6 +29,9 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish //go:embed shell/key-bindings.fish
var fishKeyBindings []byte var fishKeyBindings []byte
//go:embed man/man1/fzf.1
var manPage []byte
func printScript(label string, content []byte) { func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###") fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content))) fmt.Println(strings.TrimSpace(string(content)))
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
} }
func exit(code int, err error) { func exit(code int, err error) {
if err != nil { if code == fzf.ExitError && err != nil {
fmt.Fprintln(os.Stderr, err.Error()) fmt.Fprintln(os.Stderr, err.Error())
} }
os.Exit(code) os.Exit(code)
@@ -76,6 +80,21 @@ func main() {
} }
return 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) code, err := fzf.Run(options)
exit(code, err) 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "May 2024" "fzf 0.52.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Jul 2024" "fzf 0.54.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane
.SH SYNOPSIS .SH SYNOPSIS
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] .B fzf\-tmux [LAYOUT OPTIONS] [\-\-] [FZF OPTIONS]
.SH DESCRIPTION .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 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 your scripts as the extra options will be silently ignored if you're not on
tmux. tmux.
.SH LAYOUT OPTIONS .SH LAYOUT OPTIONS
(default layout: \fB-d 50%\fR) (default layout: \fB\-d 50%\fR)
.SS Popup window .SS Popup window
(requires tmux 3.2 or above) (requires tmux 3.2 or above)
.TP .TP
.B "-p [WIDTH[%][,HEIGHT[%]]]" .B "\-p [WIDTH[%][,HEIGHT[%]]]"
.TP .TP
.B "-w WIDTH[%]" .B "\-w WIDTH[%]"
.TP .TP
.B "-h WIDTH[%]" .B "\-h WIDTH[%]"
.TP .TP
.B "-x COL" .B "\-x COL"
.TP .TP
.B "-y ROW" .B "\-y ROW"
.SS Split pane .SS Split pane
.TP .TP
.B "-u [height[%]]" .B "\-u [height[%]]"
Split above (up) Split above (up)
.TP .TP
.B "-d [height[%]]" .B "\-d [height[%]]"
Split below (down) Split below (down)
.TP .TP
.B "-l [width[%]]" .B "\-l [width[%]]"
Split left Split left
.TP .TP
.B "-r [width[%]]" .B "\-r [width[%]]"
Split right Split right

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ function! s:shellesc_cmd(arg)
let e .= c let e .= c
endfor endfor
let e .= repeat('\', slashes) .'"' let e .= repeat('\', slashes) .'"'
return e return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
endfunction endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)
@@ -327,7 +327,10 @@ function! s:common_sink(action, lines) abort
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`) " the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h') let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
for item in a:lines 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 sep = s:is_win ? '\' : '/'
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep) let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
endif endif
@@ -487,6 +490,8 @@ function! s:extract_option(opts, name)
return opt return opt
endfunction 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 function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -529,18 +534,19 @@ try
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || let use_term = has_nvim_term || has_vim8_term
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window')) \ && !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() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_term if use_term
let optstr .= ' --no-height' let optstr .= ' --no-height --no-tmux'
elseif use_height elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --no-tmux --height='.height
endif endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' " 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 optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
@@ -573,19 +579,21 @@ function! s:fzf_tmux(dict)
if empty(size) if empty(size)
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let spec = a:dict[o] let size = o . ',' . 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
break break
endif endif
endfor endfor
endif endif
" Legacy fzf-tmux options
if size =~ '-'
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) \ &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 endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -708,10 +716,10 @@ function! s:execute(dict, command, use_height, temps) abort
call jobstart(cmd, fzf) call jobstart(cmd, fzf)
return [] return []
endif endif
elseif has('win32unix') && $TERM !=# 'cygwin' elseif s:need_cmd_window
let shellscript = s:fzf_tempname() let shellscript = s:fzf_tempname()
call s:writefile([command], shellscript) 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 let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height

View File

@@ -101,75 +101,84 @@ _fzf_opts_completion() {
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-h --help
-e --exact
+x --no-extended
-q --query
-f --filter
--literal
--scheme
--expect
--disabled
--tiebreak
--bind
--color
-d --delimiter
-n --nth
--with-nth
+s --no-sort
--track
--tac
-i
+i
-m --multi
--ansi
--no-mouse
+c --no-color +c --no-color
--no-bold +i --no-ignore-case
--layout +s --no-sort
--reverse +x --no-extended
--cycle --ansi
--keep-right --bash
--no-hscroll --bind
--hscroll-off
--scroll-off
--filepath-word
--info
--separator
--no-separator
--no-scrollbar
--jump-labels
-1 --select-1
-0 --exit-0
--read0
--print0
--print-query
--prompt
--pointer
--marker
--sync
--history
--history-size
--header
--header-lines
--header-first
--ellipsis
--preview
--preview-window
--height
--min-height
--border --border
--border-label --border-label
--border-label-pos --border-label-pos
--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
--preview-label-pos --preview-label-pos
--no-unicode --preview-window
--margin --print-query
--padding --print0
--prompt
--read0
--reverse
--scheme
--scroll-off
--separator
--sync
--tabstop --tabstop
--listen --tac
--no-clear --tiebreak
--tmux
--track
--version --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 case "${prev}" in
@@ -399,7 +408,7 @@ _fzf_complete_kill() {
} }
_fzf_proc_completion() { _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,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox command ps -eo user,pid,ppid,time,args # For BusyBox
) )
@@ -476,7 +485,7 @@ d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are # NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
# undocumented and subject to change in the future. # undocumented and subject to change in the future.
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-" a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
awk bat cat diff diff3 awk bat cat code diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open sed sftp sort source tail tee uniq vi view vim wc xdg-open

View File

@@ -157,7 +157,8 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$( matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${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 unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
@@ -170,9 +171,9 @@ __fzf_generic_path_completion() {
rest=${FZF_COMPLETION_PATH_OPTS-} rest=${FZF_COMPLETION_PATH_OPTS-}
fi fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do fi | while read -r item; do
item="${item%$suffix}$suffix" item="${item%$suffix}$suffix"
echo -n "${(q)item} " echo -n -E "${(q)item} "
done done
) )
matches=${matches% } matches=${matches% }
@@ -197,11 +198,11 @@ _fzf_dir_completion() {
"" "/" "" "" "/" ""
} }
_fzf_feed_fifo() ( _fzf_feed_fifo() {
command rm -f "$1" command rm -f "$1"
mkfifo "$1" mkfifo "$1"
cat <&0 > "$1" & cat <&0 > "$1" &|
) }
_fzf_complete() { _fzf_complete() {
setopt localoptions ksh_arrays setopt localoptions ksh_arrays
@@ -264,13 +265,14 @@ _fzf_complete_telnet() {
# The first and the only argument is the LBUFFER without the current word that contains the trigger. # 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. # The current word without the trigger is in the $prefix variable passed from the caller.
_fzf_complete_ssh() { _fzf_complete_ssh() {
local tokens=(${(z)1}) local -a tokens
tokens=(${(z)1})
case ${tokens[-1]} in case ${tokens[-1]} in
-i|-F|-E) -i|-F|-E)
_fzf_path_completion "$prefix" "$1" _fzf_path_completion "$prefix" "$1"
;; ;;
*) *)
local user= local user
[[ $prefix =~ @ ]] && user="${prefix%%@*}@" [[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}') _fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
;; ;;
@@ -296,7 +298,7 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _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,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox command ps -eo user,pid,ppid,time,args # For BusyBox
) )

View File

@@ -57,15 +57,15 @@ __fzf_cd__() {
if command -v perl > /dev/null; then if command -v perl > /dev/null; then
__fzf_history__() { __fzf_history__() {
local output script local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" | last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \ 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" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE" echo "$READLINE_LINE"
else else
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )* builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )* command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \ FZF_DEFAULT_OPTS=$(__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" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}

View File

@@ -59,20 +59,31 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
# 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 is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions # history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0. # before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; 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 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 and commandline -- $result
else else
history | eval (__fzfcmd) -q '(commandline)' | read -l result 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
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end
end end
@@ -93,7 +104,7 @@ function fzf_key_bindings
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd -- $result builtin cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""

View File

@@ -52,8 +52,8 @@ __fzf_select() {
local item local item
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \ 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=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
echo -n "${(q)item} " echo -n -E "${(q)item} "
done done
local ret=$? local ret=$?
echo echo
@@ -106,16 +106,24 @@ fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | # Ensure the associative history array, which maps event numbers to the full
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \ # 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))" 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=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected") if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then zle vi-fetch-history -n $MATCH
zle vi-fetch-history -n ${num%\*}
else # selected is a custom query, not from history else # selected is a custom query, not from history
LBUFFER="$selected" LBUFFER="$selected"
fi fi

View File

@@ -58,72 +58,74 @@ func _() {
_ = x[actToggleTrack-47] _ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48] _ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49] _ = x[actToggleHeader-49]
_ = x[actTrackCurrent-50] _ = x[actToggleWrap-50]
_ = x[actUntrackCurrent-51] _ = x[actTrackCurrent-51]
_ = x[actDown-52] _ = x[actUntrackCurrent-52]
_ = x[actUp-53] _ = x[actDown-53]
_ = x[actPageUp-54] _ = x[actUp-54]
_ = x[actPageDown-55] _ = x[actPageUp-55]
_ = x[actPosition-56] _ = x[actPageDown-56]
_ = x[actHalfPageUp-57] _ = x[actPosition-57]
_ = x[actHalfPageDown-58] _ = x[actHalfPageUp-58]
_ = x[actOffsetUp-59] _ = x[actHalfPageDown-59]
_ = x[actOffsetDown-60] _ = x[actOffsetUp-60]
_ = x[actJump-61] _ = x[actOffsetDown-61]
_ = x[actJumpAccept-62] _ = x[actOffsetMiddle-62]
_ = x[actPrintQuery-63] _ = x[actJump-63]
_ = x[actRefreshPreview-64] _ = x[actJumpAccept-64]
_ = x[actReplaceQuery-65] _ = x[actPrintQuery-65]
_ = x[actToggleSort-66] _ = x[actRefreshPreview-66]
_ = x[actShowPreview-67] _ = x[actReplaceQuery-67]
_ = x[actHidePreview-68] _ = x[actToggleSort-68]
_ = x[actTogglePreview-69] _ = x[actShowPreview-69]
_ = x[actTogglePreviewWrap-70] _ = x[actHidePreview-70]
_ = x[actTransform-71] _ = x[actTogglePreview-71]
_ = x[actTransformBorderLabel-72] _ = x[actTogglePreviewWrap-72]
_ = x[actTransformHeader-73] _ = x[actTransform-73]
_ = x[actTransformPreviewLabel-74] _ = x[actTransformBorderLabel-74]
_ = x[actTransformPrompt-75] _ = x[actTransformHeader-75]
_ = x[actTransformQuery-76] _ = x[actTransformPreviewLabel-76]
_ = x[actPreview-77] _ = x[actTransformPrompt-77]
_ = x[actChangePreview-78] _ = x[actTransformQuery-78]
_ = x[actChangePreviewWindow-79] _ = x[actPreview-79]
_ = x[actPreviewTop-80] _ = x[actChangePreview-80]
_ = x[actPreviewBottom-81] _ = x[actChangePreviewWindow-81]
_ = x[actPreviewUp-82] _ = x[actPreviewTop-82]
_ = x[actPreviewDown-83] _ = x[actPreviewBottom-83]
_ = x[actPreviewPageUp-84] _ = x[actPreviewUp-84]
_ = x[actPreviewPageDown-85] _ = x[actPreviewDown-85]
_ = x[actPreviewHalfPageUp-86] _ = x[actPreviewPageUp-86]
_ = x[actPreviewHalfPageDown-87] _ = x[actPreviewPageDown-87]
_ = x[actPrevHistory-88] _ = x[actPreviewHalfPageUp-88]
_ = x[actPrevSelected-89] _ = x[actPreviewHalfPageDown-89]
_ = x[actPut-90] _ = x[actPrevHistory-90]
_ = x[actNextHistory-91] _ = x[actPrevSelected-91]
_ = x[actNextSelected-92] _ = x[actPrint-92]
_ = x[actExecute-93] _ = x[actPut-93]
_ = x[actExecuteSilent-94] _ = x[actNextHistory-94]
_ = x[actExecuteMulti-95] _ = x[actNextSelected-95]
_ = x[actSigStop-96] _ = x[actExecute-96]
_ = x[actFirst-97] _ = x[actExecuteSilent-97]
_ = x[actLast-98] _ = x[actExecuteMulti-98]
_ = x[actReload-99] _ = x[actSigStop-99]
_ = x[actReloadSync-100] _ = x[actFirst-100]
_ = x[actDisableSearch-101] _ = x[actLast-101]
_ = x[actEnableSearch-102] _ = x[actReload-102]
_ = x[actSelect-103] _ = x[actReloadSync-103]
_ = x[actDeselect-104] _ = x[actDisableSearch-104]
_ = x[actUnbind-105] _ = x[actEnableSearch-105]
_ = x[actRebind-106] _ = x[actSelect-106]
_ = x[actBecome-107] _ = x[actDeselect-107]
_ = x[actResponse-108] _ = x[actUnbind-108]
_ = x[actShowHeader-109] _ = x[actRebind-109]
_ = x[actHideHeader-110] _ = x[actBecome-110]
_ = x[actShowHeader-111]
_ = x[actHideHeader-112]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" 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, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504} 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 { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

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

View File

@@ -22,6 +22,14 @@ func (cc *ChunkCache) Clear() {
cc.mutex.Unlock() 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 // Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) { func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {

View File

@@ -16,14 +16,16 @@ type ChunkList struct {
chunks []*Chunk chunks []*Chunk
mutex sync.Mutex mutex sync.Mutex
trans ItemBuilder trans ItemBuilder
cache *ChunkCache
} }
// NewChunkList returns a new ChunkList // NewChunkList returns a new ChunkList
func NewChunkList(trans ItemBuilder) *ChunkList { func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
return &ChunkList{ return &ChunkList{
chunks: []*Chunk{}, chunks: []*Chunk{},
mutex: sync.Mutex{}, mutex: sync.Mutex{},
trans: trans} trans: trans,
cache: cache}
} }
func (c *Chunk) push(trans ItemBuilder, data []byte) bool { func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
@@ -48,7 +50,12 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 { if len(cs) == 0 {
return 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 // Push adds the item to the list
@@ -72,18 +79,56 @@ func (cl *ChunkList) Clear() {
} }
// Snapshot returns immutable snapshot of the ChunkList // 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() 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)) ret := make([]*Chunk, len(cl.chunks))
copy(ret, cl.chunks) copy(ret, cl.chunks)
// Duplicate the last chunk // Duplicate the first and the last chunk
if cnt := len(ret); cnt > 0 { if cnt := len(ret); cnt > 0 {
if tail > 0 && cnt > 1 {
newChunk := *ret[0]
ret[0] = &newChunk
}
newChunk := *ret[cnt-1] newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk ret[cnt-1] = &newChunk
} }
cl.mutex.Unlock() 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 // FIXME global
sortCriteria = []criterion{byScore, byLength} 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) item.text = util.ToChars(s)
return true return true
}) })
// Snapshot // Snapshot
snapshot, count := cl.Snapshot() snapshot, count, _ := cl.Snapshot(0)
if len(snapshot) > 0 || count > 0 { if len(snapshot) > 0 || count > 0 {
t.Error("Snapshot should be empty now") 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 // But the new snapshot should contain the added items
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 1 && count != 2 { if len(snapshot) != 1 && count != 2 {
t.Error("Snapshot should not be empty now") t.Error("Snapshot should not be empty now")
} }
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
} }
// New snapshot // New snapshot
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 3 || !snapshot[0].IsFull() || if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk") 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) 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

@@ -58,6 +58,7 @@ const (
const ( const (
EvtReadNew util.EventType = iota EvtReadNew util.EventType = iota
EvtReadFin EvtReadFin
EvtReadNone
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
@@ -67,9 +68,9 @@ const (
) )
const ( const (
ExitCancel = -1
ExitOk = 0 ExitOk = 0
ExitNoMatch = 1 ExitNoMatch = 1
ExitError = 2 ExitError = 2
ExitBecome = 126
ExitInterrupt = 130 ExitInterrupt = 130
) )

View File

@@ -2,6 +2,7 @@
package fzf package fzf
import ( import (
"os"
"sync" "sync"
"time" "time"
@@ -17,8 +18,36 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header) Matcher -> EvtHeader -> Terminal (update header)
*/ */
type revision struct {
major int
minor int
}
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 // Run starts fzf
func Run(opts *Options) (int, error) { 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 { if err := postProcessOptions(opts); err != nil {
return ExitError, err return ExitError, err
} }
@@ -63,11 +92,12 @@ func Run(opts *Options) (int, error) {
} }
// Chunk list // Chunk list
cache := NewChunkCache()
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32 var itemIndex int32
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { 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 { if len(header) < opts.HeaderLines {
header = append(header, byteString(data)) header = append(header, byteString(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
@@ -79,7 +109,7 @@ func Run(opts *Options) (int, error) {
return true return true
}) })
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
@@ -117,14 +147,21 @@ func Run(opts *Options) (int, error) {
executor := util.NewExecutor(opts.WithShell) executor := util.NewExecutor(opts.WithShell)
// Reader // Reader
reloadOnStart := opts.reloadOnStart()
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader var reader *Reader
if !streamingFilter { if !streamingFilter {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
if reloadOnStart {
// reload or reload-sync action is bound to 'start' event, no need to start the reader
eventBox.Set(EvtReadNone, nil)
} else {
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
}
// Matcher // Matcher
forward := true forward := true
@@ -139,15 +176,14 @@ func Run(opts *Options) (int, error) {
forward = true forward = true
} }
} }
cache := NewChunkCache()
patternCache := make(map[string]*Pattern) patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(cache, patternCache, return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 inputRevision := revision{}
snapshotRevision := 0 snapshotRevision := revision{}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
@@ -181,7 +217,8 @@ func Run(opts *Options) (int, error) {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot() // NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
@@ -197,7 +234,8 @@ func Run(opts *Options) (int, error) {
} }
// Synchronous search // Synchronous search
if opts.Sync { sync := opts.Sync && !reloadOnStart
if sync {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
} }
@@ -217,7 +255,7 @@ func Run(opts *Options) (int, error) {
if heightUnknown { if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad() maxFit, padHeight = terminal.MaxFitAndPad()
} }
deferred := opts.Select1 || opts.Exit0 deferred := opts.Select1 || opts.Exit0 || sync
go terminal.Loop() go terminal.Loop()
if !deferred && !heightUnknown { if !deferred && !heightUnknown {
// Start right away // Start right away
@@ -252,7 +290,7 @@ func Run(opts *Options) (int, error) {
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++ inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ) go reader.restart(command, environ)
} }
@@ -284,6 +322,9 @@ func Run(opts *Options) (int, error) {
err = quitSignal.err err = quitSignal.err
stop = true stop = true
return return
case EvtReadNone:
reading = false
terminal.UpdateCount(0, false, nil)
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron) restart(*nextCommand, nextEnviron)
@@ -297,17 +338,18 @@ func Run(opts *Options) (int, error) {
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
if snapshotRevision != inputRevision { if !snapshotRevision.compatible(inputRevision) {
query = []rune{} query = []rune{}
} }
snapshot, count = chunkList.Snapshot() var changed bool
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
}
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
} }
@@ -340,7 +382,10 @@ func Run(opts *Options) (int, error) {
break break
} }
if !useSnapshot { 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 // 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 // and the query string is changed at the same time i.e. command != nil && changed
if command == nil || newCount > 0 { if command == nil || newCount > 0 {
@@ -392,7 +437,7 @@ func Run(opts *Options) (int, error) {
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val, true) terminal.UpdateList(val)
} }
} }
} }

View File

@@ -6,8 +6,8 @@ import (
"unsafe" "unsafe"
) )
func writeTemporaryFile(data []string, printSep string) string { func WriteTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*") f, err := os.CreateTemp("", "fzf-temp-*")
if err != nil { if err != nil {
// Unable to create temporary file // Unable to create temporary file
// FIXME: Should we terminate the program? // FIXME: Should we terminate the program?

View File

@@ -1,6 +1,8 @@
package fzf package fzf
import ( import (
"math"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
return item.text.Index 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 { func (item *Item) TrimLength() uint16 {
return item.text.TrimLength() return item.text.TrimLength()

View File

@@ -16,7 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
revision int revision revision
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -30,7 +30,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int revision revision
} }
const ( const (
@@ -40,7 +40,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache, cache: cache,
@@ -82,12 +82,16 @@ func (m *Matcher) Loop() {
break break
} }
cacheCleared := false
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
if !request.revision.compatible(m.revision) {
m.cache.Clear() m.cache.Clear()
} }
cacheCleared = true
}
// Restart search // Restart search
patternString := request.pattern.AsString() patternString := request.pattern.AsString()
@@ -95,11 +99,10 @@ func (m *Matcher) Loop() {
cancelled := false cancelled := false
count := CountItems(request.chunks) count := CountItems(request.chunks)
foundCache := false if !cacheCleared {
if count == prevCount { if count == prevCount {
// Look up mergerCache // Look up mergerCache
if cached, found := m.mergerCache[patternString]; found { if cached, found := m.mergerCache[patternString]; found {
foundCache = true
merger = cached merger = cached
} }
} else { } else {
@@ -107,8 +110,9 @@ func (m *Matcher) Loop() {
prevCount = count prevCount = count
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
} }
}
if !foundCache { if merger == nil {
merger, cancelled = m.scan(request) merger, cancelled = m.scan(request)
} }
@@ -160,6 +164,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return PassMerger(&request.chunks, m.tac, request.revision), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
minIndex := request.chunks[0].items[0].Index()
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -190,7 +195,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
for _, matches := range allMatches { for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...) sliceMatches = append(sliceMatches, matches...)
} }
if m.sort { if m.sort && request.pattern.sortable {
if m.tac { if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches)) sort.Sort(ByRelevanceTac(sliceMatches))
} else { } else {
@@ -231,11 +236,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac, 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 // 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) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -244,7 +249,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
} }
func (m *Matcher) Stop() { func (m *Matcher) Stop() {

View File

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

View File

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

View File

@@ -16,14 +16,22 @@ import (
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
const Usage = `usage: fzf [options] const Usage = `fzf is an interactive filter program for any kind of list.
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
with omitted characters and still get the results you want.
Project URL: https://github.com/junegunn/fzf
Author: Junegunn Choi <junegunn.c@gmail.com>
Usage: fzf [options]
Search Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match -e, --exact Enable Exact-match
-i Case-insensitive match (default: smart-case match) -i, --ignore-case Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i, --no-ignore-case Case-sensitive match
--scheme=SCHEME Scoring scheme [default|path|history] --scheme=SCHEME Scoring scheme [default|path|history]
--literal Do not normalize latin script letters before matching --literal Do not normalize latin script letters before matching
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
@@ -33,18 +41,21 @@ const Usage = `usage: fzf [options]
field index expressions field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tail=NUM Maximum number of items to keep in memory
--track Track the current selection when the result is updated --track Track the current selection when the result is updated
--tac Reverse the order of the input --tac Reverse the order of the input
--disabled Do not perform search --disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|chunk|begin|end|index] when the scores are tied [length|chunk|begin|end|index]
(default: length) (default: length)
Interface Interface
-m, --multi[=MAX] Enable multi-select with tab/shift-tab -m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse --no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0
--keep-right Keep the right end of the line visible on overflow --keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when --scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0) scrolling to the top or to the bottom (default: 0)
@@ -63,6 +74,9 @@ const Usage = `usage: fzf [options]
according to the input size. according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent --min-height=HEIGHT Minimum height when --height is given in percent
(default: 10) (default: 10)
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
(default: center,50%)
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
@@ -76,13 +90,16 @@ const Usage = `usage: fzf [options]
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style --info=STYLE Finder info style
[default|right|hidden|inline[-right][:PREFIX]] [default|right|hidden|inline[-right][:PREFIX]]
--info-command=COMMAND Command to generate info line
--separator=STR String to form horizontal separator on info line --separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator --no-separator Hide info line separator
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
--no-scrollbar Hide scrollbar --no-scrollbar Hide scrollbar
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>') --pointer=STR Pointer to the current line (default: '▌' or '>')
--marker=STR Multi-select marker (default: '>') --marker=STR Multi-select marker (default: '┃' or '>')
--marker-multi-line=STR Multi-select marker for multi-line entries;
3 elements for top, middle, and bottom (default: '╻┃╹')
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line --header-first Print header before the prompt line
@@ -124,7 +141,6 @@ const Usage = `usage: fzf [options]
--with-shell=STR Shell command and flags to start child processes with --with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
--version Display version information and exit
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set) Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
@@ -137,6 +153,11 @@ const Usage = `usage: fzf [options]
--zsh Print script to set up Zsh shell integration --zsh Print script to set up Zsh shell integration
--fish Print script to set up Fish shell integration --fish Print script to set up Fish shell integration
Help
--version Display version information and exit
--help Show this message
--man Show man page
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline') FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
@@ -173,6 +194,7 @@ type heightSpec struct {
percent bool percent bool
auto bool auto bool
inverse bool inverse bool
index int
} }
type sizeSpec struct { type sizeSpec struct {
@@ -180,6 +202,13 @@ type sizeSpec struct {
percent bool percent bool
} }
func (s sizeSpec) String() string {
if s.percent {
return fmt.Sprintf("%d%%", int(s.size))
}
return fmt.Sprintf("%d", int(s.size))
}
func defaultMargin() [4]sizeSpec { func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{} return [4]sizeSpec{}
} }
@@ -199,8 +228,16 @@ const (
posDown posDown
posLeft posLeft
posRight posRight
posCenter
) )
type tmuxOptions struct {
width sizeSpec
height sizeSpec
position windowPosition
index int
}
type layoutType int type layoutType int
const ( const (
@@ -248,6 +285,77 @@ func (o *previewOpts) Toggle() {
o.hidden = !o.hidden o.hidden = !o.hidden
} }
func defaultTmuxOptions(index int) *tmuxOptions {
return &tmuxOptions{
position: posCenter,
width: sizeSpec{50, true},
height: sizeSpec{50, true},
index: index}
}
func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
var err error
opts := defaultTmuxOptions(index)
tokens := splitRegexp.Split(arg, -1)
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])")
if len(tokens) == 0 || len(tokens) > 3 {
return nil, errorToReturn
}
// Defaults to 'center'
switch tokens[0] {
case "top", "up":
opts.position = posUp
opts.width = sizeSpec{100, true}
case "bottom", "down":
opts.position = posDown
opts.width = sizeSpec{100, true}
case "left":
opts.position = posLeft
opts.height = sizeSpec{100, true}
case "right":
opts.position = posRight
opts.height = sizeSpec{100, true}
case "center":
default:
tokens = append([]string{"center"}, tokens...)
}
// One size given
var size1 sizeSpec
if len(tokens) > 1 {
if size1, err = parseSize(tokens[1], 100, "size"); err != nil {
return nil, errorToReturn
}
}
// Two sizes given
var size2 sizeSpec
if len(tokens) == 3 {
if size2, err = parseSize(tokens[2], 100, "size"); err != nil {
return nil, errorToReturn
}
opts.width = size1
opts.height = size2
} else if len(tokens) == 2 {
switch tokens[0] {
case "top", "up":
opts.height = size1
case "bottom", "down":
opts.height = size1
case "left":
opts.width = size1
case "right":
opts.width = size1
case "center":
opts.width = size1
opts.height = size1
}
}
return opts, nil
}
func parseLabelPosition(opts *labelOpts, arg string) error { func parseLabelPosition(opts *labelOpts, arg string) error {
opts.column = 0 opts.column = 0
opts.bottom = false opts.bottom = false
@@ -296,9 +404,14 @@ type walkerOpts struct {
type Options struct { type Options struct {
Input chan string Input chan string
Output chan string Output chan string
NoWinpty bool
Tmux *tmuxOptions
ForceTtyIn bool
ProxyScript string
Bash bool Bash bool
Zsh bool Zsh bool
Fish bool Fish bool
Man bool
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Scheme string Scheme string
@@ -312,6 +425,7 @@ type Options struct {
Sort int Sort int
Track trackOption Track trackOption
Tac bool Tac bool
Tail int
Criteria []criterion Criteria []criterion
Multi int Multi int
Ansi bool Ansi bool
@@ -323,6 +437,9 @@ type Options struct {
MinHeight int MinHeight int
Layout layoutType Layout layoutType
Cycle bool Cycle bool
Wrap bool
WrapSign *string
MultiLine bool
CursorLine bool CursorLine bool
KeepRight bool KeepRight bool
Hscroll bool Hscroll bool
@@ -331,11 +448,13 @@ type Options struct {
FileWord bool FileWord bool
InfoStyle infoStyle InfoStyle infoStyle
InfoPrefix string InfoPrefix string
InfoCommand string
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
Pointer string Pointer *string
Marker string Marker *string
MarkerMulti *[3]string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
@@ -393,10 +512,18 @@ func defaultPreviewOpts(command string) previewOpts {
} }
func defaultOptions() *Options { func defaultOptions() *Options {
var theme *tui.ColorTheme
if os.Getenv("NO_COLOR") != "" {
theme = tui.NoColorTheme()
} else {
theme = tui.EmptyTheme()
}
return &Options{ return &Options{
Bash: false, Bash: false,
Zsh: false, Zsh: false,
Fish: false, Fish: false,
Man: false,
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "default", Scheme: "default",
@@ -414,23 +541,26 @@ func defaultOptions() *Options {
Multi: 0, Multi: 0,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: tui.EmptyTheme(), Theme: theme,
Black: false, Black: false,
Bold: true, Bold: true,
MinHeight: 10, MinHeight: 10,
Layout: layoutDefault, Layout: layoutDefault,
Cycle: false, Cycle: false,
Wrap: false,
MultiLine: true,
KeepRight: false, KeepRight: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
ScrollOff: 0, ScrollOff: 3,
FileWord: false, FileWord: false,
InfoStyle: infoDefault, InfoStyle: infoDefault,
Separator: nil, Separator: nil,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Pointer: ">", Pointer: nil,
Marker: ">", Marker: nil,
MarkerMulti: nil,
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
@@ -554,7 +684,7 @@ func splitNth(str string) ([]Range, error) {
func delimiterRegexp(str string) Delimiter { func delimiterRegexp(str string) Delimiter {
// Special handling of \t // Special handling of \t
str = strings.Replace(str, "\\t", "\t", -1) str = strings.ReplaceAll(str, "\\t", "\t")
// 1. Pattern does not contain any special character // 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str { if regexp.QuoteMeta(str) == str {
@@ -1077,7 +1207,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1132,9 +1262,9 @@ Loop:
masked += strings.Repeat(" ", loc[1]) masked += strings.Repeat(" ", loc[1])
action = action[loc[1]:] action = action[loc[1]:]
} }
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
return masked return masked
} }
@@ -1241,6 +1371,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleTrackCurrent) appendAction(actToggleTrackCurrent)
case "toggle-header": case "toggle-header":
appendAction(actToggleHeader) appendAction(actToggleHeader)
case "toggle-wrap":
appendAction(actToggleWrap)
case "show-header": case "show-header":
appendAction(actShowHeader) appendAction(actShowHeader)
case "hide-header": case "hide-header":
@@ -1297,6 +1429,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actOffsetUp) appendAction(actOffsetUp)
case "offset-down": case "offset-down":
appendAction(actOffsetDown) appendAction(actOffsetDown)
case "offset-middle":
appendAction(actOffsetMiddle)
case "preview-top": case "preview-top":
appendAction(actPreviewTop) appendAction(actPreviewTop)
case "preview-bottom": case "preview-bottom":
@@ -1449,6 +1583,8 @@ func isExecuteAction(str string) actionType {
return actExecuteSilent return actExecuteSilent
case "execute-multi": case "execute-multi":
return actExecuteMulti return actExecuteMulti
case "print":
return actPrint
case "put": case "put":
return actPut return actPut
case "transform": case "transform":
@@ -1516,8 +1652,8 @@ func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
return sizeSpec{val, percent}, nil return sizeSpec{val, percent}, nil
} }
func parseHeight(str string) (heightSpec, error) { func parseHeight(str string, index int) (heightSpec, error) {
heightSpec := heightSpec{} heightSpec := heightSpec{index: index}
if strings.HasPrefix(str, "~") { if strings.HasPrefix(str, "~") {
heightSpec.auto = true heightSpec.auto = true
str = str[1:] str = str[1:]
@@ -1734,7 +1870,41 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
} }
func parseOptions(opts *Options, allArgs []string) error { func parseMarkerMultiLine(str string) (*[3]string, error) {
if str == "" {
return &[3]string{}, nil
}
gr := uniseg.NewGraphemes(str)
parts := []string{}
totalWidth := 0
for gr.Next() {
s := string(gr.Runes())
totalWidth += uniseg.StringWidth(s)
parts = append(parts, s)
}
result := [3]string{}
if totalWidth != 3 && totalWidth != 6 {
return &result, fmt.Errorf("invalid total marker width: %d (expected: 0, 3 or 6)", totalWidth)
}
expected := totalWidth / 3
idx := 0
for _, part := range parts {
expected -= uniseg.StringWidth(part)
result[idx] += part
if expected <= 0 {
idx++
expected = totalWidth / 3
}
if idx == 3 {
break
}
}
return &result, nil
}
func parseOptions(index *int, opts *Options, allArgs []string) error {
var err error var err error
var historyMax int var historyMax int
if opts.History == nil { if opts.History == nil {
@@ -1768,10 +1938,16 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.Fish = false opts.Fish = false
opts.Help = false opts.Help = false
opts.Version = false opts.Version = false
opts.Man = false
} }
startIndex := *index
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
index := i + startIndex
switch arg { switch arg {
case "--man":
clearExitingOpts()
opts.Man = true
case "--bash": case "--bash":
clearExitingOpts() clearExitingOpts()
opts.Bash = true opts.Bash = true
@@ -1787,6 +1963,29 @@ func parseOptions(opts *Options, allArgs []string) error {
case "--version": case "--version":
clearExitingOpts() clearExitingOpts()
opts.Version = true opts.Version = true
case "--no-winpty":
opts.NoWinpty = true
case "--tmux":
given, str := optionalNextString(allArgs, &i)
if given {
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
return err
}
} else {
opts.Tmux = defaultTmuxOptions(index)
}
case "--no-tmux":
opts.Tmux = nil
case "--force-tty-in":
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
// work on Neovim. Same as '-' option of fzf-tmux.
opts.ForceTtyIn = true
case "--no-force-tty-in":
opts.ForceTtyIn = false
case "--proxy-script":
if opts.ProxyScript, err = nextString(allArgs, &i, ""); err != nil {
return err
}
case "-x", "--extended": case "-x", "--extended":
opts.Extended = true opts.Extended = true
case "-e", "--exact": case "-e", "--exact":
@@ -1914,9 +2113,18 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.Tac = true opts.Tac = true
case "--no-tac": case "--no-tac":
opts.Tac = false opts.Tac = false
case "-i": case "--tail":
if opts.Tail, err = nextInt(allArgs, &i, "number of items to keep required"); err != nil {
return err
}
if opts.Tail <= 0 {
return errors.New("number of items to keep must be a positive integer")
}
case "--no-tail":
opts.Tail = 0
case "-i", "--ignore-case":
opts.Case = CaseIgnore opts.Case = CaseIgnore
case "+i": case "+i", "--no-ignore-case":
opts.Case = CaseRespect opts.Case = CaseRespect
case "-m", "--multi": case "-m", "--multi":
if opts.Multi, err = optionalNumeric(allArgs, &i, maxMulti); err != nil { if opts.Multi, err = optionalNumeric(allArgs, &i, maxMulti); err != nil {
@@ -1962,6 +2170,20 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.CursorLine = false opts.CursorLine = false
case "--no-cycle": case "--no-cycle":
opts.Cycle = false opts.Cycle = false
case "--wrap":
opts.Wrap = true
case "--no-wrap":
opts.Wrap = false
case "--wrap-sign":
str, err := nextString(allArgs, &i, "wrap sign required")
if err != nil {
return err
}
opts.WrapSign = &str
case "--multi-line":
opts.MultiLine = true
case "--no-multi-line":
opts.MultiLine = false
case "--keep-right": case "--keep-right":
opts.KeepRight = true opts.KeepRight = true
case "--no-keep-right": case "--no-keep-right":
@@ -1990,6 +2212,12 @@ func parseOptions(opts *Options, allArgs []string) error {
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil { if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil {
return err return err
} }
case "--info-command":
if opts.InfoCommand, err = nextString(allArgs, &i, "info command required"); err != nil {
return err
}
case "--no-info-command":
opts.InfoCommand = ""
case "--no-info": case "--no-info":
opts.InfoStyle = infoHidden opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
@@ -2049,17 +2277,27 @@ func parseOptions(opts *Options, allArgs []string) error {
return err return err
} }
case "--pointer": case "--pointer":
str, err := nextString(allArgs, &i, "pointer sign string required") str, err := nextString(allArgs, &i, "pointer sign required")
if err != nil { if err != nil {
return err return err
} }
opts.Pointer = firstLine(str) str = firstLine(str)
opts.Pointer = &str
case "--marker": case "--marker":
str, err := nextString(allArgs, &i, "selected sign string required") str, err := nextString(allArgs, &i, "marker sign required")
if err != nil { if err != nil {
return err return err
} }
opts.Marker = firstLine(str) str = firstLine(str)
opts.Marker = &str
case "--marker-multi-line":
str, err := nextString(allArgs, &i, "marker sign for multi-line entries required")
if err != nil {
return err
}
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(str)); err != nil {
return err
}
case "--sync": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync", "--async": case "--no-sync", "--async":
@@ -2123,7 +2361,7 @@ func parseOptions(opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
if opts.Height, err = parseHeight(str); err != nil { if opts.Height, err = parseHeight(str, index); err != nil {
return err return err
} }
case "--min-height": case "--min-height":
@@ -2264,6 +2502,10 @@ func parseOptions(opts *Options, allArgs []string) error {
if opts.FuzzyAlgo, err = parseAlgo(value); err != nil { if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--tmux="); match {
if opts.Tmux, err = parseTmuxOptions(value, index); err != nil {
return err
}
} else if match, value := optString(arg, "--scheme="); match { } else if match, value := optString(arg, "--scheme="); match {
opts.Scheme = strings.ToLower(value) opts.Scheme = strings.ToLower(value)
} else if match, value := optString(arg, "-q", "--query="); match { } else if match, value := optString(arg, "-q", "--query="); match {
@@ -2288,12 +2530,20 @@ func parseOptions(opts *Options, allArgs []string) error {
if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil { if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--wrap-sign="); match {
opts.WrapSign = &value
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match { } else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = firstLine(value) str := firstLine(value)
opts.Pointer = &str
} else if match, value := optString(arg, "--marker="); match { } else if match, value := optString(arg, "--marker="); match {
opts.Marker = firstLine(value) str := firstLine(value)
opts.Marker = &str
} else if match, value := optString(arg, "--marker-multi-line="); match {
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(value)); err != nil {
return err
}
} else if match, value := optString(arg, "-n", "--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
if opts.Nth, err = splitNth(value); err != nil { if opts.Nth, err = splitNth(value); err != nil {
return err return err
@@ -2309,7 +2559,7 @@ func parseOptions(opts *Options, allArgs []string) error {
return err return err
} }
} else if match, value := optString(arg, "--height="); match { } else if match, value := optString(arg, "--height="); match {
if opts.Height, err = parseHeight(value); err != nil { if opts.Height, err = parseHeight(value, index); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--min-height="); match { } else if match, value := optString(arg, "--min-height="); match {
@@ -2324,6 +2574,8 @@ func parseOptions(opts *Options, allArgs []string) error {
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil { if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--info-command="); match {
opts.InfoCommand = value
} else if match, value := optString(arg, "--separator="); match { } else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match { } else if match, value := optString(arg, "--scrollbar="); match {
@@ -2425,11 +2677,19 @@ func parseOptions(opts *Options, allArgs []string) error {
} else if match, value := optString(arg, "--jump-labels="); match { } else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value opts.JumpLabels = value
validateJumpLabels = true validateJumpLabels = true
} else if match, value := optString(arg, "--tail="); match {
if opts.Tail, err = atoi(value); err != nil {
return err
}
if opts.Tail <= 0 {
return errors.New("number of items to keep must be a positive integer")
}
} else { } else {
return errors.New("unknown option: " + arg) return errors.New("unknown option: " + arg)
} }
} }
} }
*index += len(allArgs)
if opts.HeaderLines < 0 { if opts.HeaderLines < 0 {
return errors.New("header lines must be a non-negative integer") return errors.New("header lines must be a non-negative integer")
@@ -2462,32 +2722,23 @@ func parseOptions(opts *Options, allArgs []string) error {
} }
func validateSign(sign string, signOptName string) error { func validateSign(sign string, signOptName string) error {
if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName)
}
if uniseg.StringWidth(sign) > 2 { if uniseg.StringWidth(sign) > 2 {
return fmt.Errorf("%v display width should be up to 2", signOptName) return fmt.Errorf("%v display width should be up to 2", signOptName)
} }
return nil return nil
} }
// This function can have side-effects and alter some global states. func validateOptions(opts *Options) error {
// So we run it on fzf.Run and not on ParseOptions. if opts.Pointer != nil {
func postProcessOptions(opts *Options) error { if err := validateSign(*opts.Pointer, "pointer"); err != nil {
if opts.Ambidouble {
uniseg.EastAsianAmbiguousWidth = 2
}
if err := validateSign(opts.Pointer, "pointer"); err != nil {
return err return err
} }
if err := validateSign(opts.Marker, "marker"); err != nil {
return err
} }
if !tui.IsLightRendererSupported() && opts.Height.size > 0 { if opts.Marker != nil {
return errors.New("--height option is currently not supported on this platform") if err := validateSign(*opts.Marker, "marker"); err != nil {
return err
}
} }
if opts.Scrollbar != nil { if opts.Scrollbar != nil {
@@ -2502,6 +2753,82 @@ func postProcessOptions(opts *Options) error {
} }
} }
if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent padding")
}
}
}
return nil
}
// This function can have side-effects and alter some global states.
// So we run it on fzf.Run and not on ParseOptions.
func postProcessOptions(opts *Options) error {
if opts.Ambidouble {
uniseg.EastAsianAmbiguousWidth = 2
}
if opts.BorderShape == tui.BorderUndefined {
opts.BorderShape = tui.BorderNone
}
if opts.Pointer == nil {
defaultPointer := "▌"
if !opts.Unicode {
defaultPointer = ">"
}
opts.Pointer = &defaultPointer
}
markerLen := 1
if opts.Marker == nil {
if opts.MarkerMulti != nil && opts.MarkerMulti[0] == "" {
empty := ""
opts.Marker = &empty
markerLen = 0
} else {
// "▎" looks better, but not all terminals render it correctly
defaultMarker := "┃"
if !opts.Unicode {
defaultMarker = ">"
}
opts.Marker = &defaultMarker
}
} else {
markerLen = uniseg.StringWidth(*opts.Marker)
}
markerMultiLen := 1
if opts.MarkerMulti == nil {
if *opts.Marker == "" {
opts.MarkerMulti = &[3]string{}
markerMultiLen = 0
} else if opts.Unicode {
opts.MarkerMulti = &[3]string{"╻", "┃", "╹"}
} else {
opts.MarkerMulti = &[3]string{".", "|", "'"}
}
} else {
markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0])
}
diff := markerMultiLen - markerLen
if diff > 0 {
padded := *opts.Marker + strings.Repeat(" ", diff)
opts.Marker = &padded
} else if diff < 0 {
for idx := range opts.MarkerMulti {
opts.MarkerMulti[idx] += strings.Repeat(" ", -diff)
}
}
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs { if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
@@ -2548,19 +2875,6 @@ func postProcessOptions(opts *Options) error {
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()] opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
} }
if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent padding")
}
}
}
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
if !opts.Extended || len(opts.Nth) == 1 { if !opts.Extended || len(opts.Nth) == 1 {
@@ -2589,12 +2903,22 @@ func postProcessOptions(opts *Options) error {
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
// If --height option is not supported on the platform, just ignore it
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
opts.Height = heightSpec{}
}
if err := opts.initProfiling(); err != nil {
return errors.New("failed to start pprof profiles: " + err.Error())
}
return processScheme(opts) return processScheme(opts)
} }
// ParseOptions parses command-line options // ParseOptions parses command-line options
func ParseOptions(useDefaults bool, args []string) (*Options, error) { func ParseOptions(useDefaults bool, args []string) (*Options, error) {
opts := defaultOptions() opts := defaultOptions()
index := 0
if useDefaults { if useDefaults {
// 1. Options from $FZF_DEFAULT_OPTS_FILE // 1. Options from $FZF_DEFAULT_OPTS_FILE
@@ -2609,7 +2933,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return nil, errors.New(path + ": " + parseErr.Error()) return nil, errors.New(path + ": " + parseErr.Error())
} }
if len(words) > 0 { if len(words) > 0 {
if err := parseOptions(opts, words); err != nil { if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New(path + ": " + err.Error()) return nil, errors.New(path + ": " + err.Error())
} }
} }
@@ -2621,20 +2945,36 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error()) return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
} }
if len(words) > 0 { if len(words) > 0 {
if err := parseOptions(opts, words); err != nil { if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error()) return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error())
} }
} }
} }
// 3. Options from command-line arguments // 3. Options from command-line arguments
if err := parseOptions(opts, args); err != nil { if err := parseOptions(&index, opts, args); err != nil {
return nil, err return nil, err
} }
if err := opts.initProfiling(); err != nil { // 4. Final validation of merged options
return nil, errors.New("failed to start pprof profiles: " + err.Error()) if err := validateOptions(opts); err != nil {
return nil, err
} }
return opts, nil return opts, nil
} }
func (opts *Options) reloadOnStart() bool {
// Not compatible with --filter
if opts.Filter != nil {
return false
}
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
for _, action := range actions {
if action.t == actReload || action.t == actReloadSync {
return true
}
}
}
return false
}

View File

@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
} }
func TestIrrelevantNth(t *testing.T) { func TestIrrelevantNth(t *testing.T) {
index := 0
{ {
opts := defaultOptions() opts := defaultOptions()
words := []string{"--nth", "..", "-x"} words := []string{"--nth", "..", "-x"}
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) 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"}} { for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
{ {
opts := defaultOptions() opts := defaultOptions()
words = append(words, "-x") words = append(words, "-x")
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 2 { if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %v", opts.Nth) t.Errorf("nth should not be empty: %v", opts.Nth)
@@ -335,10 +336,11 @@ func TestColorSpec(t *testing.T) {
} }
func TestDefaultCtrlNP(t *testing.T) { func TestDefaultCtrlNP(t *testing.T) {
index := 0
check := func(words []string, et tui.EventType, expected actionType) { check := func(words []string, et tui.EventType, expected actionType) {
e := et.AsEvent() e := et.AsEvent()
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[e][0].t != expected { if opts.Keymap[e][0].t != expected {
t.Error() t.Error()
@@ -364,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
} }
func optsFor(words ...string) *Options { func optsFor(words ...string) *Options {
index := 0
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
return opts return opts
} }
@@ -451,7 +454,6 @@ func TestValidateSign(t *testing.T) {
{"> ", true}, {"> ", true},
{"아", true}, {"아", true},
{"😀", true}, {"😀", true},
{"", false},
{">>>", false}, {">>>", false},
} }

View File

@@ -155,14 +155,14 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
} }
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1) str = strings.ReplaceAll(str, "\\ ", "\t")
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
switchSet := false switchSet := false
afterBar := false afterBar := false
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
lowerText := strings.ToLower(text) lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect || caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText caseMode == CaseSmart && text != lowerText

View File

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

146
src/proxy.go Normal file
View File

@@ -0,0 +1,146 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"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) *exec.Cmd, 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
if withExports {
exports = os.Environ()
for idx, pairStr := range exports {
pair := strings.SplitN(pairStr, "=", 2)
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
}
}
temp := WriteTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
cmd := cmdBuilder(temp)
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
}

38
src/proxy_unix.go Normal file
View File

@@ -0,0 +1,38 @@
//go:build !windows
package fzf
import (
"io"
"os"
"golang.org/x/sys/unix"
)
func sh() (string, error) {
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
}

81
src/proxy_windows.go Normal file
View File

@@ -0,0 +1,81 @@
//go:build windows
package fzf
import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync/atomic"
)
var shPath atomic.Value
func sh() (string, error) {
if cached := shPath.Load(); cached != nil {
return cached.(string), nil
}
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
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()
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()
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()
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" "bytes"
"context" "context"
"io" "io"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -25,6 +26,7 @@ type Reader struct {
finChan chan bool finChan chan bool
mutex sync.Mutex mutex sync.Mutex
exec *exec.Cmd exec *exec.Cmd
execOut io.ReadCloser
command *string command *string
killed bool killed bool
wait bool wait bool
@@ -32,7 +34,7 @@ type Reader struct {
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader { func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
} }
func (r *Reader) startEventPoller() { func (r *Reader) startEventPoller() {
@@ -79,6 +81,7 @@ func (r *Reader) terminate() {
r.mutex.Lock() r.mutex.Lock()
r.killed = true r.killed = true
if r.exec != nil && r.exec.Process != nil { if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec) util.KillCommand(r.exec)
} else { } else {
os.Stdin.Close() os.Stdin.Close()
@@ -113,7 +116,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
var success bool var success bool
if inputChan != nil { if inputChan != nil {
success = r.readChannel(inputChan) success = r.readChannel(inputChan)
} else if util.IsTty() { } else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores) success = r.readFiles(root, opts, ignores)
@@ -220,17 +223,45 @@ func (r *Reader) readFromStdin() bool {
return true 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 { func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
r.killed = false 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(),
}
fn := func(path string, de os.DirEntry, err error) error { fn := func(path string, de os.DirEntry, err error) error {
if err != nil { if err != nil {
return nil return nil
} }
path = filepath.Clean(path) path = trimPath(path)
if path != "." { if path != "." {
isDir := de.IsDir() isDir := de.IsDir()
if isDir { if isDir || opts.follow && isSymlinkToDir(path, de) {
base := filepath.Base(path) base := filepath.Base(path)
if !opts.hidden && base[0] == '.' { if !opts.hidden && base[0] == '.' {
return filepath.SkipDir return filepath.SkipDir
@@ -241,7 +272,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)) atomic.StoreInt32(&r.event, int32(EvtReadNew))
} }
} }
@@ -263,16 +294,23 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
if environ != nil { if environ != nil {
r.exec.Env = environ r.exec.Env = environ
} }
out, err := r.exec.StdoutPipe()
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock() r.mutex.Unlock()
return false return false
} }
err = r.exec.Start() err = r.exec.Start()
r.mutex.Unlock()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock()
return false return false
} }
r.feed(out)
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil return r.exec.Wait() == nil
} }

View File

@@ -15,6 +15,7 @@ type Offset [2]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
color tui.ColorPair color tui.ColorPair
match bool
} }
type Result struct { type Result struct {
@@ -80,7 +81,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1)) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
} }
} }
} }
@@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
if len(itemColors) == 0 { if len(itemColors) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range matchOffsets { 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 return offsets
} }
@@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} }
} }
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color}) offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
} else { } else {
ansi := itemColors[curr-1] ansi := itemColors[curr-1]
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase)}) color: ansiToColorPair(ansi, colBase),
match: false})
} }
} }
} }

View File

@@ -40,7 +40,7 @@ const (
type httpServer struct { type httpServer struct {
apiKey []byte apiKey []byte
actionChannel chan []*action actionChannel chan []*action
responseChannel chan string getHandler func(getParams) string
} }
type listenAddress struct { type listenAddress struct {
@@ -73,7 +73,7 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil return listenAddress{parts[0], port}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) { func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
host := address.host host := address.host
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
@@ -101,7 +101,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
server := httpServer{ server := httpServer{
apiKey: []byte(apiKey), apiKey: []byte(apiKey),
actionChannel: actionChannel, actionChannel: actionChannel,
responseChannel: responseChannel, getHandler: getHandler,
} }
go func() { go func() {
@@ -165,17 +165,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
case 0: case 0:
getMatch := getRegex.FindStringSubmatch(text) getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 { if len(getMatch) > 0 {
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}} response := server.getHandler(parseGetParams(getMatch[1]))
select { if len(response) > 0 {
case response := <-server.responseChannel:
return good(response) 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") { } else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method") return bad("invalid request method")
} }

File diff suppressed because it is too large Load Diff

57
src/tmux.go Normal file
View File

@@ -0,0 +1,57 @@
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", "-yS")
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) *exec.Cmd {
sh, _ := sh()
tmuxArgs = append(tmuxArgs, sh, temp)
return exec.Command("tmux", tmuxArgs...)
}, opts, true)
}

View File

@@ -107,11 +107,12 @@ func _() {
_ = x[Result-96] _ = x[Result-96]
_ = x[Jump-97] _ = x[Jump-97]
_ = x[JumpCancel-98] _ = x[JumpCancel-98]
_ = x[ClickHeader-99]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel" 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} 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 { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -73,7 +73,7 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h") fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset() r.queued.Reset()
} }
} }
@@ -88,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clicks [][2]int
ttyin *os.File ttyin *os.File
ttyout *os.File
buffer []byte buffer []byte
origState *term.State origState *term.State
width int width int
@@ -126,10 +127,10 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) { func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn() out, err := openTtyOut()
if err != nil { if err != nil {
return nil, err out = os.Stderr
} }
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false), closed: util.NewAtomicBool(false),
@@ -137,7 +138,8 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: in, ttyin: ttyin,
ttyout: out,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
@@ -792,6 +794,9 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color w.bg = r.theme.Bg.Color
} }
if !w.bg.IsDefault() && w.border.shape != BorderNone {
w.Erase()
}
w.drawBorder(false) w.drawBorder(false)
return w return w
} }
@@ -1019,7 +1024,7 @@ func (w *LightWindow) Print(text string) {
} }
func cleanse(str string) string { func cleanse(str string) string {
return strings.Replace(str, "\x1b", "", -1) return strings.ReplaceAll(str, "\x1b", "")
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {

View File

@@ -33,27 +33,21 @@ func (r *LightRenderer) fd() int {
return int(r.ttyin.Fd()) return int(r.ttyin.Fd())
} }
func (r *LightRenderer) initPlatform() error { func (r *LightRenderer) initPlatform() (err error) {
fd := r.fd() r.origState, err = term.MakeRaw(r.fd())
origState, err := term.GetState(fd)
if err != nil {
return err return err
} }
r.origState = origState
term.MakeRaw(fd)
return nil
}
func (r *LightRenderer) closePlatform() { func (r *LightRenderer) closePlatform() {
// NOOP r.ttyout.Close()
} }
func openTtyIn() (*os.File, error) { func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil { if err != nil {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil return in, nil
} }
} }
@@ -62,6 +56,14 @@ func openTtyIn() (*os.File, error) {
return in, nil 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() { func (r *LightRenderer) setupTerminal() {
term.MakeRaw(r.fd()) term.MakeRaw(r.fd())
} }

View File

@@ -96,6 +96,10 @@ func openTtyIn() (*os.File, error) {
return nil, nil return nil, nil
} }
func openTtyOut() (*os.File, error) {
return os.Stderr, nil
}
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
return err return err
@@ -135,7 +139,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
return -1, -1 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) { func (r *LightRenderer) getch(nonblock bool) (int, bool) {

View File

@@ -3,6 +3,7 @@
package tui package tui
import ( import (
"os"
"testing" "testing"
"github.com/gdamore/tcell/v2" "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. // Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) { 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 // This test is skipped when output goes to terminal, because it causes
// some glitches: // some glitches:
// - output lines may not start at the beginning of a row which makes // - output lines may not start at the beginning of a row which makes

View File

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

View File

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

View File

@@ -334,15 +334,6 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int
@@ -356,7 +347,8 @@ type MouseEvent struct {
type BorderShape int type BorderShape int
const ( const (
BorderNone BorderShape = iota BorderUndefined BorderShape = iota
BorderNone
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
@@ -701,9 +693,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
@@ -731,9 +723,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
@@ -761,9 +753,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
@@ -822,7 +814,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
// These colors are not defined in the base themes // These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg) theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg) theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch) theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled) theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter) theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)

View File

@@ -1,6 +1,7 @@
package util package util
import ( import (
"bytes"
"fmt" "fmt"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
return chars.slice 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 { func (chars *Chars) optionalRunes() []rune {
if chars.inBytes { if chars.inBytes {
return nil return nil
@@ -196,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
chars.slice = append([]byte(prefix), chars.slice...) 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 package util
import "testing" import (
"fmt"
"testing"
)
func TestToCharsAscii(t *testing.T) { func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar")) chars := ToChars([]byte("foobar"))
@@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5) check(" h o ", 5)
check(" ", 0) 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 ( import (
"math" "math"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@@ -137,14 +138,20 @@ func DurWithin(
return val return val
} }
// IsTty returns true if stdin is a terminal // IsTty returns true if the file is a terminal
func IsTty() bool { func IsTty(file *os.File) bool {
return isatty.IsTerminal(os.Stdin.Fd()) fd := file.Fd()
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
} }
// ToTty returns true if stdout is a terminal // RunOnce runs the given function only once
func ToTty() bool { func RunOnce(f func()) func() {
return isatty.IsTerminal(os.Stdout.Fd()) once := Once(true)
return func() {
if once() {
f()
}
}
} }
// Once returns a function that returns the specified boolean value only once // Once returns a function that returns the specified boolean value only once
@@ -152,7 +159,7 @@ func Once(nextResponse bool) func() bool {
state := nextResponse state := nextResponse
return func() bool { return func() bool {
prevState := state prevState := state
state = false state = !nextResponse
return prevState return prevState
} }
} }
@@ -188,3 +195,34 @@ func ToKebabCase(s string) string {
} }
return strings.ToLower(name) 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() { if o() {
t.Error("Expected: false") t.Error("Expected: false")
} }
if o() { if !o() {
t.Error("Expected: false") t.Error("Expected: true")
}
if !o() {
t.Error("Expected: true")
} }
o = Once(true) o = Once(true)
@@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
if o() { if o() {
t.Error("Expected: false") t.Error("Expected: false")
} }
if o() {
t.Error("Expected: false")
}
} }
func TestRunesWidth(t *testing.T) { func TestRunesWidth(t *testing.T) {
@@ -203,3 +209,34 @@ func TestStringWidth(t *testing.T) {
t.Errorf("Expected: %d, Actual: %d", 1, w) 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

@@ -7,6 +7,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@@ -20,6 +21,8 @@ const (
shellTypePowerShell shellTypePowerShell
) )
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
type Executor struct { type Executor struct {
shell string shell string
shellType shellType shellType shellType
@@ -131,7 +134,9 @@ func escapeArg(s string) string {
b = append(b, '\\') b = append(b, '\\')
} }
b = append(b, '"') b = append(b, '"')
return string(b) return escapeRegex.ReplaceAllStringFunc(string(b), func(match string) string {
return "^" + match
})
} }
func (x *Executor) QuoteEntry(entry string) string { func (x *Executor) QuoteEntry(entry string) string {
@@ -152,10 +157,10 @@ func (x *Executor) QuoteEntry(entry string) string {
*/ */
return escapeArg(entry) return escapeArg(entry)
case shellTypePowerShell: case shellTypePowerShell:
escaped := strings.Replace(entry, `"`, `\"`, -1) escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'" return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default: default:
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
} }
} }

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

75
src/winpty_windows.go Normal file
View File

@@ -0,0 +1,75 @@
//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) {
sh, err := sh()
if err != nil {
return ExitError, err
}
argStr := escapeSingleQuote(args[0])
for _, arg := range args[1:] {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-winpty`
if isMintty345() {
return runProxy(argStr, func(temp string) *exec.Cmd {
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
}, opts, false)
}
return runProxy(argStr, func(temp string) *exec.Cmd {
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
}, opts, false)
}

File diff suppressed because it is too large Load Diff