Compare commits

...

240 Commits

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

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

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

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

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

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

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

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

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

Fixes #3541.
2023-12-16 15:15:00 +09:00
Junegunn Choi
e8b34cb00d Clarification on accept-or-print-query vs. become 2023-12-14 23:13:52 +09:00
Alec Scott
82954258c1 Add Spack installation instructions to README (#3526) 2023-12-10 16:08:54 +09:00
Junegunn Choi
50f092551b Lint: RuboCop 2023-12-10 16:04:30 +09:00
Junegunn Choi
c36a64be68 Add accept-or-print-query
Close #3528
2023-12-10 15:59:45 +09:00
dependabot[bot]
a343b20775 Bump golang.org/x/term from 0.13.0 to 0.15.0 (#3525)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.13.0 to 0.15.0.
- [Commits](https://github.com/golang/term/compare/v0.13.0...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-10 15:16:47 +09:00
junegunn
a714e76ae1 Deploying to master from @ junegunn/fzf@d21d5c9510 🚀 2023-12-10 00:01:46 +00:00
junegunn
d21d5c9510 Deploying to master from @ junegunn/fzf@cd6788a2bb 🚀 2023-12-03 00:01:38 +00:00
Junegunn Choi
cd6788a2bb Increase buffer size of event channel to avoid freeze on zero event
Fix #3516
2023-11-30 17:23:46 +09:00
junegunn
6b99399c41 Deploying to master from @ junegunn/fzf@952b6af445 🚀 2023-11-26 00:01:42 +00:00
Laurent Cheylus
952b6af445 Allow files creation in /tmp on OpenBSD (#3512)
- src/protector/protector_openbsd.go: add tmppath for pledge
    permissions
  - fix junegunn/fzf#3511

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

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

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

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

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

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

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

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

An example:

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

A known issue:

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

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

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

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

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

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

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

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

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

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

If not defined, fall back to the previous code.

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

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

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

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

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

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

Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Christoph Anton Mitterer
2d0db98e83 [shell] don’t print error on non-existent SSH files
Signed-off-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-10-12 20:44:25 +09:00
Junegunn Choi
3df06a1c68 Fix offset-up and offset-down with --layout=reverse (#3456) 2023-10-12 19:14:03 +09:00
dependabot[bot]
a8f9432a3a Bump golang.org/x/term from 0.12.0 to 0.13.0 (#3469)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/term/compare/v0.12.0...v0.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 13:17:47 +09:00
Christoph Anton Mitterer
561e0b04a8 [bash] Use command to “protect” further commands (#3462)
This commit causes all simple commands that are not built-ins or functions to be
invoked via `command` in order to protect them from alias substitution or from
accidentally taking functions of the same name.

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

* Example:

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

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

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

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

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

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

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

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

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

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

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

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

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

Related: #3295, #3309, #3310.

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

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

**Performance comparison:**

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

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

**Other Notes**

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

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

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

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

Close #3433
2023-09-19 13:39:57 +09:00
dependabot[bot]
d2b852f7cb Bump golang.org/x/term from 0.11.0 to 0.12.0 (#3426)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/term/compare/v0.11.0...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 09:20:46 +09:00
Junegunn Choi
901939bd96 Add support for limit and offset parameters for GET / endpoint
Related #3372
2023-09-18 00:55:20 +09:00
Timofei Bredov
edfdcc8cee Basic context-aware completion for ssh command (#3424)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-09-18 00:15:04 +09:00
Junegunn Choi
3982c9a552 [vimdoc] Replace unicode figure space (U+2007) with regular space
Related: https://github.com/junegunn/fzf.vim/pull/1507

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

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

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

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

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

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

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

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

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

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

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

	Limit scope on which option is set to bare minimum.

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

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 11:51:31 +09:00
dependabot[bot]
45f92e6b38 Bump actions/checkout from 2 to 3 (#3399)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

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

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

Fixes #3095.
2023-08-12 23:29:00 +09:00
dependabot[bot]
84a9c2c112 Bump golang.org/x/term from 0.10.0 to 0.11.0 (#3393)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/term/compare/v0.10.0...v0.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 14:10:03 +09:00
dependabot[bot]
af368119cb Bump golang.org/x/sys from 0.10.0 to 0.11.0 (#3392)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/sys/compare/v0.10.0...v0.11.0)

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

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

Close #3339
Close #3364
2023-07-12 13:55:59 +09:00
dependabot[bot]
0130f64934 Bump golang.org/x/term from 0.9.0 to 0.10.0 (#3360)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/term/compare/v0.9.0...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 00:35:44 +09:00
dependabot[bot]
361e0543ee Bump golang.org/x/sys from 0.9.0 to 0.10.0 (#3361)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/sys/compare/v0.9.0...v0.10.0)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-22 17:00:06 +09:00
dependabot[bot]
e6095cb7e8 Bump golang.org/x/sys from 0.8.0 to 0.9.0 (#3337)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/sys/compare/v0.8.0...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-21 13:41:21 +09:00
dependabot[bot]
b876b8af11 Bump crate-ci/typos from 1.14.10 to 1.15.0 (#3331)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.14.10 to 1.15.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.14.10...v1.15.0)

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

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

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

Close #3299
2023-05-26 00:08:20 +09:00
dependabot[bot]
d2bde205f0 Bump golang.org/x/term from 0.7.0 to 0.8.0 (#3285)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/term/compare/v0.7.0...v0.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 23:55:57 +09:00
dependabot[bot]
5620f70f9a Bump crate-ci/typos from 1.13.16 to 1.14.10 (#3306)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.13.16 to 1.14.10.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.13.16...v1.14.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 23:55:34 +09:00
Syphdias
37f258b1bf Add key combinations for ctrl-delete and shift-delete (#3284)
Currently there is not option to bind ctrl-delete and shift-delete. As
suggested by issue #3240, shift-delete could be used to bind "delete
entry from history" as it is a common way to do so in other
applications, e.g. browsers.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 23:24:56 +09:00
dependabot[bot]
ac4d22cd12 Bump golang.org/x/sys from 0.6.0 to 0.7.0 (#3248)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/compare/v0.6.0...v0.7.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fix #3149
2023-01-30 21:39:18 +09:00
Junegunn Choi
0f4af38457 [vim] Simplify --border injection
Prepend the border options so that the user can override them in
'options' entry of the spec.
2023-01-27 14:00:22 +09:00
Junegunn Choi
aef39f1160 [vim] Fix missing --border when --border-label is present 2023-01-27 11:00:59 +09:00
66 changed files with 3871 additions and 1039 deletions

View File

@@ -33,12 +33,12 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: 1.19 go-version: 1.19

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.18

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

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

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

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

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

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

View File

@@ -74,6 +74,7 @@ builds:
- arm64 - arm64
- loong64 - loong64
- ppc64le - ppc64le
- s390x
goarm: goarm:
- 5 - 5
- 6 - 6

View File

@@ -1 +1 @@
golang 1.19 golang 1.20.4

View File

@@ -1,7 +1,10 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
*(Last update: 2022/08/25)* * *Last update: 2023/12/29*
* *Requires fzf 0.45.0 or above*
---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
@@ -13,6 +16,7 @@ Advanced fzf examples
* [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)
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
* [Ripgrep integration](#ripgrep-integration) * [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher) * [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
@@ -205,6 +209,30 @@ find * | fzf --prompt 'All> ' \
![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png) ![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png)
### Toggling with a single key binding
The above example uses two different key bindings to toggle between two modes,
but can we just use a single key binding?
To make a key binding behave differently each time it is pressed, we need:
1. a way to store the current state. i.e. "which mode are we in?"
2. and a way to dynamically perform different actions depending on the state.
The following example shows how to 1. store the current mode in the prompt
string, 2. and use this information (`{fzf:prompt}`) to determine which
actions to perform using the `transform` action.
```sh
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
--preview '[[ {fzf:prompt} =~ Files ]] && bat --color=always {} || tree -C {}'
```
Ripgrep integration Ripgrep integration
------------------- -------------------
@@ -236,15 +264,13 @@ file called `rfv`.
# 1. Search for text in files using Ripgrep # 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf # 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim # 3. Open the file in Vim
IFS=: read -ra selected < <( rg --color=always --line-number --no-heading --smart-case "${*:-}" |
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
fzf --ansi \ fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
And run it with an initial query string. And run it with an initial query string.
@@ -307,8 +333,12 @@ I know it's a lot to digest, let's try to break down the code.
position in the window position in the window
- `~3` makes the top three lines fixed header so that they are always - `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset visible regardless of the scroll offset
- Once we selected a line, we open the file with `vim` (`vim - Instead of using shell script to process the final output of fzf, we use
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`). `become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
into a new process that opens the file with `vim` (`vim {1}`) and move the
cursor to the line (`+{2}`).
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
### Using fzf as interactive Ripgrep launcher ### Using fzf as interactive Ripgrep launcher
@@ -331,25 +361,22 @@ 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="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload:$RG_PREFIX {q}" \
fzf --ansi \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
![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 `rg ... | fzf` form, we start fzf without an - Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way an empty input (`: | fzf`), then we make it start the initial Ripgrep
fzf can kill the initial Ripgrep process it starts with the initial query. process immediately via `start:reload` binding. This way, fzf owns the
Otherwise, the initial Ripgrep process will keep consuming system resources initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
even after `reload` is triggered. 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
@@ -358,8 +385,6 @@ IFS=: read -ra selected < <(
### Switching to fzf-only search mode ### Switching to fzf-only search mode
*(Requires fzf 0.27.1 or above)*
In the previous example, we lost fuzzy matching capability as we completely In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event. fzf-only search mode by *"unbinding"* `reload` action from `change` event.
@@ -375,19 +400,16 @@ 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="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload:$RG_PREFIX {q}" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--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" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \ --prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
* Phase 1. Filtering with Ripgrep * Phase 1. Filtering with Ripgrep
@@ -408,10 +430,8 @@ IFS=: read -ra selected < <(
### Switching between Ripgrep mode and fzf mode ### Switching between Ripgrep mode and fzf mode
*(Requires fzf 0.30.0 or above)* [fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
that were previously "unbound" via `unbind`.
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
@@ -421,25 +441,32 @@ CTRL-F.
#!/usr/bin/env bash #!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F) # Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--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+clear-query+rebind(ctrl-r)" \ --bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \ --bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--prompt '1. Ripgrep> ' \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \ --header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
- To restore the query string when switching between modes, we store the
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
- Also note that we unbind `ctrl-r` binding on `start` event which is
triggered once when fzf starts.
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
Log tailing Log tailing
----------- -----------
@@ -465,14 +492,15 @@ Kubernetes pods.
```bash ```bash
pods() { pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \ : | command='kubectl get pods --all-namespaces' fzf \
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 '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 > /dev/tty' \
--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}) > /dev/tty' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--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

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.17 or above - Go 1.18 or above
### Using Makefile ### Using Makefile

View File

@@ -1,6 +1,283 @@
CHANGELOG CHANGELOG
========= =========
0.45.0
------
- Added `transform` action to conditionally perform a series of actions
```sh
# Disallow selecting an empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
# Move cursor past the empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
--bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
# A single key binding to toggle between modes
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"'
```
- Added placeholder expressions
- `{fzf:action}` - The name of the last action performed
- `{fzf:prompt}` - Prompt string (including ANSI color codes)
- `{fzf:query}` - Synonym for `{q}`
- Added support for negative height
```sh
# Terminal height minus 1, so you can still see the command line
fzf --height=-1
```
- This handles a terminal resize better than `--height=$(($(tput lines) - 1))`
- Added `accept-or-print-query` action that acts like `accept` but prints the
current query when there's no match for the query
```sh
# You can make CTRL-R paste the current query when there's no match
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'
```
- Note that there are alternative ways to implement the same strategy
```sh
# 'become' is apparently more versatile but it's not available on Windows.
export FZF_CTRL_R_OPTS='--bind "enter:become:if [ -z {} ]; then echo {q}; else echo {}; fi"'
# Using the new 'transform' action
export FZF_CTRL_R_OPTS='--bind "enter:transform:[ -z {} ] && echo print-query || echo accept"'
```
- Added `show-header` and `hide-header` actions
- Bug fixes
0.44.1
------
- Fixed crash when preview window is hidden on `focus` event
0.44.0
------
- (Experimental) Sixel image support in preview window (not available on Windows)
- [bin/fzf-preview.sh](bin/fzf-preview.sh) is added to demonstrate how to
display an image using Kitty image protocol or Sixel. You can use it
like so:
```sh
fzf --preview='fzf-preview.sh {}'
```
- (Experimental) iTerm2 inline image protocol support in preview window (not available on Windows)
```sh
# Using https://iterm2.com/utilities/imgcat
fzf --preview 'imgcat -W $FZF_PREVIEW_COLUMNS -H $FZF_PREVIEW_LINES {}'
```
- HTTP server can be configured to accept remote connections
```sh
# FZF_API_KEY is required for a non-localhost listen address
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
```
- To allow remote process execution, use `--listen-unsafe` instead
(`execute*`, `reload*`, `become`, `preview`, `change-preview`, `transform-*`)
```sh
fzf --listen-unsafe 0.0.0.0:6266
```
- Bug fixes
0.43.0
------
- (Experimental) Added support for Kitty image protocol in the preview window
(not available on Windows)
```sh
fzf --preview='
if file --mime-type {} | grep -qF image/; then
# --transfer-mode=memory is the fastest option but if you want fzf to be able
# to redraw the image on terminal resize or on 'change-preview-window',
# you need to use --transfer-mode=stream.
kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \$d
else
bat --color=always {}
fi
'
```
- (Experimental) `--listen` server can report program state in JSON format (`GET /`)
```sh
# fzf server started in "headless" mode
fzf --listen 6266 2> /dev/null
# Get program state
curl localhost:6266 | jq .
# Increase the number of items returned (default: 100)
curl localhost:6266?limit=1000 | jq .
```
- `--listen` server can be secured by setting `$FZF_API_KEY` environment
variable.
```sh
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
# Server
fzf --listen 6266
# Client
curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
```
- Added `toggle-header` action
- Added mouse events for `--bind`
- `scroll-up` (bound to `up`)
- `scroll-down` (bound to `down`)
- `shift-scroll-up` (bound to `toggle+up`)
- `shift-scroll-down` (bound to `toggle+down`)
- `shift-left-click` (bound to `toggle`)
- `shift-right-click` (bound to `toggle`)
- `preview-scroll-up` (bound to `preview-up`)
- `preview-scroll-down` (bound to `preview-down`)
```sh
# Twice faster scrolling both in the main window and the preview window
fzf --bind 'scroll-up:up+up,scroll-down:down+down' \
--bind 'preview-scroll-up:preview-up+preview-up' \
--bind 'preview-scroll-down:preview-down+preview-down' \
--preview 'cat {}'
```
- Added `offset-up` and `offset-down` actions
```sh
# Scrolling will behave similarly to CTRL-E and CTRL-Y of vim
fzf --bind scroll-up:offset-up,scroll-down:offset-down \
--bind ctrl-y:offset-up,ctrl-e:offset-down \
--scroll-off=5
```
- Shell extensions
- Updated bash completion for fzf options
- bash key bindings no longer requires perl; it will use awk or mawk
instead if perl is not found
- Basic context-aware completion for ssh command
- Applied `--scheme=path` for better ordering of the result
- Bug fixes and improvements
0.42.0
------
- Added new info style: `--info=right`
- Added new info style: `--info=inline-right`
- Added new border style `thinblock` which uses symbols for legacy computing
[one eighth block elements](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing)
- Similarly to `block`, this style is suitable when using a different
background color because the window is completely contained within the border.
```sh
BAT_THEME=GitHub fzf --info=right --border=thinblock --preview-window=border-thinblock \
--margin=3 --scrollbar=▏▕ --preview='bat --color=always --style=numbers {}' \
--color=light,query:238,fg:238,bg:251,bg+:249,gutter:251,border:248,preview-bg:253
```
- This style may not render correctly depending on the font and the
terminal emulator.
0.41.1
------
- Fixed a bug where preview window is not updated when `--disabled` is set and
a reload is triggered by `change:reload` binding
0.41.0
------
- Added color name `preview-border` and `preview-scrollbar`
- Added new border style `block` which uses [block elements](https://en.wikipedia.org/wiki/Block_Elements)
- `--scrollbar` can take two characters, one for the main window, the other
for the preview window
- Putting it altogether:
```sh
fzf-tmux -p 80% --padding 1,2 --preview 'bat --style=plain --color=always {}' \
--color 'bg:237,bg+:235,gutter:237,border:238,scrollbar:236' \
--color 'preview-bg:235,preview-border:236,preview-scrollbar:234' \
--preview-window 'border-block' --border block --scrollbar '▌▐'
```
- Bug fixes and improvements
0.40.0
------
- Added `zero` event that is triggered when there's no match
```sh
# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
```
- New actions
- Added `track` action which makes fzf track the current item when the
search result is updated. If the user manually moves the cursor, or the
item is not in the updated search result, tracking is automatically
disabled. Tracking is useful when you want to see the surrounding items
by deleting the query string.
```sh
# Narrow down the list with a query, point to a command,
# and hit CTRL-T to see its surrounding commands.
export FZF_CTRL_R_OPTS="
--preview 'echo {}' --preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-t:track+clear-query'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- Added `change-header(...)`
- Added `transform-header(...)`
- Added `toggle-track` action
- Fixed `--track` behavior when used with `--tac`
- However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing.
- Bug fixes and improvements
0.39.0
------
- Added `one` event that is triggered when there's only one match
```sh
# Automatically select the only match
seq 10 | fzf --bind one:accept
```
- Added `--track` option that makes fzf track the current selection when the
result list is updated. This can be useful when browsing logs using fzf with
sorting disabled.
```sh
git log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list
```
- If you use `--listen` option without a port number fzf will automatically
allocate an available port and export it as `$FZF_PORT` environment
variable.
```sh
# Automatic port assignment
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
# Say hello
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
```
- A carriage return and a line feed character will be rendered as dim ␍ and
␊ respectively.
```sh
printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}'
```
- fzf will stop rendering a non-displayable characters as a space. This will
likely cause less glitches in the preview window.
```sh
fzf --preview 'head -1000 /dev/random'
```
- Bug fixes and improvements
0.38.0
------
- New actions
- `become(...)` - Replace the current fzf process with the specified
command using `execve(2)` system call.
See https://github.com/junegunn/fzf#turning-into-a-different-process for
more information.
```sh
# Open selected files in Vim
fzf --multi --bind 'enter:become(vim {+})'
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
- This action is not supported on Windows
- `show-preview`
- `hide-preview`
- Bug fixes
- `--preview-window 0,hidden` should not execute the preview command until
`toggle-preview` action is triggered
0.37.0 0.37.0
------ ------
- Added a way to customize the separator of inline info - Added a way to customize the separator of inline info
@@ -118,7 +395,7 @@ CHANGELOG
- Added color name `preview-label` for `--preview-label` (defaults to `label` - Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`) for `--border-label`)
- Better support for (Windows) terminals where each box-drawing character - Better support for (Windows) terminals where each box-drawing character
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`. takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
- On Vim, the variable will be automatically set if `&ambiwidth` is `double` - On Vim, the variable will be automatically set if `&ambiwidth` is `double`
- Behavior changes - Behavior changes
- fzf will always execute the preview command if the command template - fzf will always execute the preview command if the command template
@@ -227,7 +504,7 @@ CHANGELOG
(sleep 2; seq 1000) | fzf --height ~50% (sleep 2; seq 1000) | fzf --height ~50%
``` ```
- Fixed tcell renderer used to render full-screen fzf on Windows - Fixed tcell renderer used to render full-screen fzf on Windows
- `--no-clear` is deprecated. Use `reload` action instead. - ~~`--no-clear` is deprecated. Use `reload` action instead.~~
0.33.0 0.33.0
------ ------

View File

@@ -1,4 +1,4 @@
FROM archlinux FROM --platform=linux/amd64 archlinux
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

View File

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

View File

@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION ifdef FZF_REVISION
REVISION := $(FZF_REVISION) REVISION := $(FZF_REVISION)
else else
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null) REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
endif endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION) $(error Not on git repository; cannot determine $$FZF_REVISION)
@@ -29,6 +29,7 @@ BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYS390 := fzf-$(GOOS)_s390x
BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32) BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386) else ifeq ($(UNAME_M),i386)
@@ -54,7 +57,9 @@ else ifeq ($(UNAME_M),armv6l)
else ifeq ($(UNAME_M),armv7l) else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7) BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),armv8l) else ifeq ($(UNAME_M),armv8l)
BINARY := $(BINARYARM8) # armv8l is always 32-bit and should implement the armv7 ISA, so
# just use the same filename as for armv7.
BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),arm64) else ifeq ($(UNAME_M),arm64)
BINARY := $(BINARYARM8) BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),aarch64) else ifeq ($(UNAME_M),aarch64)
@@ -84,10 +89,17 @@ bench:
install: bin/fzf install: bin/fzf
generate:
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
build: build:
goreleaser --rm-dist --snapshot goreleaser build --rm-dist --snapshot --skip-post-hooks
release: release:
# Make sure that the tests pass and the build works
TAGS=tcell make test
make test build clean
ifndef GITHUB_TOKEN ifndef GITHUB_TOKEN
$(error GITHUB_TOKEN is not defined) $(error GITHUB_TOKEN is not defined)
endif endif
@@ -132,6 +144,8 @@ target/$(BINARY32): $(SOURCES)
target/$(BINARY64): $(SOURCES) target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYS390): $(SOURCES)
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm # https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
@@ -170,4 +184,4 @@ update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all build release test bench install clean docker docker-test update .PHONY: all generate build release test bench install clean docker docker-test update

View File

@@ -15,7 +15,7 @@ set rtp+=/usr/local/opt/fzf
" If installed using Homebrew on Apple Silicon " If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf set rtp+=/opt/homebrew/opt/fzf
" If installed using git " If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
@@ -26,7 +26,10 @@ written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If installed using Homebrew on Apple Silicon
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
``` ```
@@ -118,7 +121,7 @@ let g:fzf_action = {
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }')) call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
copen copen
cc cc
endfunction endfunction
@@ -486,4 +489,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi

116
README.md

File diff suppressed because one or more lines are too long

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

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

View File

@@ -151,12 +151,18 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
else
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
fi
cleanup() { cleanup() {
\rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options # Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
eval "tmux ${tmux_win_opts[*]}" eval "tmux ${tmux_win_opts[*]}"
fi fi
@@ -179,15 +185,21 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then if [[ "$opt" =~ "-E" ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS" tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
tmux_version=$(tmux -V) if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
if [[ ! $tmux_version =~ 3\.2 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS" FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt" opt="-B $opt"
elif [[ $tmux_version = 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
exit 2
fi fi
fi fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"
# Build arguments to fzf # Build arguments to fzf
@@ -221,9 +233,9 @@ else
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux set-window-option synchronize-panes off \;\ tmux \
set-window-option remain-on-exit off \;\
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \ split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
$tmux_off_opts \
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; } > /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
cat $fifo2 cat $fifo2
exit "$(cat $fifo3)" exit "$(cat $fifo3)"

View File

@@ -1,4 +1,4 @@
fzf.txt fzf Last change: May 19 2021 fzf.txt fzf Last change: January 1 2024
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -32,7 +32,10 @@ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using git " If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
< <
If you use {vim-plug}{1}, the same can be written as: If you use {vim-plug}{1}, the same can be written as:
@@ -40,7 +43,10 @@ If you use {vim-plug}{1}, the same can be written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If installed using Homebrew on Apple Silicon
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
< <
But if you want the latest Vim plugin file from GitHub rather than the one But if you want the latest Vim plugin file from GitHub rather than the one
@@ -68,16 +74,16 @@ SUMMARY *fzf-summary*
The Vim plugin of fzf provides two core functions, and `:FZF` command which is The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them. the basic file selector command built on top of them.
1. `fzf#run([specdict])` 1. `fzf#run([spec dict])`
- Starts fzf inside Vim with the given spec - Starts fzf inside Vim with the given spec
- `:callfzf#run({'source':'ls'})` - `:call fzf#run({'source': 'ls'})`
2. `fzf#wrap([specdict])->(dict)` 2. `fzf#wrap([spec dict]) -> (dict)`
- Takes a spec for `fzf#run` and returns an extended version of it with - Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`) additional options for addressing global preferences (`g:fzf_xxx`)
- `:echofzf#wrap({'source':'ls'})` - `:echo fzf#wrap({'source': 'ls'})`
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run` - We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
- `:callfzf#run(fzf#wrap({'source':'ls'}))` - `:call fzf#run(fzf#wrap({'source': 'ls'}))`
3. `:FZF[fzf_optionsstring][pathstring]` 3. `:FZF [fzf_options string] [path string]`
- Basic fuzzy file selector - Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript to - A reference implementation for those who don't want to write VimScript to
implement custom commands implement custom commands
@@ -143,7 +149,7 @@ Examples~
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }')) call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
copen copen
cc cc
endfunction endfunction
@@ -222,12 +228,12 @@ list:
`spinner` | Streaming input indicator `spinner` | Streaming input indicator
`query` | Query string `query` | Query string
`disabled` | Query string when search is disabled `disabled` | Query string when search is disabled
`prompt` | Prompt before query ( `>` ) `prompt` | Prompt before query ( `> ` )
`pointer` | Pointer to the current line ( `>` ) `pointer` | Pointer to the current line ( `>` )
----------------------------+------------------------------------------------------ ----------------------------+------------------------------------------------------
- `component` specifies the component (`fg` / `bg`) from which to extract the - `component` specifies the component (`fg` / `bg`) from which to extract the
color when considering each of the following highlight groups color when considering each of the following highlight groups
- `group1[,group2,...]` is a list of highlight groups that are searched (in - `group1 [, group2, ...]` is a list of highlight groups that are searched (in
order) for a matching color definition order) for a matching color definition
For example, consider the following specification: For example, consider the following specification:
@@ -268,7 +274,7 @@ as the sink.
< <
Instead of using the default find command, you can use any shell command as Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's the source. The following example will list the files managed by git. It's
equivalent to running `gitls-files|fzf` on shell. equivalent to running `git ls-files | fzf` on shell.
> >
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})
< <
@@ -296,7 +302,7 @@ The following table summarizes the available options.
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
Option name | Type | Description ~ Option name | Type | Description ~
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` ) `source` | string | External command to generate input to fzf (e.g. `find .` )
`source` | list | Vim list as input to fzf `source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item `sink` | funcref | Reference to function to process each selected item
@@ -305,8 +311,8 @@ The following table summarizes the available options.
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `verticalaboveleft30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width':0.9,'height':0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string `options` entry can be either a string or a list. For simple cases, string
@@ -343,7 +349,7 @@ So how can we make our custom `fzf#run` calls also respect those variables?
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
`fzf#run`. `fzf#run`.
- `fzf#wrap([namestring],[specdict],[fullscreenbool])->(dict)` - `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
- All arguments are optional. Usually we only need to pass a spec - All arguments are optional. Usually we only need to pass a spec
dictionary. dictionary.
- `name` is for managing history files. It is ignored if `g:fzf_history_dir` - `name` is for managing history files. It is ignored if `g:fzf_history_dir`
@@ -377,7 +383,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0)) command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
< <
Our `:LS` command will be much more useful if we can pass a directory argument Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS/tmp` is possible. to it, so that something like `:LS /tmp` is possible.
> >
command! -bang -complete=dir -nargs=? LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
@@ -396,10 +402,10 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
- `g:fzf_layout` - `g:fzf_layout`
- `g:fzf_action` - `g:fzf_action`
- Works only when no custom `sink` (or `sink*`) is provided - Works only when no custom `sink` (or `sinklist`) is provided
- Having custom sink usually means that each entry is not an ordinary - Having custom sink usually means that each entry is not an ordinary
file path (e.g. name of color scheme), so we can't blindly apply the file path (e.g. name of color scheme), so we can't blindly apply the
same strategy (i.e. `tabeditsome-color-scheme` doesn't make sense) same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
- `g:fzf_colors` - `g:fzf_colors`
- `g:fzf_history_dir` - `g:fzf_history_dir`
@@ -488,7 +494,7 @@ or above) by putting fzf-tmux options in `tmux` key.
*fzf-hide-statusline* *fzf-hide-statusline*
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. the window.
For example, if you open fzf on the bottom on the screen (e.g. `{'down': For example, if you open fzf on the bottom on the screen (e.g. `{'down':
@@ -506,7 +512,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

10
go.mod
View File

@@ -5,17 +5,19 @@ require (
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-runewidth v0.0.14 github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.2 github.com/rivo/uniseg v0.4.4
github.com/saracen/walker v0.1.3 github.com/saracen/walker v0.1.3
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab golang.org/x/sys v0.15.0
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.15.0
) )
require ( require (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
) )
go 1.17 go 1.17

16
go.sum
View File

@@ -11,14 +11,16 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g= github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk= github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -26,16 +28,20 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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=
@@ -44,4 +50,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.37.0 version=0.45.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -178,6 +178,7 @@ case "$archi" in
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;; Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;; Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;; Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;; FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
@@ -195,12 +196,12 @@ if [ -n "$binary_error" ]; then
echo " - $binary_error !!!" echo " - $binary_error !!!"
fi fi
if command -v go > /dev/null; then if command -v go > /dev/null; then
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... " echo -n "Building binary (go install github.com/junegunn/fzf) ... "
if [ -z "${GOPATH-}" ]; then if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH" mkdir -p "$GOPATH"
fi fi
if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then
echo "OK" echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
@@ -244,7 +245,7 @@ for shell in $shells; do
src=${prefix_expand}.${shell} src=${prefix_expand}.${shell}
echo -n "Generate $src ... " echo -n "Generate $src ... "
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
if [ $auto_completion -eq 0 ]; then if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi

View File

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

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.37" var version string = "0.45"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

View File

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

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2024" "fzf 0.45.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -57,9 +57,9 @@ Choose scoring scheme tailored for different types of input.
.br .br
.BR default " Generic scoring scheme designed to work well with any type of input" .BR default " Generic scoring scheme designed to work well with any type of input"
.br .br
.BR path " Scoring scheme for paths (additional bonus point only after path separator) .BR path " Scoring scheme well suited for file paths
.br .br
.BR history " Scoring scheme for command history (no additional bonus points). .BR history " Scoring scheme well suited for command history or any input where chronological ordering is important
Sets \fB--tiebreak=index\fR as well. Sets \fB--tiebreak=index\fR as well.
.br .br
.TP .TP
@@ -92,6 +92,19 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort" .B "+s, --no-sort"
Do not sort the result Do not sort the result
.TP .TP
.B "--track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is
not recommended to use this option with \fB--tac\fR as the resulting behavior
can be confusing. Also, consider using \fBtrack\fR action instead of this
option.
.RS
e.g.
\fBgit log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list\fR
.RE
.TP
.B "--tac" .B "--tac"
Reverse the order of the input Reverse the order of the input
@@ -179,9 +192,21 @@ Label characters for \fBjump\fR and \fBjump-accept\fR
.TP .TP
.BI "--height=" "[~]HEIGHT[%]" .BI "--height=" "[~]HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using Display fzf window below the cursor with the given height instead of using
the full screen. When prefixed with \fB~\fR, fzf will automatically determine the full screen.
the height in the range according to the input size. Note that adaptive height
is not compatible with top/bottom margin and padding given in percent size. If a negative value is specified, the height is calculated as the terminal
height minus the given value.
fzf --height=-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the
range according to the input size. Note that adaptive height is not compatible
with top/bottom margin and padding given in percent size. It is also not
compatible with a negative height value.
# Will not take up 100% of the screen
seq 5 | fzf --height=~100%
.TP .TP
.BI "--min-height=" "HEIGHT" .BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10). Minimum height when \fB--height\fR is given in percent (default: 10).
@@ -215,6 +240,10 @@ Draw border around the finder
.br .br
.BR double " Border with double lines" .BR double " Border with double lines"
.br .br
.BR block " Border using block elements; suitable when using different background colors"
.br
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals"
.br
.BR horizontal " Horizontal lines above and below the finder" .BR horizontal " Horizontal lines above and below the finder"
.br .br
.BR vertical " Vertical lines on each side of the finder" .BR vertical " Vertical lines on each side of the finder"
@@ -231,8 +260,9 @@ Draw border around the finder
.br .br
If you use a terminal emulator where each box-drawing character takes If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is 2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
still not properly rendered, set \fB--no-unicode\fR. \fB0\fR or \fB1\fR. If the border is still not properly rendered, set
\fB--no-unicode\fR.
.TP .TP
.BI "--border-label" [=LABEL] .BI "--border-label" [=LABEL]
@@ -339,10 +369,14 @@ Determines the display style of finder info (match counters).
.br .br
.BR default " Display on the next line to the prompt" .BR default " Display on the next line to the prompt"
.br .br
.BR right " Display on the right end of the next line to the prompt"
.br
.BR inline " Display on the same line with the default separator ' < '" .BR inline " Display on the same line with the default separator ' < '"
.br .br
.BR inline:SEPARATOR " Display on the same line with a non-default separator" .BR inline:SEPARATOR " Display on the same line with a non-default separator"
.br .br
.BR inline-right " Display on the right end of the same line
.br
.BR hidden " Do not display finder info" .BR hidden " Do not display finder info"
.br .br
@@ -363,9 +397,10 @@ Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB \fB--separator=''\fB
.TP .TP
.BI "--scrollbar=" "CHAR" .BI "--scrollbar=" "CHAR1[CHAR2]"
Use the given character to render scrollbar. (default: '│' or ':' depending on Use the given character to render scrollbar. (default: '│' or ':' depending on
\fB--no-unicode\fR). \fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
the preview window.
.TP .TP
.B "--no-scrollbar" .B "--no-scrollbar"
@@ -432,8 +467,10 @@ color mappings.
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR) \fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
\fBseparator \fRHorizontal separator on info line
\fBscrollbar \fRScrollbar \fBscrollbar \fRScrollbar
\fBpreview-border \fRBorder around the preview window (\fB--preview\fR)
\fBpreview-scrollbar \fRScrollbar
\fBseparator \fRHorizontal separator on info line
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR) \fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR) \fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
\fBprompt \fRPrompt \fBprompt \fRPrompt
@@ -523,6 +560,9 @@ they represent the exact size of the preview window. (It also overrides
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
prefix.) prefix.)
fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
the preview command can determine the position of the preview window.
A placeholder expression starting with \fB+\fR flag will be replaced to the A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection space-separated list of the selected lines (or the current line if no selection
was made) individually quoted. was made) individually quoted.
@@ -534,10 +574,6 @@ e.g.
When using a field index expression, leading and trailing whitespace is stripped When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag. from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may multi-select a large number of items and the length of the evaluated string may
@@ -549,6 +585,16 @@ e.g.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\ seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR --preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
Also,
* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
.br
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
.br
* \fB{fzf:action}\fR is replaced to to the name of the last action performed
* \fB{fzf:prompt}\fR is replaced to to the prompt string
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current Preview window will be updated even when there is no match for the current
@@ -566,6 +612,14 @@ e.g.
echo "$i" echo "$i"
sleep 0.01 sleep 0.01
done'\fR done'\fR
fzf has experimental support for Kitty graphics protocol and Sixel graphics.
The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-preview.sh
script to render an image using either of the protocols inside the preview window.
e.g.
\fBfzf --preview='fzf-preview.sh {}'
.RE .RE
.TP .TP
@@ -582,6 +636,10 @@ Should be used with one of the following \fB--preview-window\fR options.
.br .br
.B * border-double .B * border-double
.br .br
.B * border-block
.br
.B * border-thinblock
.br
.B * border-horizontal .B * border-horizontal
.br .br
.B * border-top .B * border-top
@@ -729,6 +787,21 @@ Read input delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--print0" .B "--print0"
Print output delimited by ASCII NUL characters instead of newline characters Print output delimited by ASCII NUL characters instead of newline characters
.TP
.B "--no-clear"
Do not clear finder interface on exit. If fzf was started in full screen mode,
it will not switch back to the original screen, so you'll have to manually run
\fBtput rmcup\fR to return. This option can be used to avoid flickering of the
screen when your application needs to start fzf multiple times in order. (Note
that in most cases, it is preferable to use \fBreload\fR action instead.)
e.g.
\fBfoo=$(seq 100 | fzf --no-clear) || (
# Need to manually switch back to the main screen when cancelled
tput rmcup
exit 1
) && seq "$foo" 100 | fzf
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
@@ -738,16 +811,40 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--listen=HTTP_PORT" .B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server on the given port. It allows external processes to send Start HTTP server and listen on the given address. It allows external processes
actions to perform via POST method. to send actions to perform via POST method.
- If the port number is omitted or given as 0, fzf will automatically choose
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
sending an API key with the same value in the \fBx-api-key\fR HTTP header
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
- To allow remote process execution, use \fB--listen-unsafe\fR
e.g. e.g.
\fB# Start HTTP server on port 6266 \fB# Start HTTP server on port 6266
fzf --listen 6266 fzf --listen 6266
# Get program state in JSON format (experimental)
curl localhost:6266
# Send action to the server # Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Start HTTP server on port 6266 with remote connections allowed
# * Listening on non-localhost address requires using an API key
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
# Send an authenticated action
curl -XPOST localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
# Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR \fR
.TP .TP
.B "--version" .B "--version"
@@ -765,6 +862,11 @@ this case make sure that the command is POSIX-compliant.
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.TP
.B FZF_API_KEY
Can be used to require an API key when using \fB--listen\fR option. If not set,
no authentication will be required by the server. You can set this value if
you need to protect against DNS rebinding and privilege escalation attacks.
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"
@@ -850,6 +952,8 @@ e.g.
.br .br
\fIctrl-space\fR \fIctrl-space\fR
.br .br
\fIctrl-delete\fR
.br
\fIctrl-\\\fR \fIctrl-\\\fR
.br .br
\fIctrl-]\fR \fIctrl-]\fR
@@ -918,6 +1022,8 @@ e.g.
.br .br
\fIshift-right\fR \fIshift-right\fR
.br .br
\fIshift-delete\fR
.br
\fIalt-shift-up\fR \fIalt-shift-up\fR
.br .br
\fIalt-shift-down\fR \fIalt-shift-down\fR
@@ -932,6 +1038,22 @@ e.g.
.br .br
\fIdouble-click\fR \fIdouble-click\fR
.br .br
\fIscroll-up\fR
.br
\fIscroll-down\fR
.br
\fIpreview-scroll-up\fR
.br
\fIpreview-scroll-down\fR
.br
\fIshift-left-click\fR
.br
\fIshift-right-click\fR
.br
\fIshift-scroll-up\fR
.br
\fIshift-scroll-down\fR
.br
or any single character or any single character
.SS AVAILABLE EVENTS: .SS AVAILABLE EVENTS:
@@ -977,6 +1099,28 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE .RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
effective before the interactive finder starts but \fBone\fR event is triggered
by the interactive finder.
e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIzero\fR
.RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
effective before the interactive finder starts but \fBzero\fR event is
triggered by the interactive finder.
e.g.
\fB# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
.RE
\fIbackward-eof\fR \fIbackward-eof\fR
.RS .RS
@@ -994,14 +1138,17 @@ A key or an event can be bound to one or more of the following actions.
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBaccept-or-print-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) \fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
@@ -1036,6 +1183,10 @@ A key or an event can be bound to one or more of the following actions.
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBhide-header\fR
\fBhide-preview\fR
\fBoffset-down\fR (similar to CTRL-E of Vim)
\fBoffset-up\fR (similar to CTRL-Y of Vim)
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprev-selected\fR (move to the previous selected item) \fBprev-selected\fR (move to the previous selected item)
@@ -1058,17 +1209,24 @@ A key or an event can be bound to one or more of the following actions.
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBshow-header\fR
\fBshow-preview\fR
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-header\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle-track\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtrack\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command)
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header using an external command)
\fBtransform-preview-label(...)\fR (transform preview label using an external command) \fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command) \fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command) \fBtransform-query(...)\fR (transform query string using an external command)
@@ -1141,6 +1299,14 @@ On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant. POSIX-compliant.
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
current fzf process with the specified command using \fBexecve(2)\fR system
call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list
@@ -1171,6 +1337,28 @@ e.g.
\fB# You can still filter and select entries from the initial list for 3 seconds \fB# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
.SS TRANSFORM ACTIONS
Actions with \fBtransform-\fR prefix are used to transform the states of fzf
using the output of an external command. The output of these commands are
expected to be a single line of text.
e.g.
\fBfzf --bind 'focus:transform-header:file --brief {}'\fR
\fBtransform(...)\fR action runs an external command that should print a series
of actions to be performed. The output should be in the same format as the
payload of HTTP POST request to the \fB--listen\fR server.
e.g.
\fB# Disallow selecting an empty line
echo -e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \\
--bind 'enter:transform:[[ -n {} ]] &&
echo accept ||
echo "change-header:Invalid selection"'
\fR
.SS PREVIEW BINDING .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2023 Junegunn Choi " Copyright (c) 2013-2024 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -164,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin) if has_key(s:versions, a:bin)
return s:versions[a:bin] return s:versions[a:bin]
end end
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height' let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
let output = systemlist(command) let output = systemlist(command)
if v:shell_error || empty(output) if v:shell_error || empty(output)
return '' return ''
@@ -456,6 +456,30 @@ function! s:writefile(...)
endif endif
endfunction endfunction
function! s:extract_option(opts, name)
let opt = ''
let expect = 0
" There are a few cases where this function doesn't work as expected.
" Let's just assume such cases are extremely unlikely in real world.
" e.g. --query --border
for word in split(a:opts)
if expect && word !~ '^"\=-'
let opt = opt . ' ' . word
let expect = 0
elseif word == '--no-'.a:name
let opt = ''
elseif word =~ '^--'.a:name.'='
let opt = word
elseif word =~ '^--'.a:name.'$'
let opt = word
let expect = 1
elseif expect
let expect = 0
endif
endfor
return opt
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -511,10 +535,8 @@ try
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --height='.height
endif endif
" Respect --border option given in 'options' " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0 let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let optstr .= s:border_opt(get(dict, 'window', 0))
endif
let prev_default_command = $FZF_DEFAULT_COMMAND let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command) if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command let $FZF_DEFAULT_COMMAND = source_command
@@ -741,7 +763,7 @@ function! s:calc_size(max, val, dict)
return size return size
endif endif
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
if stridx(opts, '--header') > stridx(opts, '--no-header') if stridx(opts, '--header') > stridx(opts, '--no-header')
let margin += len(split(opts, "\n")) let margin += len(split(opts, "\n"))
endif endif
@@ -923,7 +945,7 @@ function! s:execute_term(dict, command, temps) abort
let term_opts.curwin = 1 let term_opts.curwin = 1
endif endif
call s:handle_ambidouble(term_opts) call s:handle_ambidouble(term_opts)
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts) keepjumps let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
if is_popup && exists('#TerminalWinOpen') if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen doautocmd <nomodeline> TerminalWinOpen
endif endif

View File

@@ -9,23 +9,24 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
if [[ $- =~ i ]]; then [[ $- =~ i ]] || return 0
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -F _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
} }
fi fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -F _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
} }
fi fi
@@ -68,59 +69,185 @@ _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
-x --extended -x --extended
-e --exact -e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo --algo
-i +i --scheme
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth -n --nth
--with-nth --with-nth
-d --delimiter -s --sort
+s --no-sort +s --no-sort
--track
--no-track
--tac --tac
--tiebreak --no-tac
-i
+i
-m --multi -m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse --no-mouse
--bind +c --no-color
--cycle +2 --no-256
--no-hscroll --black
--jump-labels --no-black
--height --bold
--literal --no-bold
--layout
--reverse --reverse
--margin --no-reverse
--cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info --inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels
-1 --select-1
+1 --no-select-1
-0 --exit-0
+0 --no-exit-0
--read0
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt --prompt
--pointer --pointer
--marker --marker
--header --sync
--header-lines --no-sync
--ansi --async
--tabstop --no-history
--color
--no-bold
--history --history
--history-size --history-size
--no-header
--no-header-lines
--header
--header-lines
--header-first
--no-header-first
--ellipsis
--preview --preview
--no-preview
--preview-window --preview-window
-q --query --height
-1 --select-1 --min-height
-0 --exit-0 --no-height
-f --filter --no-margin
--print-query --no-padding
--expect --no-border
--sync" --border
--no-border-label
--border-label
--border-label-pos
--no-preview-label
--preview-label
--preview-label-pos
--no-unicode
--unicode
--margin
--padding
--tabstop
--listen
--no-listen
--clear
--no-clear
--version
--"
case "${prev}" in case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
;;
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
return 0 return 0
;; ;;
--color) --color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
return 0 return 0
;; ;;
--history) --layout)
COMPREPLY=() COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
return 0
;;
--info)
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
return 0
;;
--preview-window)
COMPREPLY=( $(compgen -W "
default
hidden
nohidden
wrap
nowrap
cycle
nocycle
up top
down bottom
left
right
rounded border border-rounded
sharp border-sharp
border-bold
border-block
border-thinblock
border-double
noborder border-none
border-horizontal
border-vertical
border-up border-top
border-down border-bottom
border-left
border-right
follow
nofollow" -- "$cur") )
return 0
;;
--border)
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
return 0
;;
--border-label-pos|--preview-label-pos)
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
return 0 return 0
;; ;;
esac esac
@@ -170,9 +297,9 @@ __fzf_generic_path_completion() {
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" eval "base=$base" 2> /dev/null || return
dir= dir=
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
@@ -182,7 +309,7 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[[ -z "$dir" ]] && dir='.' [[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
printf "%q " "${item%$3}$3" printf "%q " "${item%$3}$3"
done) done)
matches=${matches% } matches=${matches% }
@@ -195,7 +322,7 @@ __fzf_generic_path_completion() {
printf '\e[5n' printf '\e[5n'
return 0 return 0
fi fi
dir=$(dirname "$dir") dir=$(command dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/ [[ "$dir" =~ /$ ]] || dir="$dir"/
done done
else else
@@ -229,16 +356,16 @@ _fzf_complete() {
fi fi
local cur selected trigger cmd post local cur selected trigger cmd post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | command awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post='command cat'
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ') selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then if [[ -n "$selected" ]]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -270,33 +397,68 @@ _fzf_complete_kill() {
} }
_fzf_proc_completion() { _fzf_proc_completion() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
_fzf_proc_completion_post() { _fzf_proc_completion_post() {
awk '{print $2}' command awk '{print $2}'
} }
# To use custom hostname lists, override __fzf_list_hosts.
# The function is expected to print hostnames, one per line as well as in the
# desired sorting and with any duplicates removed, to standard output.
#
# e.g.
# # Use bash-completionss _known_hosts_real() for getting the list of hosts
# __fzf_list_hosts() {
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
# local COMPREPLY=()
# _known_hosts_real ''
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
# }
if ! declare -F __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() {
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
}
fi
_fzf_host_completion() { _fzf_host_completion() {
_fzf_complete +m -- "$@" < <( _fzf_complete +m -- "$@" < <(__fzf_list_hosts)
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \ }
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | # Values for $1 $2 $3 are described here
awk '{if (length($2) > 0) {print $2}}' | sort -u # https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
) # > the first argument ($1) is the name of the command whose arguments are being completed,
# > the second argument ($2) is the word being completed,
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
_fzf_complete_ssh() {
case $3 in
-i|-F|-E)
_fzf_path_completion "$@"
;;
*)
local user=
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_var_completion() { _fzf_var_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
declare -xp | sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p' declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
) )
} }
_fzf_alias_completion() { _fzf_alias_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
alias | sed -En 's|^alias ([^=]+).*|\1|p' alias | command sed -En 's|^alias ([^=]+).*|\1|p'
) )
} }
@@ -309,7 +471,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds=" a_cmds="
awk cat diff diff3 awk bat cat 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
@@ -319,7 +481,7 @@ a_cmds="
svn tar unzip zip" svn tar unzip zip"
# Preserve existing completion # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
@@ -350,6 +512,9 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
# ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds unset cmd d_cmds a_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
@@ -373,9 +538,7 @@ _fzf_setup_completion() {
} }
# Environment variables / Aliases / Hosts / Process # Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset _fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' ssh telnet _fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill
fi

View File

@@ -9,6 +9,9 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
# __fzf_completion_options such that `eval $__fzf_completion_options` sets # __fzf_completion_options such that `eval $__fzf_completion_options` sets
# all options to the same values they currently have. We'll do just that at # all options to the same values they currently have. We'll do just that at
@@ -67,15 +70,12 @@ fi
# control. There are several others that could wreck havoc if they are set # control. There are several others that could wreck havoc if they are set
# to values we don't expect. With the following `emulate` command we # to values we don't expect. With the following `emulate` command we
# sidestep this issue entirely. # sidestep this issue entirely.
'emulate' 'zsh' '-o' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
# This brace is the start of try-always block. The `always` part is like # This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options. # `finally` in lesser languages. We use it to *always* restore user options.
{ {
# Bail out if not interactive shell.
[[ -o interactive ]] || return 0
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
@@ -137,7 +137,10 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
setopt localoptions nonomatch setopt localoptions nonomatch
eval "base=$base" if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
return
fi
eval "base=$base" 2> /dev/null || return
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [[ -z "$dir" || -d ${dir} ]]; then if [[ -z "$dir" || -d ${dir} ]]; then
@@ -145,7 +148,7 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
item="${item%$suffix}$suffix" item="${item%$suffix}$suffix"
echo -n "${(q)item} " echo -n "${(q)item} "
done) done)
@@ -215,21 +218,37 @@ _fzf_complete() {
command rm -f "$fifo" command rm -f "$fifo"
} }
_fzf_complete_telnet() { # To use custom hostname lists, override __fzf_list_hosts.
_fzf_complete +m -- "$@" < <( # The function is expected to print hostnames, one per line as well as in the
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | # desired sorting and with any duplicates removed, to standard output.
awk '{if (length($2) > 0) {print $2}}' | sort -u if ! declare -f __fzf_list_hosts > /dev/null; then
) __fzf_list_hosts() {
}
_fzf_complete_ssh() {
_fzf_complete +m -- "$@" < <(
setopt localoptions nonomatch setopt localoptions nonomatch
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \ command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
) }
fi
_fzf_complete_telnet() {
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
}
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
# The current word without the trigger is in the $prefix variable passed from the caller.
_fzf_complete_ssh() {
local tokens=(${(z)1})
case ${tokens[-1]} in
-i|-F|-E)
_fzf_path_completion "$prefix" "$1"
;;
*)
local user=
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_complete_export() { _fzf_complete_export() {
@@ -251,8 +270,9 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
@@ -292,6 +312,9 @@ fzf-completion() {
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
return
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd} > /dev/null"; then if eval "type _fzf_complete_${cmd} > /dev/null"; then

View File

@@ -11,15 +11,18 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd opts local cmd opts
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | command cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
eval "$cmd" | eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
while read -r item; do while read -r item; do
@@ -27,8 +30,6 @@ __fzf_select__() {
done done
} }
if [[ $- =~ i ]]; then
__fzfcmd() { __fzfcmd() {
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } && [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
@@ -42,19 +43,21 @@ fzf-file-widget() {
__fzf_cd__() { __fzf_cd__() {
local cmd opts dir local cmd opts dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}" -o -type d -print 2> /dev/null | command cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir" dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
} }
__fzf_history__() { if command -v perl > /dev/null; then
__fzf_history__() {
local output opts script local output opts script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
set +o pipefail
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}
@@ -63,7 +66,37 @@ __fzf_history__() {
else else
READLINE_POINT=0x7fffffff READLINE_POINT=0x7fffffff
fi fi
} }
else # awk - fallback for POSIX systems
__fzf_history__() {
local output opts script n x y z d
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next }
/^\t/ { P(b); b = substr($0, 2); next }
{ b = b RS $0 }
END { if (NR) P(b) }'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
fi
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind -m emacs-standard '"\er": redraw-current-line' bind -m emacs-standard '"\er": redraw-current-line'
@@ -79,7 +112,7 @@ if (( BASH_VERSINFO[0] < 4 )); then
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"' bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"' bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else else
@@ -98,5 +131,3 @@ fi
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi

View File

@@ -11,6 +11,9 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
status is-interactive; or exit 0
# Key bindings # Key bindings
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
@@ -22,17 +25,17 @@ function fzf_key_bindings
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3] set -l prefix $commandline[3]
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # "-path \$dir'*/.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden. # $dir itself, even if hidden.
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND " test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 's@^\./@@'" -o -type l -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
@@ -79,11 +82,11 @@ function fzf_key_bindings
set -l prefix $commandline[3] set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND " test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 's@^\./@@'" -o -type d -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]

View File

@@ -11,6 +11,9 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0
# Key bindings # Key bindings
# ------------ # ------------
@@ -32,21 +35,19 @@ else
} }
fi fi
'emulate' 'zsh' '-o' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{ {
[[ -o interactive ]] || return 0
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@@ -72,10 +73,10 @@ bindkey -M viins '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)" local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0

View File

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

129
src/actiontype_string.go Normal file
View File

@@ -0,0 +1,129 @@
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
package fzf
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[actIgnore-0]
_ = x[actStart-1]
_ = x[actClick-2]
_ = x[actInvalid-3]
_ = x[actChar-4]
_ = x[actMouse-5]
_ = x[actBeginningOfLine-6]
_ = x[actAbort-7]
_ = x[actAccept-8]
_ = x[actAcceptNonEmpty-9]
_ = x[actAcceptOrPrintQuery-10]
_ = x[actBackwardChar-11]
_ = x[actBackwardDeleteChar-12]
_ = x[actBackwardDeleteCharEof-13]
_ = x[actBackwardWord-14]
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleHeader-46]
_ = x[actTrack-47]
_ = x[actDown-48]
_ = x[actUp-49]
_ = x[actPageUp-50]
_ = x[actPageDown-51]
_ = x[actPosition-52]
_ = x[actHalfPageUp-53]
_ = x[actHalfPageDown-54]
_ = x[actOffsetUp-55]
_ = x[actOffsetDown-56]
_ = x[actJump-57]
_ = x[actJumpAccept-58]
_ = x[actPrintQuery-59]
_ = x[actRefreshPreview-60]
_ = x[actReplaceQuery-61]
_ = x[actToggleSort-62]
_ = x[actShowPreview-63]
_ = x[actHidePreview-64]
_ = x[actTogglePreview-65]
_ = x[actTogglePreviewWrap-66]
_ = x[actTransform-67]
_ = x[actTransformBorderLabel-68]
_ = x[actTransformHeader-69]
_ = x[actTransformPreviewLabel-70]
_ = x[actTransformPrompt-71]
_ = x[actTransformQuery-72]
_ = x[actPreview-73]
_ = x[actChangePreview-74]
_ = x[actChangePreviewWindow-75]
_ = x[actPreviewTop-76]
_ = x[actPreviewBottom-77]
_ = x[actPreviewUp-78]
_ = x[actPreviewDown-79]
_ = x[actPreviewPageUp-80]
_ = x[actPreviewPageDown-81]
_ = x[actPreviewHalfPageUp-82]
_ = x[actPreviewHalfPageDown-83]
_ = x[actPrevHistory-84]
_ = x[actPrevSelected-85]
_ = x[actPut-86]
_ = x[actNextHistory-87]
_ = x[actNextSelected-88]
_ = x[actExecute-89]
_ = x[actExecuteSilent-90]
_ = x[actExecuteMulti-91]
_ = x[actSigStop-92]
_ = x[actFirst-93]
_ = x[actLast-94]
_ = x[actReload-95]
_ = x[actReloadSync-96]
_ = x[actDisableSearch-97]
_ = x[actEnableSearch-98]
_ = x[actSelect-99]
_ = x[actDeselect-100]
_ = x[actUnbind-101]
_ = x[actRebind-102]
_ = x[actBecome-103]
_ = x[actResponse-104]
_ = x[actShowHeader-105]
_ = x[actHideHeader-106]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
}

View File

@@ -221,9 +221,9 @@ func charClassOfAscii(char rune) charClass {
return charUpper return charUpper
} else if char >= '0' && char <= '9' { } else if char >= '0' && char <= '9' {
return charNumber return charNumber
} else if strings.IndexRune(whiteChars, char) >= 0 { } else if strings.ContainsRune(whiteChars, char) {
return charWhite return charWhite
} else if strings.IndexRune(delimiterChars, char) >= 0 { } else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter return charDelimiter
} }
return charNonWord return charNonWord
@@ -240,7 +240,7 @@ func charClassOfNonAscii(char rune) charClass {
return charLetter return charLetter
} else if unicode.IsSpace(char) { } else if unicode.IsSpace(char) {
return charWhite return charWhite
} else if strings.IndexRune(delimiterChars, char) >= 0 { } else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter return charDelimiter
} }
return charNonWord return charNonWord

View File

@@ -351,9 +351,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0 var delimiter byte = 0
count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 { if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
count++
switch state256 { switch state256 {
case 0: case 0:
switch num { switch num {
@@ -381,10 +383,19 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.attr = state.attr | tui.Reverse state.attr = state.attr | tui.Reverse
case 9: case 9:
state.attr = state.attr | tui.StrikeThrough state.attr = state.attr | tui.StrikeThrough
case 22:
state.attr = state.attr &^ tui.Bold
state.attr = state.attr &^ tui.Dim
case 23: // tput rmso case 23: // tput rmso
state.attr = state.attr &^ tui.Italic state.attr = state.attr &^ tui.Italic
case 24: // tput rmul case 24: // tput rmul
state.attr = state.attr &^ tui.Underline state.attr = state.attr &^ tui.Underline
case 25:
state.attr = state.attr &^ tui.Blink
case 27:
state.attr = state.attr &^ tui.Reverse
case 29:
state.attr = state.attr &^ tui.StrikeThrough
case 0: case 0:
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
@@ -426,6 +437,13 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
} }
} }
// Empty sequence: reset
if count == 0 {
state.fg = -1
state.bg = -1
state.attr = 0
}
if state256 > 0 { if state256 > 0 {
*ptr = -1 *ptr = -1
} }

View File

@@ -348,6 +348,9 @@ func TestAnsiCodeStringConversion(t *testing.T) {
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "") assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m") assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m") assert("\x1b[41m", nil, "\x1b[39;41m")

View File

@@ -58,9 +58,9 @@ var defaultCommand string
func init() { func init() {
if !util.IsWindows() { if !util.IsWindows() {
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
} else if os.Getenv("TERM") == "cygwin" { } else if os.Getenv("TERM") == "cygwin" {
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
} }
} }

View File

@@ -138,7 +138,9 @@ func Run(opts *Options, version string, revision string) {
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)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -198,7 +200,7 @@ func Run(opts *Options, version string, revision string) {
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
if heightUnknown { if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad(opts) maxFit, padHeight = terminal.MaxFitAndPad()
} }
deferred := opts.Select1 || opts.Exit0 deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()
@@ -209,8 +211,6 @@ func Run(opts *Options, version string, revision string) {
// Event coordination // Event coordination
reading := true reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
@@ -219,6 +219,7 @@ func Run(opts *Options, version string, revision string) {
determine := func(final bool) { determine := func(final bool) {
if heightUnknown { if heightUnknown {
if total >= maxFit || final { if total >= maxFit || final {
deferred = false
heightUnknown = false heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight} terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
} }
@@ -230,26 +231,20 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false useSnapshot := false
var snapshot []*Chunk var snapshot []*Chunk
var prevSnapshot []*Chunk
var count int var count int
restart := func(command string) { restart := func(command string) {
reading = true reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
// We should not update snapshot if reload is triggered again while
// the previous reload is in progress
if useSnapshot && prevSnapshot != nil {
snapshot, count = chunkList.Snapshot()
}
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command) go reader.restart(command)
} }
for { for {
delay := true delay := true
ticks++ ticks++
input := func(reloaded bool) []rune { input := func() []rune {
reloaded := snapshotRevision != inputRevision
paused, input := terminal.Input() paused, input := terminal.Input()
if reloaded && paused { if reloaded && paused {
query = []rune{} query = []rune{}
@@ -279,29 +274,30 @@ func Run(opts *Options, version string, revision string) {
} }
if useSnapshot && evt == EvtReadFin { if useSnapshot && evt == EvtReadFin {
useSnapshot = false useSnapshot = false
prevSnapshot = nil
} }
if !useSnapshot { if !useSnapshot {
snapshot, count = chunkList.Snapshot() snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision
} }
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
opts.Sync = false opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
} }
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
} }
reset := !useSnapshot && clearCache() matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
var changed bool
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
changed = val.changed
if command != nil { if command != nil {
useSnapshot = val.sync useSnapshot = val.sync
} }
@@ -313,13 +309,20 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
restart(*command) restart(*command)
} }
}
if !changed {
break break
} }
if !useSnapshot { if !useSnapshot {
snapshot, _ = chunkList.Snapshot() newSnapshot, _ := chunkList.Snapshot()
// We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed
if command == nil || len(newSnapshot) > 0 {
snapshot = newSnapshot
snapshotRevision = inputRevision
} }
reset := !useSnapshot && clearCache() }
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset) matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -359,7 +362,7 @@ func Run(opts *Options, version string, revision string) {
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val, clearSelection()) terminal.UpdateList(val)
} }
} }
} }

View File

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

View File

@@ -1,7 +1,6 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os" "os"
"runtime" "runtime"
"testing" "testing"
@@ -25,7 +24,7 @@ func TestHistory(t *testing.T) {
} }
} }
f, _ := ioutil.TempFile("", "fzf-history") f, _ := os.CreateTemp("", "fzf-history")
f.Close() f.Close()
{ // Append lines { // Append lines

View File

@@ -16,7 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool revision int
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -29,6 +29,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int
} }
const ( const (
@@ -38,7 +39,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
@@ -48,7 +49,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: partitions, partitions: partitions,
slab: make([]*util.Slab, partitions), slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger),
revision: revision}
} }
// Loop puts Matcher in action // Loop puts Matcher in action
@@ -70,8 +72,9 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort || request.clearCache { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
} }
@@ -140,11 +143,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
numChunks := len(request.chunks) numChunks := len(request.chunks)
if numChunks == 0 { if numChunks == 0 {
return EmptyMerger, false return EmptyMerger(request.revision), false
} }
pattern := request.pattern pattern := request.pattern
if pattern.IsEmpty() { if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
@@ -218,11 +221,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac), false return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -231,5 +234,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
} }

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -32,6 +33,7 @@ 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
--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
@@ -55,13 +57,15 @@ const usage = `usage: fzf [options]
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calcalated as the terminal height
minus the given value.
If prefixed with '~', fzf will determine the height If prefixed with '~', fzf will determine the height
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)
--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|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
@@ -70,10 +74,11 @@ const usage = `usage: fzf [options]
(default: 0 or center) (default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (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) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR] --info=STYLE Finder info style
[default|right|hidden|inline[:SEPARATOR]|inline-right]
--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[=CHAR] Scrollbar character --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: '>')
@@ -115,13 +120,15 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--listen=HTTP_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)
--version Display version information and exit --version Display version information and exit
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 FZF_DEFAULT_OPTS Default options
(e.g. '--layout=reverse --inline-info') (e.g. '--layout=reverse --inline-info')
FZF_API_KEY X-API-Key header for HTTP server (--listen)
` `
@@ -152,6 +159,7 @@ type heightSpec struct {
size float64 size float64
percent bool percent bool
auto bool auto bool
inverse bool
} }
type sizeSpec struct { type sizeSpec struct {
@@ -163,6 +171,14 @@ func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{} return [4]sizeSpec{}
} }
type trackOption int
const (
trackDisabled trackOption = iota
trackEnabled
trackCurrent
)
type windowPosition int type windowPosition int
const ( const (
@@ -184,10 +200,16 @@ type infoStyle int
const ( const (
infoDefault infoStyle = iota infoDefault infoStyle = iota
infoRight
infoInline infoInline
infoInlineRight
infoHidden infoHidden
) )
func (s infoStyle) noExtraLine() bool {
return s == infoInline || s == infoInlineRight || s == infoHidden
}
type labelOpts struct { type labelOpts struct {
label string label string
column int column int
@@ -265,6 +287,7 @@ type Options struct {
WithNth []Range WithNth []Range
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Track trackOption
Tac bool Tac bool
Criteria []criterion Criteria []criterion
Multi int Multi int
@@ -315,7 +338,8 @@ type Options struct {
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Tabstop int Tabstop int
ListenPort int ListenAddr *listenAddress
Unsafe bool
ClearOnExit bool ClearOnExit bool
Version bool Version bool
} }
@@ -337,6 +361,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Track: trackDisabled,
Tac: false, Tac: false,
Criteria: []criterion{byScore, byLength}, Criteria: []criterion{byScore, byLength},
Multi: 0, Multi: 0,
@@ -384,6 +409,7 @@ func defaultOptions() *Options {
Tabstop: 8, Tabstop: 8,
BorderLabel: labelOpts{}, BorderLabel: labelOpts{},
PreviewLabel: labelOpts{}, PreviewLabel: labelOpts{},
Unsafe: false,
ClearOnExit: true, ClearOnExit: true,
Version: false} Version: false}
} }
@@ -532,6 +558,10 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderSharp return tui.BorderSharp
case "bold": case "bold":
return tui.BorderBold return tui.BorderBold
case "block":
return tui.BorderBlock
case "thinblock":
return tui.BorderThinBlock
case "double": case "double":
return tui.BorderDouble return tui.BorderDouble
case "horizontal": case "horizontal":
@@ -552,7 +582,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
if optional && str == "" { if optional && str == "" {
return tui.DefaultBorderShape return tui.DefaultBorderShape
} }
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)") errorExit("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
} }
return tui.BorderNone return tui.BorderNone
} }
@@ -600,6 +630,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.BSpace) add(tui.BSpace)
case "ctrl-space": case "ctrl-space":
add(tui.CtrlSpace) add(tui.CtrlSpace)
case "ctrl-delete":
add(tui.CtrlDelete)
case "ctrl-^", "ctrl-6": case "ctrl-^", "ctrl-6":
add(tui.CtrlCaret) add(tui.CtrlCaret)
case "ctrl-/", "ctrl-_": case "ctrl-/", "ctrl-_":
@@ -618,6 +650,10 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.Load) add(tui.Load)
case "focus": case "focus":
add(tui.Focus) add(tui.Focus)
case "one":
add(tui.One)
case "zero":
add(tui.Zero)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
@@ -666,12 +702,30 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.SLeft) add(tui.SLeft)
case "shift-right": case "shift-right":
add(tui.SRight) add(tui.SRight)
case "shift-delete":
add(tui.SDelete)
case "left-click": case "left-click":
add(tui.LeftClick) add(tui.LeftClick)
case "right-click": case "right-click":
add(tui.RightClick) add(tui.RightClick)
case "shift-left-click":
add(tui.SLeftClick)
case "shift-right-click":
add(tui.SRightClick)
case "double-click": case "double-click":
add(tui.DoubleClick) add(tui.DoubleClick)
case "scroll-up":
add(tui.ScrollUp)
case "scroll-down":
add(tui.ScrollDown)
case "shift-scroll-up":
add(tui.SScrollUp)
case "shift-scroll-down":
add(tui.SScrollDown)
case "preview-scroll-up":
add(tui.PreviewScrollUp)
case "preview-scroll-down":
add(tui.PreviewScrollDown)
case "f10": case "f10":
add(tui.F10) add(tui.F10)
case "f11": case "f11":
@@ -872,10 +926,14 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
mergeAttr(&theme.CurrentMatch) mergeAttr(&theme.CurrentMatch)
case "border": case "border":
mergeAttr(&theme.Border) mergeAttr(&theme.Border)
case "preview-border":
mergeAttr(&theme.PreviewBorder)
case "separator": case "separator":
mergeAttr(&theme.Separator) mergeAttr(&theme.Separator)
case "scrollbar": case "scrollbar":
mergeAttr(&theme.Scrollbar) mergeAttr(&theme.Scrollbar)
case "preview-scrollbar":
mergeAttr(&theme.PreviewScrollbar)
case "label": case "label":
mergeAttr(&theme.BorderLabel) mergeAttr(&theme.BorderLabel)
case "preview-label": case "preview-label":
@@ -921,7 +979,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?: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|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1015,6 +1073,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actAccept) appendAction(actAccept)
case "accept-non-empty": case "accept-non-empty":
appendAction(actAcceptNonEmpty) appendAction(actAcceptNonEmpty)
case "accept-or-print-query":
appendAction(actAcceptOrPrintQuery)
case "print-query": case "print-query":
appendAction(actPrintQuery) appendAction(actPrintQuery)
case "refresh-preview": case "refresh-preview":
@@ -1026,7 +1086,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "backward-delete-char": case "backward-delete-char":
appendAction(actBackwardDeleteChar) appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof": case "backward-delete-char/eof":
appendAction(actBackwardDeleteCharEOF) appendAction(actBackwardDeleteCharEof)
case "backward-word": case "backward-word":
appendAction(actBackwardWord) appendAction(actBackwardWord)
case "clear-screen": case "clear-screen":
@@ -1034,7 +1094,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "delete-char": case "delete-char":
appendAction(actDeleteChar) appendAction(actDeleteChar)
case "delete-char/eof": case "delete-char/eof":
appendAction(actDeleteCharEOF) appendAction(actDeleteCharEof)
case "deselect": case "deselect":
appendAction(actDeselect) appendAction(actDeselect)
case "end-of-line": case "end-of-line":
@@ -1077,6 +1137,16 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleAll) appendAction(actToggleAll)
case "toggle-search": case "toggle-search":
appendAction(actToggleSearch) appendAction(actToggleSearch)
case "toggle-track":
appendAction(actToggleTrack)
case "toggle-header":
appendAction(actToggleHeader)
case "show-header":
appendAction(actShowHeader)
case "hide-header":
appendAction(actHideHeader)
case "track":
appendAction(actTrack)
case "select": case "select":
appendAction(actSelect) appendAction(actSelect)
case "select-all": case "select-all":
@@ -1111,12 +1181,20 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actPrevSelected) appendAction(actPrevSelected)
case "next-selected": case "next-selected":
appendAction(actNextSelected) appendAction(actNextSelected)
case "show-preview":
appendAction(actShowPreview)
case "hide-preview":
appendAction(actHidePreview)
case "toggle-preview": case "toggle-preview":
appendAction(actTogglePreview) appendAction(actTogglePreview)
case "toggle-preview-wrap": case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap) appendAction(actTogglePreviewWrap)
case "toggle-sort": case "toggle-sort":
appendAction(actToggleSort) appendAction(actToggleSort)
case "offset-up":
appendAction(actOffsetUp)
case "offset-down":
appendAction(actOffsetDown)
case "preview-top": case "preview-top":
appendAction(actPreviewTop) appendAction(actPreviewTop)
case "preview-bottom": case "preview-bottom":
@@ -1139,7 +1217,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actDisableSearch) appendAction(actDisableSearch)
case "put": case "put":
if putAllowed { if putAllowed {
appendAction(actRune) appendAction(actChar)
} else { } else {
exit("unable to put non-printable character") exit("unable to put non-printable character")
} }
@@ -1167,6 +1245,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
switch t { switch t {
case actBecome:
if util.IsWindows() {
exit("become action is not supported on Windows")
}
case actUnbind, actRebind: case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -1219,6 +1301,8 @@ func isExecuteAction(str string) actionType {
prefix := actionNameRegexp.FindString(str) prefix := actionNameRegexp.FindString(str)
switch prefix { switch prefix {
case "become":
return actBecome
case "reload": case "reload":
return actReload return actReload
case "reload-sync": case "reload-sync":
@@ -1231,6 +1315,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-border-label": case "change-border-label":
return actChangeBorderLabel return actChangeBorderLabel
case "change-header":
return actChangeHeader
case "change-preview-label": case "change-preview-label":
return actChangePreviewLabel return actChangePreviewLabel
case "change-preview-window": case "change-preview-window":
@@ -1251,10 +1337,14 @@ func isExecuteAction(str string) actionType {
return actExecuteMulti return actExecuteMulti
case "put": case "put":
return actPut return actPut
case "transform":
return actTransform
case "transform-border-label": case "transform-border-label":
return actTransformBorderLabel return actTransformBorderLabel
case "transform-preview-label": case "transform-preview-label":
return actTransformPreviewLabel return actTransformPreviewLabel
case "transform-header":
return actTransformHeader
case "transform-prompt": case "transform-prompt":
return actTransformPrompt return actTransformPrompt
case "transform-query": case "transform-query":
@@ -1305,6 +1395,13 @@ func parseHeight(str string) heightSpec {
heightSpec.auto = true heightSpec.auto = true
str = str[1:] str = str[1:]
} }
if strings.HasPrefix(str, "-") {
if heightSpec.auto {
errorExit("negative(-) height is not compatible with adaptive(~) height")
}
heightSpec.inverse = true
str = str[1:]
}
size := parseSize(str, 100, "height") size := parseSize(str, 100, "height")
heightSpec.size = size.size heightSpec.size = size.size
@@ -1330,8 +1427,12 @@ func parseInfoStyle(str string) (infoStyle, string) {
switch str { switch str {
case "default": case "default":
return infoDefault, "" return infoDefault, ""
case "right":
return infoRight, ""
case "inline": case "inline":
return infoInline, defaultInfoSep return infoInline, defaultInfoSep
case "inline-right":
return infoInlineRight, ""
case "hidden": case "hidden":
return infoHidden, "" return infoHidden, ""
default: default:
@@ -1339,7 +1440,7 @@ func parseInfoStyle(str string) (infoStyle, string) {
if strings.HasPrefix(str, prefix) { if strings.HasPrefix(str, prefix) {
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ") return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
} }
errorExit("invalid info style (expected: default|hidden|inline|inline:SEPARATOR)") errorExit("invalid info style (expected: default|right|hidden|inline[:SEPARATOR]|inline-right)")
} }
return infoDefault, "" return infoDefault, ""
} }
@@ -1392,6 +1493,10 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
opts.border = tui.BorderSharp opts.border = tui.BorderSharp
case "border-bold": case "border-bold":
opts.border = tui.BorderBold opts.border = tui.BorderBold
case "border-block":
opts.border = tui.BorderBlock
case "border-thinblock":
opts.border = tui.BorderThinBlock
case "border-double": case "border-double":
opts.border = tui.BorderDouble opts.border = tui.BorderDouble
case "noborder", "border-none": case "noborder", "border-none":
@@ -1551,6 +1656,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i, 1) opts.Sort = optionalNumeric(allArgs, &i, 1)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--track":
opts.Track = trackEnabled
case "--no-track":
opts.Track = trackDisabled
case "--tac": case "--tac":
opts.Tac = true opts.Tac = true
case "--no-tac": case "--no-tac":
@@ -1744,10 +1853,21 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen": case "--listen", "--listen-unsafe":
opts.ListenPort = nextInt(allArgs, &i, "listen port required") given, str := optionalNextString(allArgs, &i)
case "--no-listen": addr := defaultListenAddr
opts.ListenPort = 0 if given {
var err error
err, addr = parseListenAddress(str)
if err != nil {
errorExit(err.Error())
}
}
opts.ListenAddr = &addr
opts.Unsafe = arg == "--listen-unsafe"
case "--no-listen", "--no-listen-unsafe":
opts.ListenAddr = nil
opts.Unsafe = false
case "--clear": case "--clear":
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
@@ -1838,7 +1958,19 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match { } else if match, value := optString(arg, "--listen="); match {
opts.ListenPort = atoi(value) err, addr := parseListenAddress(value)
if err != nil {
errorExit(err.Error())
}
opts.ListenAddr = &addr
opts.Unsafe = false
} else if match, value := optString(arg, "--listen-unsafe="); match {
err, addr := parseListenAddress(value)
if err != nil {
errorExit(err.Error())
}
opts.ListenAddr = &addr
opts.Unsafe = true
} else if match, value := optString(arg, "--hscroll-off="); match { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match { } else if match, value := optString(arg, "--scroll-off="); match {
@@ -1868,10 +2000,6 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer") errorExit("tab stop must be a positive integer")
} }
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
errorExit("invalid listen port")
}
if len(opts.JumpLabels) == 0 { if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels") errorExit("empty jump labels")
} }
@@ -1912,9 +2040,17 @@ func postProcessOptions(opts *Options) {
errorExit("--height option is currently not supported on this platform") errorExit("--height option is currently not supported on this platform")
} }
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 { if opts.Scrollbar != nil {
runes := []rune(*opts.Scrollbar)
if len(runes) > 2 {
errorExit("--scrollbar should be given one or two characters")
}
for _, r := range runes {
if runewidth.RuneWidth(r) != 1 {
errorExit("scrollbar display width should be 1") errorExit("scrollbar display width should be 1")
} }
}
}
// 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 {
@@ -1929,25 +2065,25 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action reordered := []*action{}
for _, act := range actions { for _, act := range actions {
switch act.t { switch act.t {
case actToggleSort: case actToggleSort:
// To display "+S"/"-S" on info line // To display "+S"/"-S" on info line
opts.ToggleSort = true opts.ToggleSort = true
case actChangePreviewWindow: case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
lastChangePreviewWindow = act reordered = append(reordered, act)
} }
} }
// Re-organize actions so that we only keep the last change-preview-window // Re-organize actions so that we put actions that change the preview window first in the list.
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20) // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {}) // -> change-preview-window(up,+10)+change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil { if len(reordered) > 0 {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions { for _, act := range actions {
if act.t != actChangePreviewWindow { switch act.t {
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
default:
reordered = append(reordered, act) reordered = append(reordered, act)
} }
} }
@@ -1990,9 +2126,7 @@ func postProcessOptions(opts *Options) {
theme := opts.Theme theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr { boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c dup := c
if !theme.Colored { if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold
} else if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold dup.Attr |= tui.Bold
} }
return dup return dup

View File

@@ -2,7 +2,7 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
@@ -357,7 +357,7 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
f, _ := ioutil.TempFile("", "fzf-history") f, _ := os.CreateTemp("", "fzf-history")
f.Close() f.Close()
hist := "--history=" + f.Name() hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlN, actNextHistory)

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") unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
} }

View File

@@ -6,6 +6,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -98,8 +99,17 @@ func (r *Reader) ReadSource() {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if util.IsTty() {
// The default command for *nix requires bash // The default command for *nix requires a shell that supports "pipefail"
// https://unix.stackexchange.com/a/654932/62171
shell := "bash" shell := "bash"
currentShell := os.Getenv("SHELL")
currentShellName := path.Base(currentShell)
for _, shellName := range []string{"bash", "zsh", "ksh", "ash", "hush", "mksh", "yash"} {
if currentShellName == shellName {
shell = currentShell
break
}
}
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
if defaultCommand != "" { if defaultCommand != "" {

View File

@@ -80,7 +80,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
} }
} }
} }

View File

@@ -3,30 +3,102 @@ package fzf
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/subtle"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
var getRegex *regexp.Regexp
func init() {
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
}
type getParams struct {
limit int
offset int
}
const ( const (
crlf = "\r\n" crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpReadTimeout = 10 * time.Second httpReadTimeout = 10 * time.Second
maxContentLength = 1024 * 1024 maxContentLength = 1024 * 1024
) )
func startHttpServer(port int, channel chan []*action) error { type httpServer struct {
apiKey []byte
actionChannel chan []*action
responseChannel chan string
}
type listenAddress struct {
host string
port int
}
func (addr listenAddress) IsLocal() bool {
return addr.host == "localhost" || addr.host == "127.0.0.1"
}
var defaultListenAddr = listenAddress{"localhost", 0}
func parseListenAddress(address string) (error, listenAddress) {
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
if len(parts) != 2 {
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
}
portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
}
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
return nil, listenAddress{parts[0], port}
}
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
host := address.host
port := address.port
apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 {
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
}
addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr)
if err != nil {
return fmt.Errorf("failed to listen on %s", addrStr), port
}
if port == 0 { if port == 0 {
return nil addr := listener.Addr().String()
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return err, port
}
} }
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) server := httpServer{
if err != nil { apiKey: []byte(apiKey),
return fmt.Errorf("port not available: %d", port) actionChannel: actionChannel,
responseChannel: responseChannel,
} }
go func() { go func() {
@@ -39,13 +111,13 @@ func startHttpServer(port int, channel chan []*action) error {
continue continue
} }
} }
conn.Write([]byte(handleHttpRequest(conn, channel))) conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close() conn.Close()
} }
listener.Close() listener.Close()
}() }()
return nil return nil, port
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http
@@ -54,12 +126,22 @@ func startHttpServer(port int, channel chan []*action) error {
// * No --listen: 2.8MB // * No --listen: 2.8MB
// * --listen with net/http: 5.7MB // * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB // * --listen w/o net/http: 3.3MB
func handleHttpRequest(conn net.Conn, channel chan []*action) string { func (server *httpServer) handleHttpRequest(conn net.Conn) string {
contentLength := 0 contentLength := 0
apiKey := ""
body := "" body := ""
bad := func(message string) string { answer := func(code string, message string) string {
message += "\n" message += "\n"
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message) return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
unauthorized := func(message string) string {
return answer(httpUnauthorized, message)
}
bad := func(message string) string {
return answer(httpBadRequest, message)
}
good := func(message string) string {
return answer(httpOk+"Content-Type: application/json"+crlf, message)
} }
conn.SetReadDeadline(time.Now().Add(httpReadTimeout)) conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
@@ -80,7 +162,12 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
text := scanner.Text() text := scanner.Text()
switch section { switch section {
case 0: case 0:
if !strings.HasPrefix(text, "POST / HTTP") { getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
response := <-server.responseChannel
return good(response)
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method") return bad("invalid request method")
} }
section++ section++
@@ -93,18 +180,27 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
continue continue
} }
pair := strings.SplitN(text, ":", 2) pair := strings.SplitN(text, ":", 2)
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" { if len(pair) == 2 {
switch strings.ToLower(pair[0]) {
case "content-length":
length, err := strconv.Atoi(strings.TrimSpace(pair[1])) length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
if err != nil || length <= 0 || length > maxContentLength { if err != nil || length <= 0 || length > maxContentLength {
return bad("invalid content length") return bad("invalid content length")
} }
contentLength = length contentLength = length
case "x-api-key":
apiKey = strings.TrimSpace(pair[1])
}
} }
case 2: case 2:
body += text body += text
} }
} }
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
return unauthorized("invalid api key")
}
if len(body) < contentLength { if len(body) < contentLength {
return bad("incomplete request") return bad("incomplete request")
} }
@@ -121,6 +217,28 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
return bad("no action specified") return bad("no action specified")
} }
channel <- actions server.actionChannel <- actions
return httpOk return httpOk + crlf
}
func parseGetParams(query string) getParams {
params := getParams{limit: 100, offset: 0}
for _, pair := range strings.Split(query, "&") {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
switch parts[0] {
case "limit":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.limit = val
}
case "offset":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.offset = val
}
}
}
}
return params
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,20 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
delimiter: delimiter,
printsep: printsep,
forcePlus: forcePlus,
query: query,
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
})
}
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1} items1 := []*Item{item1, item1}
@@ -52,90 +66,90 @@ func TestReplacePlaceholder(t *testing.T) {
*/ */
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus // forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter // Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.O}}") checkFormat("echo {{.O}} foo{{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}}bar baz{{.O}}") checkFormat("echo {{.O}}bar baz{{.O}}")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter // Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`) regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}}{{.I}}{{.O}}") checkFormat("echo {{.O}}{{.I}}{{.O}}")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
// No match // No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter // String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter // Regex delimiter
regex = regexp.MustCompile("[oa]+") regex = regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/* /*
@@ -155,7 +169,6 @@ func TestReplacePlaceholder(t *testing.T) {
newItem("7a 7b 7c 7d 7e 7f"), newItem("7a 7b 7c 7d 7e 7f"),
} }
stripAnsi := false stripAnsi := false
printsep = "\n"
forcePlus := false forcePlus := false
query := "sample query" query := "sample query"
@@ -198,18 +211,23 @@ func TestReplacePlaceholder(t *testing.T) {
// query flag is not removed after parsing, so it gets doubled // query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes // while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
// IV. escaping placeholder // IV. escaping placeholder
templateToOutput[`\{}`] = `{}` templateToOutput[`\{}`] = `{}`
templateToOutput[`\{q}`] = `{q}`
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
templateToOutput[`\{++}`] = `{++}` templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`] templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput { for giveTemplate, wantOutput := range templateToOutput {
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
checkFormat(wantOutput) checkFormat(wantOutput)
} }
for giveTemplate, wantOutput := range templateToFile { for giveTemplate, wantOutput := range templateToFile {
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
data, err := readFile(path) data, err := readFile(path)
if err != nil { if err != nil {
@@ -563,7 +581,7 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases // evaluate the test cases
for idx, test := range tests { for idx, test := range tests {
gotOutput := replacePlaceholder( gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query, test.give.query,
test.give.allItems) test.give.allItems)
@@ -605,7 +623,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
if flags.file { if flags.file {
encoded += "f" encoded += "f"
} }
if flags.query { if flags.forceUpdate { // FIXME
encoded += "q" encoded += "q"
} }
return encoded return encoded

View File

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

View File

@@ -33,12 +33,15 @@ func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) PassThrough(string) {}
func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false } func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }

View File

@@ -31,21 +31,31 @@ const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) PassThrough(str string) {
r.stderrInternal(str, true) r.queued.WriteString("\x1b7" + str + "\x1b8")
} }
// FIXME: Need better handling of non-displayable characters func (r *LightRenderer) stderr(str string) {
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { r.stderrInternal(str, true, "")
}
const CR string = "\x1b[2m␍"
const LF string = "\x1b[2m␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
runes := []rune{} runes := []rune{}
for len(bytes) > 0 { for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes) r, sz := utf8.DecodeRune(bytes)
nlcr := r == '\n' || r == '\r' nlcr := r == '\n' || r == '\r'
if r >= 32 || r == '\x1b' || nlcr { if r >= 32 || r == '\x1b' || nlcr {
if r == utf8.RuneError || nlcr && !allowNLCR { if nlcr && !allowNLCR {
runes = append(runes, ' ') if r == '\r' {
runes = append(runes, []rune(CR+resetCode)...)
} else { } else {
runes = append(runes, []rune(LF+resetCode)...)
}
} else if r != utf8.RuneError {
runes = append(runes, r) runes = append(runes, r)
} }
} }
@@ -54,8 +64,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
r.queued.WriteString(string(runes)) r.queued.WriteString(string(runes))
} }
func (r *LightRenderer) csi(code string) { func (r *LightRenderer) csi(code string) string {
r.stderr("\x1b[" + code) fullcode := "\x1b[" + code
r.stderr(fullcode)
return fullcode
} }
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
@@ -390,7 +402,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{F3, 0, nil} return Event{F3, 0, nil}
case 'S': case 'S':
return Event{F4, 0, nil} return Event{F4, 0, nil}
case '1', '2', '3', '4', '5', '6': case '1', '2', '3', '4', '5', '6', '7', '8':
if len(r.buffer) < 4 { if len(r.buffer) < 4 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -422,13 +434,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
if r.buffer[3] == '~' {
return Event{Del, 0, nil} return Event{Del, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{SDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
return Event{PgUp, 0, nil} return Event{PgUp, 0, nil}
case '6': case '6':
return Event{PgDn, 0, nil} return Event{PgDn, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
return Event{End, 0, nil}
case '1': case '1':
switch r.buffer[3] { switch r.buffer[3] {
case '~': case '~':
@@ -696,6 +724,10 @@ func (r *LightRenderer) Close() {
r.restoreTerminal() r.restoreTerminal()
} }
func (r *LightRenderer) Top() int {
return r.yoffset
}
func (r *LightRenderer) MaxX() int { func (r *LightRenderer) MaxX() int {
return r.width return r.width
} }
@@ -731,13 +763,17 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
return w return w
} }
func (w *LightWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *LightWindow) DrawHBorder() { func (w *LightWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }
func (w *LightWindow) drawBorder(onlyHorizontal bool) { func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape { switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
w.drawBorderAround(onlyHorizontal) w.drawBorderAround(onlyHorizontal)
case BorderHorizontal: case BorderHorizontal:
w.drawBorderHorizontal(true, true) w.drawBorderHorizontal(true, true)
@@ -768,14 +804,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.horizontal) hw := runewidth.RuneWidth(w.border.top)
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} }
if bottom { if bottom {
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} }
} }
@@ -791,11 +827,11 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0) w.Move(y, 0)
if left { if left {
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.left))
} }
w.CPrint(color, repeat(' ', width)) w.CPrint(color, repeat(' ', width))
if right { if right {
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.right))
} }
} }
} }
@@ -806,31 +842,31 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.horizontal) hw := runewidth.RuneWidth(w.border.top)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight) tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight) bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight)) w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal { if !onlyHorizontal {
vw := runewidth.RuneWidth(w.border.vertical) vw := runewidth.RuneWidth(w.border.left)
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2)) w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.right))
} }
} }
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
rem = (w.width - bcw) % hw rem = (w.width - bcw) % hw
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight)) w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
} }
func (w *LightWindow) csi(code string) { func (w *LightWindow) csi(code string) string {
w.renderer.csi(code) return w.renderer.csi(code)
} }
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
w.renderer.stderrInternal(str, allowNLCR) w.renderer.stderrInternal(str, allowNLCR, resetCode)
} }
func (w *LightWindow) Top() int { func (w *LightWindow) Top() int {
@@ -936,10 +972,10 @@ func colorCodes(fg Color, bg Color) []string {
return codes return codes
} }
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
w.csi(";" + strings.Join(codes, ";") + "m") code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0 return len(codes) > 0, code
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@@ -951,16 +987,17 @@ func cleanse(str string) string {
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
w.csi("m") w.csi("m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
if w.csiColor(fg, bg, attr) { hasColors, code := w.csiColor(fg, bg, attr)
if hasColors {
defer w.csi("m") defer w.csi("m")
} }
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
} }
type wrappedLine struct { type wrappedLine struct {
@@ -980,6 +1017,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
if len(rs) == 1 && rs[0] == '\t' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w) str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else { } else {
w = runewidth.StringWidth(str) w = runewidth.StringWidth(str)
} }
@@ -998,12 +1037,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
return lines return lines
} }
func (w *LightWindow) fill(str string, onMove func()) FillReturn { func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n") allLines := strings.Split(str, "\n")
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines { for j, wl := range lines {
w.stderrInternal(wl.text, false) w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth w.posx += wl.displayWidth
// Wrap line // Wrap line
@@ -1013,7 +1052,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
onMove() w.renderer.stderr(resetCode)
} }
} }
} }
@@ -1022,22 +1061,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
return FillSuspend return FillSuspend
} }
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
onMove() w.renderer.stderr(resetCode)
return FillNextLine return FillNextLine
} }
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() { func (w *LightWindow) setBg() string {
if w.bg != colDefault { if w.bg != colDefault {
w.csiColor(colDefault, w.bg, AttrRegular) _, code := w.csiColor(colDefault, w.bg, AttrRegular)
return code
} }
// Should clear dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
return "\x1b[m"
} }
func (w *LightWindow) Fill(text string) FillReturn { func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx) w.Move(w.posy, w.posx)
w.setBg() code := w.setBg()
return w.fill(text, w.setBg) return w.fill(text, code)
} }
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
@@ -1048,22 +1091,29 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }
if w.csiColor(fg, bg, attr) { if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
defer w.csi("m") defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) }) return w.fill(text, resetCode)
} }
return w.fill(text, w.setBg) return w.fill(text, w.setBg())
} }
func (w *LightWindow) FinishFill() { func (w *LightWindow) FinishFill() {
if w.posy < w.height {
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
}
for y := w.posy + 1; y < w.height; y++ { for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0) w.MoveAndClear(y, 0)
} }
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
w.drawBorder(false) w.DrawBorder()
// We don't erase the window here to avoid flickering during scroll w.Move(0, 0)
w.FinishFill()
w.Move(0, 0) w.Move(0, 0)
} }
func (w *LightWindow) EraseMaybe() bool {
return false
}

View File

@@ -10,6 +10,7 @@ import (
"syscall" "syscall"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/sys/unix"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -108,3 +109,11 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
return int(b[0]), true return int(b[0]), true
} }
func (r *LightRenderer) Size() TermSize {
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
if err != nil {
return TermSize{}
}
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
}

View File

@@ -110,16 +110,24 @@ func (r *LightRenderer) restoreTerminal() error {
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
} }
func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) Size() TermSize {
var w, h int
var bufferInfo windows.ConsoleScreenBufferInfo var bufferInfo windows.ConsoleScreenBufferInfo
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
r.width = getEnv("COLUMNS", defaultWidth) w = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) h = r.maxHeightFunc(getEnv("LINES", defaultHeight))
} else { } else {
r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left) w = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top)) h = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
} }
return TermSize{h, w, 0, 0}
}
func (r *LightRenderer) updateTerminalSize() {
size := r.Size()
r.width = size.Columns
r.height = size.Lines
} }
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {

View File

@@ -8,6 +8,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding" "github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
@@ -97,6 +98,11 @@ const (
AttrClear = Attr(1 << 8) AttrClear = Attr(1 << 8)
) )
func (r *FullscreenRenderer) PassThrough(str string) {
// No-op
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
}
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme { func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
@@ -166,6 +172,10 @@ func (r *FullscreenRenderer) Init() {
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
} }
func (r *FullscreenRenderer) Top() int {
return 0
}
func (r *FullscreenRenderer) MaxX() int { func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size() ncols, _ := _screen.Size()
return int(ncols) return int(ncols)
@@ -197,6 +207,12 @@ func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
// TODO: Pixel width and height not implemented
func (r *FullscreenRenderer) Size() TermSize {
cols, lines := _screen.Size()
return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
@@ -412,6 +428,12 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyHome: case tcell.KeyHome:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case tcell.KeyDelete: case tcell.KeyDelete:
if ctrl {
return Event{CtrlDelete, 0, nil}
}
if shift {
return Event{SDelete, 0, nil}
}
return Event{Del, 0, nil} return Event{Del, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
@@ -529,9 +551,15 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
w.drawBorder(false)
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ') fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
} }
func (w *TcellWindow) EraseMaybe() bool {
w.Erase()
return true
}
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) && return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height) y >= w.top && y < (w.top+w.height)
@@ -572,26 +600,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 { if len(rs) == 1 {
r := rs[0] r := rs[0]
if r < rune(' ') { // ignore control characters if r == '\r' {
continue st = style.Dim(true)
rs[0] = '␍'
} else if r == '\n' { } else if r == '\n' {
w.lastY++ st = style.Dim(true)
lx = 0 rs[0] = '␊'
continue } else if r < rune(' ') { // ignore control characters
} else if r == '\u000D' { // skip carriage return
continue continue
} }
} }
var xPos = w.left + w.lastX + lx var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) { if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style) _screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
} }
lx += runewidth.StringWidth(string(rs)) lx += util.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
} }
@@ -620,13 +649,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Italic(a&Attr(tcell.AttrItalic) != 0) Italic(a&Attr(tcell.AttrItalic) != 0)
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 && rs[0] == '\n' { if len(rs) == 1 {
r := rs[0]
switch r {
case '\r':
st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++ w.lastY++
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
continue continue Loop
}
} }
// word wrap: // word wrap:
@@ -643,8 +681,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
return FillSuspend return FillSuspend
} }
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style) _screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
lx += runewidth.StringWidth(string(rs)) lx += util.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
if w.lastX == w.width { if w.lastX == w.width {
@@ -670,6 +708,10 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a)) return w.fillString(str, NewColorPair(fg, bg, a))
} }
func (w *TcellWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *TcellWindow) DrawHBorder() { func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }
@@ -696,9 +738,9 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = w.normal.style() style = w.normal.style()
} }
hw := runewidth.RuneWidth(w.borderStyle.horizontal) hw := runewidth.RuneWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw max := right - 2*hw
if shape == BorderHorizontal || shape == BorderTop { if shape == BorderHorizontal || shape == BorderTop {
max = right - hw max = right - hw
@@ -709,36 +751,36 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
// ================== // ==================
// ( HH ) => TR is ignored // ( HH ) => TR is ignored
for x := left; x <= max; x += hw { for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) _screen.SetContent(x, top, w.borderStyle.top, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
max := right - 2*hw max := right - 2*hw
if shape == BorderHorizontal || shape == BorderBottom { if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw max = right - hw
} }
for x := left; x <= max; x += hw { for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) _screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
} }
} }
if !onlyHorizontal { if !onlyHorizontal {
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style) _screen.SetContent(left, y, w.borderStyle.left, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.vertical) vw := runewidth.RuneWidth(w.borderStyle.right)
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style) _screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
} }
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)

View File

@@ -3,7 +3,6 @@
package tui package tui
import ( import (
"io/ioutil"
"os" "os"
"syscall" "syscall"
) )
@@ -17,13 +16,17 @@ func ttyname() string {
} }
for _, prefix := range devPrefixes { for _, prefix := range devPrefixes {
files, err := ioutil.ReadDir(prefix) files, err := os.ReadDir(prefix)
if err != nil { if err != nil {
continue continue
} }
for _, file := range files { for _, file := range files {
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { info, err := file.Info()
if err != nil {
continue
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name() return prefix + file.Name()
} }
} }

View File

@@ -41,6 +41,7 @@ const (
CtrlZ CtrlZ
ESC ESC
CtrlSpace CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash CtrlBackSlash
@@ -54,6 +55,14 @@ const (
DoubleClick DoubleClick
LeftClick LeftClick
RightClick RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
BTab BTab
BSpace BSpace
@@ -74,6 +83,7 @@ const (
SDown SDown
SLeft SLeft
SRight SRight
SDelete
F1 F1
F2 F2
@@ -93,6 +103,8 @@ const (
Start Start
Load Load
Focus Focus
One
Zero
AltBS AltBS
@@ -272,6 +284,8 @@ type ColorTheme struct {
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
Border ColorAttr Border ColorAttr
PreviewBorder ColorAttr
PreviewScrollbar ColorAttr
BorderLabel ColorAttr BorderLabel ColorAttr
PreviewLabel ColorAttr PreviewLabel ColorAttr
} }
@@ -282,6 +296,15 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int
@@ -299,6 +322,8 @@ const (
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
BorderBlock
BorderThinBlock
BorderDouble BorderDouble
BorderHorizontal BorderHorizontal
BorderVertical BorderVertical
@@ -326,8 +351,10 @@ func (s BorderShape) HasTop() bool {
type BorderStyle struct { type BorderStyle struct {
shape BorderShape shape BorderShape
horizontal rune top rune
vertical rune bottom rune
left rune
right rune
topLeft rune topLeft rune
topRight rune topRight rune
bottomLeft rune bottomLeft rune
@@ -340,8 +367,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if !unicode { if !unicode {
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '-', top: '-',
vertical: '|', bottom: '-',
left: '|',
right: '|',
topLeft: '+', topLeft: '+',
topRight: '+', topRight: '+',
bottomLeft: '+', bottomLeft: '+',
@@ -352,8 +381,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
case BorderSharp: case BorderSharp:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '─', top: '─',
vertical: '', bottom: '',
left: '│',
right: '│',
topLeft: '┌', topLeft: '┌',
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
@@ -362,18 +393,54 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
case BorderBold: case BorderBold:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '━', top: '━',
vertical: '', bottom: '',
left: '┃',
right: '┃',
topLeft: '┏', topLeft: '┏',
topRight: '┓', topRight: '┓',
bottomLeft: '┗', bottomLeft: '┗',
bottomRight: '┛', bottomRight: '┛',
} }
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
// ▌ ▐
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
return BorderStyle{
shape: shape,
top: '▀',
bottom: '▄',
left: '▌',
right: '▐',
topLeft: '▛',
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
}
case BorderThinBlock:
// 🭽▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔🭾
// ▏ ▕
// 🭼▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁🭿
return BorderStyle{
shape: shape,
top: '▔',
bottom: '▁',
left: '▏',
right: '▕',
topLeft: '🭽',
topRight: '🭾',
bottomLeft: '🭼',
bottomRight: '🭿',
}
case BorderDouble: case BorderDouble:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '═', top: '═',
vertical: '', bottom: '',
left: '║',
right: '║',
topLeft: '╔', topLeft: '╔',
topRight: '╗', topRight: '╗',
bottomLeft: '╚', bottomLeft: '╚',
@@ -382,8 +449,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
} }
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '─', top: '─',
vertical: '', bottom: '',
left: '│',
right: '│',
topLeft: '╭', topLeft: '╭',
topRight: '╮', topRight: '╮',
bottomLeft: '╰', bottomLeft: '╰',
@@ -394,14 +463,23 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
func MakeTransparentBorder() BorderStyle { func MakeTransparentBorder() BorderStyle {
return BorderStyle{ return BorderStyle{
shape: BorderRounded, shape: BorderRounded,
horizontal: ' ', top: ' ',
vertical: ' ', bottom: ' ',
left: ' ',
right: ' ',
topLeft: ' ', topLeft: ' ',
topRight: ' ', topRight: ' ',
bottomLeft: ' ', bottomLeft: ' ',
bottomRight: ' '} bottomRight: ' '}
} }
type TermSize struct {
Lines int
Columns int
PxWidth int
PxHeight int
}
type Renderer interface { type Renderer interface {
Init() Init()
Resize(maxHeightFunc func(int) int) Resize(maxHeightFunc func(int) int)
@@ -411,13 +489,17 @@ type Renderer interface {
RefreshWindows(windows []Window) RefreshWindows(windows []Window)
Refresh() Refresh()
Close() Close()
PassThrough(string)
NeedScrollbarRedraw() bool NeedScrollbarRedraw() bool
GetChar() Event GetChar() Event
Top() int
MaxX() int MaxX() int
MaxY() int MaxY() int
Size() TermSize
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
} }
@@ -427,6 +509,7 @@ type Window interface {
Width() int Width() int
Height() int Height() int
DrawBorder()
DrawHBorder() DrawHBorder()
Refresh() Refresh()
FinishFill() FinishFill()
@@ -443,6 +526,7 @@ type Window interface {
Fill(text string) FillReturn Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn CFill(fg Color, bg Color, attr Attr, text string) FillReturn
Erase() Erase()
EraseMaybe() bool
} }
type FullscreenRenderer struct { type FullscreenRenderer struct {
@@ -492,6 +576,8 @@ var (
ColPreviewBorder ColorPair ColPreviewBorder ColorPair
ColBorderLabel ColorPair ColBorderLabel ColorPair
ColPreviewLabel ColorPair ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
ColPreviewSpinner ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
@@ -516,6 +602,8 @@ func EmptyTheme() *ColorTheme {
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: ColorAttr{colUndefined, AttrUndefined},
@@ -525,28 +613,30 @@ func EmptyTheme() *ColorTheme {
func NoColorTheme() *ColorTheme { func NoColorTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: false, Colored: false,
Input: ColorAttr{colDefault, AttrRegular}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrRegular}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrRegular}, Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrRegular}, DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrRegular}, Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline}, Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse}, Current: ColorAttr{colDefault, Reverse},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline}, CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Spinner: ColorAttr{colDefault, AttrRegular}, Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrRegular}, Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrRegular}, Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrRegular}, Selected: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrRegular}, Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrRegular}, Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrRegular}, BorderLabel: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrRegular}, Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrRegular}, PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrRegular}, PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrRegular}, Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrRegular}, PreviewBorder: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrRegular}, PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrRegular}, PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
} }
} }
@@ -577,6 +667,8 @@ func init() {
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: ColorAttr{colUndefined, AttrUndefined},
@@ -602,6 +694,8 @@ func init() {
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: ColorAttr{colUndefined, AttrUndefined},
@@ -627,6 +721,8 @@ func init() {
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: ColorAttr{colUndefined, AttrUndefined},
@@ -670,8 +766,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg) theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel) theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator) theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar) theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
initPalette(theme) initPalette(theme)
} }
@@ -709,5 +807,7 @@ func initPalette(theme *ColorTheme) {
ColBorderLabel = pair(theme.BorderLabel, theme.Bg) ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg) ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.Border, theme.PreviewBg) ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
} }

View File

@@ -11,6 +11,11 @@ import (
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
// StringWidth returns string width where each CR/LF character takes 1 column
func StringWidth(s string) int {
return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
}
// RunesWidth returns runes width // RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0 width := 0
@@ -22,8 +27,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
if len(rs) == 1 && rs[0] == '\t' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop w = tabstop - (prefixWidth+width)%tabstop
} else { } else {
s := string(rs) w = StringWidth(string(rs))
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
} }
width += w width += w
if width > limit { if width > limit {
@@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) {
gr := uniseg.NewGraphemes(input) gr := uniseg.NewGraphemes(input)
for gr.Next() { for gr.Next() {
rs := gr.Runes() rs := gr.Runes()
w := runewidth.StringWidth(string(rs)) w := StringWidth(string(rs))
if width+w > limit { if width+w > limit {
return runes, width return runes, width
} }

View File

@@ -70,7 +70,7 @@ func TestMin32(t *testing.T) {
} }
} }
func TestContrain(t *testing.T) { func TestConstrain(t *testing.T) {
if Constrain(-3, -1, 3) != -1 { if Constrain(-3, -1, 3) != -1 {
t.Error("Expected", -1) t.Error("Expected", -1)
} }
@@ -83,7 +83,7 @@ func TestContrain(t *testing.T) {
} }
} }
func TestContrain32(t *testing.T) { func TestConstrain32(t *testing.T) {
if Constrain32(-3, -1, 3) != -1 { if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1) t.Error("Expected", -1)
} }
@@ -184,3 +184,10 @@ func TestRepeatToFill(t *testing.T) {
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2]) t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
} }
} }
func TestStringWidth(t *testing.T) {
w := StringWidth("─")
if w != 1 {
t.Errorf("Expected: %d, Actual: %d", 1, w)
}
}

View File

@@ -6,6 +6,8 @@ import (
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
@@ -45,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(int(fd), b) return syscall.Read(int(fd), b)
} }
func SetStdin(file *os.File) {
unix.Dup2(int(file.Fd()), 0)
}

View File

@@ -81,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(syscall.Handle(fd), b) return syscall.Read(syscall.Handle(fd), b)
} }
func SetStdin(file *os.File) {
// No-op
}

View File

@@ -8,6 +8,7 @@ require 'shellwords'
require 'erb' require 'erb'
require 'tempfile' require 'tempfile'
require 'net/http' require 'net/http'
require 'json'
TEMPLATE = DATA.read TEMPLATE = DATA.read
UNSETS = %w[ UNSETS = %w[
@@ -16,6 +17,7 @@ UNSETS = %w[
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
FZF_ALT_C_COMMAND FZF_ALT_C_COMMAND
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
FZF_API_KEY
fish_history fish_history
].freeze ].freeze
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
@@ -180,7 +182,7 @@ class TestBase < Minitest::Test
end end
def writelines(path, lines) def writelines(path, lines)
File.unlink(path) while File.exist?(path) FileUtils.rm_f(path) while File.exist?(path)
File.open(path, 'w') { |f| f.puts lines } File.open(path, 'w') { |f| f.puts lines }
end end
@@ -188,7 +190,7 @@ class TestBase < Minitest::Test
wait { assert_path_exists tempname } wait { assert_path_exists tempname }
File.read(tempname) File.read(tempname)
ensure ensure
File.unlink(tempname) while File.exist?(tempname) FileUtils.rm_f(tempname) while File.exist?(tempname)
@temp_suffix += 1 @temp_suffix += 1
tmux.prepare tmux.prepare
end end
@@ -739,6 +741,12 @@ class TestGoFZF < TestBase
'xxoxxxxxxx', 'xxoxxxxxxx',
'xoxxxxxxxx' 'xoxxxxxxxx'
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true) ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
writelines(tempname, ['/bar/baz', '/foo/bar/baz'])
assert_equal [
'/foo/bar/baz',
'/bar/baz'
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
end end
def test_tiebreak_length_with_nth def test_tiebreak_length_with_nth
@@ -905,11 +913,7 @@ class TestGoFZF < TestBase
history_file = '/tmp/fzf-test-history' history_file = '/tmp/fzf-test-history'
# History with limited number of entries # History with limited number of entries
begin FileUtils.rm_f(history_file)
File.unlink(history_file)
rescue StandardError
nil
end
opts = "--history=#{history_file} --history-size=4" opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44] input = %w[00 11 22 33 44]
input.each do |keys| input.each do |keys|
@@ -955,7 +959,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 33', lines[-1] } tmux.until { |lines| assert_equal '> 33', lines[-1] }
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
File.unlink(history_file) FileUtils.rm_f(history_file)
end end
def test_execute def test_execute
@@ -984,11 +988,7 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_multi def test_execute_multi
@@ -1013,20 +1013,12 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_plus_flag def test_execute_plus_flag
output = tempname + '.tmp' output = tempname + '.tmp'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, ['foo bar', '123 456']) writelines(tempname, ['foo bar', '123 456'])
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
@@ -1059,21 +1051,13 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
rescue StandardError rescue StandardError
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_shell def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, writelines(tempname,
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"]) ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
system("chmod +x #{tempname}") system("chmod +x #{tempname}")
@@ -1087,11 +1071,7 @@ class TestGoFZF < TestBase
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true) assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_cycle def test_cycle
@@ -1240,6 +1220,39 @@ class TestGoFZF < TestBase
end end
end end
def test_toggle_header
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border", :Enter
before = <<~OUTPUT
4
> 3
2/2
>
2
1
foo
OUTPUT
tmux.until { assert_block(before, _1) }
tmux.send_keys :Space
after = <<~OUTPUT
4
> 3
2/2
>
OUTPUT
tmux.until { assert_block(after, _1) }
tmux.send_keys :Space
tmux.until { assert_block(before, _1) }
end
def test_cancel def test_cancel
tmux.send_keys "seq 10 | #{fzf('--bind 2:cancel')}", :Enter tmux.send_keys "seq 10 | #{fzf('--bind 2:cancel')}", :Enter
tmux.until { |lines| assert_equal ' 10/10', lines[-2] } tmux.until { |lines| assert_equal ' 10/10', lines[-2] }
@@ -1485,6 +1498,83 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' } tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
end end
def test_toggle_preview_without_default_preview_command
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '║ [1]'
end
# toggle-preview should do nothing
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[1], '║ [1]'
refute_includes lines[1], '║ [2]'
end
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '║ [3]'
end
# One-off preview action
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
# toggle-preview to hide it
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
# toggle-preview again does nothing
tmux.send_keys :Enter, :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '║ [4]'
end
end
def test_show_and_hide_preview
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
# Hidden by default
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '┃ [1]'
end
# Show
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
# Already shown
tmux.send_keys :a
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
# Hide
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '┃ [3]'
end
# Already hidden
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '┃ [4]'
end
# Show it again
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
end
def test_preview_hidden def test_preview_hidden
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
@@ -1497,11 +1587,7 @@ class TestGoFZF < TestBase
end end
def test_preview_size_0 def test_preview_size_0
begin FileUtils.rm_f(tempname)
File.unlink(tempname)
rescue StandardError
nil
end
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 100, lines.item_count assert_equal 100, lines.item_count
@@ -1526,6 +1612,32 @@ class TestGoFZF < TestBase
end end
end end
def test_preview_size_0_hidden
FileUtils.rm_f(tempname)
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_includes lines, '> 3' }
wait { refute_path_exists tempname }
tmux.send_keys :Space
wait do
assert_path_exists tempname
assert_equal %w[3], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Down
wait do
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_includes lines, '> 5' }
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines, '> 6' }
tmux.send_keys :Space
wait do
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
end
end
def test_preview_flags def test_preview_flags
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' | tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
@@ -1670,6 +1782,35 @@ class TestGoFZF < TestBase
assert_equal %w[foo], readonce.lines(chomp: true) assert_equal %w[foo], readonce.lines(chomp: true)
end end
def test_accept_or_print_query_without_match
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[99999], readonce.lines(chomp: true)
end
def test_accept_or_print_query_with_match
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys '^99$'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[99], readonce.lines(chomp: true)
end
def test_accept_or_print_query_with_multi_selection
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query --multi')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal 3, lines.select_count }
tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[1 2 3], readonce.lines(chomp: true)
end
def test_preview_update_on_select def test_preview_update_on_select
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter :Enter
@@ -1794,6 +1935,67 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines.last } tmux.until { |lines| assert_equal '>', lines.last }
end end
def test_change_and_transform_header
[
'space:change-header:$(seq 4)',
'space:transform-header:seq 4'
].each_with_index do |binding, i|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
next unless i.zero?
teardown
setup
end
end
def test_change_header
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_change_query def test_change_query
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
tmux.until { |lines| assert_equal 0, lines.item_count } tmux.until { |lines| assert_equal 0, lines.item_count }
@@ -1820,6 +2022,13 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> RAB', lines[-1] } tmux.until { |lines| assert_equal '> RAB', lines[-1] }
end end
def test_transform
tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter
tmux.until { |lines| assert_equal 'start', lines[-1] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 'up', lines[-1] }
end
def test_clear_selection def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count } tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -1859,7 +2068,7 @@ class TestGoFZF < TestBase
def test_keep_right def test_keep_right
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
tmux.until { |lines| assert lines.any_include?('9999 10000') } tmux.until { |lines| assert lines.any_include?('999910000') }
end end
def test_backward_eof def test_backward_eof
@@ -2087,11 +2296,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_default_command_on_accept def test_kill_default_command_on_accept
@@ -2109,11 +2314,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_abort def test_kill_reload_command_on_abort
@@ -2134,11 +2335,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_accept def test_kill_reload_command_on_accept
@@ -2158,11 +2355,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_preview_header def test_preview_header
@@ -2377,6 +2570,39 @@ class TestGoFZF < TestBase
end end
end end
def test_change_preview_window_rotate_hidden
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
"a:change-preview-window(nohidden||down,1|)'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_change_preview_window_rotate_hidden_down
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_ellipsis def test_ellipsis
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until { |lines| assert_equal 1, lines.match_count }
@@ -2479,12 +2705,16 @@ class TestGoFZF < TestBase
end end
def test_focus_event def test_focus_event
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]]),?:unbind(focus)"', :Enter
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') } tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') } tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
tmux.send_keys :X tmux.send_keys :X
tmux.until { |lines| assert_includes(lines[-1], '[[]]') } tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
tmux.send_keys '?'
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
end end
def test_labels_center def test_labels_center
@@ -2554,6 +2784,16 @@ class TestGoFZF < TestBase
tmux.until { assert(_1[-2] == ' 1/100') } tmux.until { assert(_1[-2] == ' 1/100') }
end end
def test_info_right
tmux.send_keys "#{FZF} --info=right --separator x --bind 'start:reload:seq 100; sleep 10'", :Enter
tmux.until { assert_match(%r{xxx [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-2]) }
end
def test_info_inline_right
tmux.send_keys "#{FZF} --info=inline-right --bind 'start:reload:seq 100; sleep 10'", :Enter
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
end
def test_prev_next_selected def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count } tmux.until { |lines| assert_equal 10, lines.item_count }
@@ -2574,11 +2814,49 @@ class TestGoFZF < TestBase
end end
def test_listen def test_listen
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter { '--listen 6266' => -> { URI('http://localhost:6266') },
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count } tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ') state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal 10, state[:totalCount]
assert_equal 10, state[:matchCount]
assert_empty state[:query]
assert_equal({ index: 0, text: '1' }, state[:current])
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] } tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal 100, state[:totalCount]
assert_equal 0, state[:matchCount]
assert_equal 'yo', state[:query]
assert_nil state[:current]
teardown
setup
end
end
def test_listen_with_api_key
post_uri = URI('http://localhost:6266')
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
# Incorrect API Key
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
end
# Valid API Key
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '200', res.code
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
end
end end
def test_toggle_alternative_preview_window def test_toggle_alternative_preview_window
@@ -2588,6 +2866,209 @@ class TestGoFZF < TestBase
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '/1/1/' } tmux.until { |lines| assert_includes lines, '/1/1/' }
end end
def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 99, lines.item_count }
end
def test_no_extra_newline_issue_3209
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
expected = <<~OUTPUT
something
3
2
> 1
100/100
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_track
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
index = tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 555'
end.index('> 555')
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys '555'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
assert_includes lines[-2], '+T'
end
tmux.send_keys 't'
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys 't'
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
end
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 55'
end
# Automatically disabled when the tracking item is no longer visible
tmux.send_keys '4'
tmux.until do |lines|
assert_equal 28, lines.match_count
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
end
def test_one_and_zero
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
tmux.send_keys '1'
tmux.until do |lines|
assert_equal 2, lines.match_count
refute(lines.any? { _1.include?('only match') })
refute(lines.any? { _1.include?('no match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert(lines.any? { _1.include?('only match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 0, lines.match_count
assert(lines.any? { _1.include?('no match') })
end
end
def test_height_range_with_exit_0
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.send_keys :c
tmux.until { |lines| assert_equal 0, lines.match_count }
end
def test_reload_and_change
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_reload_and_change_cache
tmux.send_keys "echo bar | #{FZF} --bind 'zero:change-header(foo)+reload(echo foo)+clear-query'", :Enter
expected = <<~OUTPUT
> bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :z
expected = <<~OUTPUT
> foo
foo
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_delete_with_modifiers
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'C-Delete'
tmux.until { |lines| assert_equal '[3]', lines[-1] }
tmux.send_keys 'S-Delete'
tmux.until { |lines| assert_equal '[2]', lines[-1] }
end
def test_become_tty
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
tmux.until { |lines| assert_includes lines, '/dev/tty' }
end
def test_disabled_preview_update
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
tmux.send_keys :x
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
end
def test_preview_window_hidden_on_focus
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' }
end
end end
module TestShell module TestShell
@@ -2716,9 +3197,9 @@ module TestShell
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar' tmux.send_keys 'foo bar'
tmux.until { |lines| assert lines[-3]&.end_with?('bar"') } tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') } tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] } tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end end
@@ -2933,6 +3414,34 @@ module CompletionTest
tmux.prepare tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter tmux.send_keys 'unset -f _fzf_comprun', :Enter
end end
def test_ssh_completion
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
tmux.send_keys 'ssh jg@localhost**', :Tab
tmux.until do |lines|
assert lines.match_count >= 1
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
tmux.until do |lines|
assert lines.match_count >= 5
assert_equal 0, lines.select_count
end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until do |lines|
assert_equal 3, lines.select_count
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
tmux.send_keys 'localhost**', :Tab
tmux.until do |lines|
assert lines.match_count >= 1
end
end
end end
class TestBash < TestBase class TestBash < TestBase

10
typos.toml Normal file
View File

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