Compare commits

...

127 Commits

Author SHA1 Message Date
Junegunn Choi
4e3f9854e6
Update README.md 2025-05-17 22:06:06 +09:00
Junegunn Choi
b27943423e
Show ellipsis for truncated labels
Close #4390
2025-05-17 11:25:15 +09:00
Junegunn Choi
894a1016bc
RuboCop lint 2025-05-17 11:20:29 +09:00
Junegunn Choi
efe6cddd34
Update README 2025-05-16 22:15:38 +09:00
Junegunn Choi
f1c6bdf3e8
Update README 2025-05-16 22:15:06 +09:00
Junegunn Choi
710659bcf5
Update SECURITY.md 2025-05-14 11:06:15 +09:00
Josef Andersson
be67775da4
Add initial security policy (#4379)
Signed-off-by: Josef Andersson <janderssonse@proton.me>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 11:05:20 +09:00
jiz4oh
2c6381499c
[neovim] Respect winborder of Neovim 0.11+ (#4389)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 00:53:26 +09:00
junegunn
4df842e78c Deploying to master from @ junegunn/fzf@b81696fb64 🚀 2025-05-11 00:02:09 +00:00
Ajeet D'Souza
b81696fb64
bash: set keybinding right before printing special character (#4377) 2025-05-10 15:29:27 +09:00
Junegunn Choi
d226d841a1
0.62.0 2025-05-04 18:31:18 +09:00
Junegunn Choi
c6d83047e5
Allow whitespace as separator in --color option 2025-05-04 15:08:23 +09:00
Junegunn Choi
46dabccdf1
[vim] Update g:fzf_colors example with 'query' 2025-05-04 14:52:22 +09:00
Junegunn Choi
cd9517b679
Add 'alt-bg' color for striped lines (#4370)
Test cases:

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

  go run main.go --bind load:jump

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

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

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

3. 'selected-bg' should take precedence

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

4. Should work with text with ANSI colors

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

---

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

Fix #4360
Fix #4359
2025-04-22 20:20:21 +09:00
Junegunn Choi
90ad1b7f22
0.61.2 2025-04-20 11:37:15 +09:00
Junegunn Choi
f22fbcd1af
Fix typo and update CHANGLOG 2025-04-20 11:31:15 +09:00
Junegunn Choi
1d761684c5
Add --tty-default=/dev/tty and --no-tty-default option (#4352)
Fix #4242.

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

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

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

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

For example,

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

The preview command becomes:

  echo 'foo   bar' foo   bar

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

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

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

Close #4330
2025-03-30 19:49:05 +09:00
Junegunn Choi
ba6d1b8772
Add change-ghost and transform-ghost 2025-03-28 23:35:20 +09:00
Junegunn Choi
0dce561ec9
Fix header window not updated on change-header 2025-03-28 23:23:43 +09:00
dependabot[bot]
376142eb0d
Bump github.com/charlievieth/fastwalk from 1.0.9 to 1.0.10 (#4307)
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.9 to 1.0.10.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.9...v1.0.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-28 23:05:34 +09:00
Junegunn Choi
664ee1f483
Add change-pointer and transform-pointer
Close #4178
2025-03-28 21:28:25 +09:00
Junegunn Choi
dac5b6fde1
Fix info not updated after track-current is disabled due to race condition 2025-03-26 16:00:05 +09:00
Junegunn Choi
998c57442b
Fix query precedence in an action chain (#4326)
When 'search' and any action that modifies the query are in an action
chain, anything that comes later takes precedence.
2025-03-26 15:47:43 +09:00
Junegunn Choi
4a0ab6c926
Improve query modification prevention in input-less mode
fzf would restore the original query in input-less mode after executing
a chain of actions.

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

Fix #4326
2025-03-26 10:34:52 +09:00
Junegunn Choi
f43e82f17f
Do not ignore current query when input is hidden
* The initial query given by --query should be respected
* The current query should still be respected after `hide-input`
  (or `toggle-input)

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

  export FZF_COMPLETION_PATH_OPTS="--prompt 'Hello World> '"
2025-03-17 18:12:26 +09:00
Junegunn Choi
779e3cc5b5
[vim] Use 24-bit colors on gvim even when &termguicolors is off
Close #2563
2025-03-17 17:46:56 +09:00
junegunn
3f3d1ef8f5 Deploying to master from @ junegunn/fzf@f92f9f137a 🚀 2025-03-16 00:02:19 +00:00
Junegunn Choi
f92f9f137a
Fix wrapping of the list section
# The first line of the second chunk would prematurely wrap
  printf '%0500s\n\n%0500s' 0 0 | fzf --wrap --read0
2025-03-16 01:57:20 +09:00
Junegunn Choi
87f7f436e8
Fix ghost text with inline info
Fix #4312
2025-03-15 18:42:08 +09:00
Junegunn Choi
4298c0b1eb
Add --ghost=TEXT to display a ghost text when the input is empty 2025-03-14 16:46:23 +09:00
Gabriel Marin
6c104d771e
Change 'interface{}' to 'any' (#4308) 2025-03-11 14:24:54 +09:00
Junegunn Choi
aefb9a5bc4
Nullify unwanted FZF_DEFAULT_* variables in tmux popup
Fix #4298
2025-03-10 18:18:50 +09:00
Junegunn Choi
8868d7cbb8
Add .idea to .gitignore 2025-03-10 18:15:53 +09:00
junegunn
10cbac20f9 Deploying to master from @ junegunn/fzf@26bcd0c90d 🚀 2025-03-09 00:01:51 +00:00
Junegunn Choi
26bcd0c90d
README: Sponsors ❤️ 2025-03-04 18:30:50 +09:00
Junegunn Choi
fbece2bb67
Update README 2025-03-04 17:43:02 +09:00
Junegunn Choi
0012183ede
0.60.3 2025-03-03 17:10:49 +09:00
Junegunn Choi
8916cbc6ab
[windows] Prevent fzf from consuming user input while paused
This partly fixes #4260.

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

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

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

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

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

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

1. during backward scan by "minim" pattern

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

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

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

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

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

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

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

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

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

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

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

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

Close #3987
Close #1323
2025-02-09 11:53:35 +09:00
Eric Chen
a1994ff0ab
Update README.md (#4225) 2025-02-09 09:19:15 +09:00
junegunn
ca0e858871 Deploying to master from @ junegunn/fzf@06c6615507 🚀 2025-02-09 00:02:24 +00:00
bitraid
06c6615507
[fish] Fix for directories with special characters (#4230)
Using CTRL-T or ALT-C when the current command line token contained a
directory with special characters, the script would fail to detect it.
For exampe, an existing directory named `it\'s\ a\ test`, instead of
using it as walker-root, it would use it as the query.
2025-02-08 22:18:05 +09:00
Junegunn Choi
818d0be436
Fix change-header-label+change-header
Fix #4227
2025-02-07 20:57:09 +09:00
Junegunn Choi
fcd2baa945
Fix scrolling performance when --wrap is enabled
Fix #4221
2025-02-06 22:30:39 +09:00
Junegunn Choi
62e0a2824a
Fix nth highlighting
Fix #4222
2025-02-06 19:57:39 +09:00
54 changed files with 1873 additions and 652 deletions

View File

@ -1,5 +1,5 @@
--- ---
name: Test fzf on Linux name: build
on: on:
push: push:

1
.gitignore vendored
View File

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

View File

@ -1,6 +1,127 @@
CHANGELOG CHANGELOG
========= =========
0.62.0
------
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
```sh
# seoul256-light
fzf --style full --color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'
```
- Added `alt-bg` color to create striped lines to visually separate rows
```sh
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi \
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
```
- [fish] Improvements in CTRL-R binding (@bitraid)
- You can trigger CTRL-R in the middle of a command to insert the selected item
- You can delete history items with SHIFT-DEL
- Bug fixes and improvements
- Fixed unnecessary 100ms delay after `reload` (#4364)
- Fixed `selected-bg` not applied to colored items (#4372)
0.61.3
------
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
- More environment variables for child processes (#4356)
0.61.2
------
- Fixed panic when using header border without pointer/marker (@phanen)
- Fixed `--tmux` option when already inside a tmux popup (@peikk0)
- Bug fixes and improvements in CTRL-T binding of fish (#4334) (@bitraid)
- Added `--no-tty-default` option to make fzf search for the current TTY device instead of defaulting to `/dev/tty` (#4242)
0.61.1
------
- Disable bracketed-paste mode on exit. This fixes issue where pasting breaks after running fzf on old bash versions that don't support the mode.
0.61.0
------
- Added `--ghost=TEXT` to display a ghost text when the input is empty
```sh
# Display "Type to search" when the input is empty
fzf --ghost "Type to search"
```
- Added `change-ghost` and `transform-ghost` actions for dynamically changing the ghost text
- Added `change-pointer` and `transform-pointer` actions for dynamically changing the pointer sign
- Added `r` flag for placeholder expression (raw mode) for unquoted output
- Bug fixes and improvements
0.60.3
------
- Bug fixes and improvements
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
- [walker] Append '/' to directory entries on MSYS2 (#4281)
- Trim trailing whitespaces after processing ANSI sequences (#4282)
- Remove temp files before `become` when using `--tmux` option (#4283)
- Fix condition for using item numlines cache (#4285) (@alex-huff)
- Make `--accept-nth` compatible with `--select-1` (#4287)
- Increase the query length limit from 300 to 1000 (#4292)
- [windows] Prevent fzf from consuming user input while paused (#4260)
0.60.2
------
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
- Thanks to @phanen for the fix
- Fixed 'jump' action when the pointer is an empty string
0.60.1
------
- Bug fixes and minor improvements
- Built-in walker now prints directory entries with a trailing slash
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
0.60.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
- Added `--accept-nth` for choosing output fields
```sh
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
# Becomes
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
git branch | fzf | cut -c3-
# Can be rewritten as
git branch | fzf --accept-nth -1
```
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
```sh
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
# foo, baz, bar
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
# foo,baz,bar,foo,bar
```
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
```sh
seq 100 | fzf --bind 'ctrl-x:exclude'
# 'exclude-multi' will exclude the selected items or the current item
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
```
- Preview window now prints wrap indicator when wrapping is enabled
```sh
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
```
- Bug fixes and improvements
0.59.0 0.59.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_ _Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_

View File

@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
let g:fzf_colors = let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'], \ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'], \ 'bg': ['bg', 'Normal'],
\ 'query': ['fg', 'Normal'],
\ 'hl': ['fg', 'Comment'], \ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],

File diff suppressed because one or more lines are too long

33
SECURITY.md Normal file
View File

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

View File

@ -57,15 +57,15 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1)) dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi fi
# 1. Use kitty icat on kitty terminal # 1. Use icat (from Kitty) if kitten is installed
if [[ $KITTY_WINDOW_ID ]]; then if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable, # 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'. # you have to use 'stream'.
# #
# 2. The last line of the output is the ANSI reset code without newline. # 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator. # 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. # So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/' kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output # 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then elif command -v chafa > /dev/null; then

6
go.mod
View File

@ -1,13 +1,13 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/charlievieth/fastwalk v1.0.9 github.com/charlievieth/fastwalk v1.0.10
github.com/gdamore/tcell/v2 v2.8.1 github.com/gdamore/tcell/v2 v2.8.1
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.29.0 golang.org/x/sys v0.30.0
golang.org/x/term v0.28.0 golang.org/x/term v0.29.0
) )
require ( require (

10
go.sum
View File

@ -1,5 +1,5 @@
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0= github.com/charlievieth/fastwalk v1.0.10 h1:0qUbvA2O+K+X+IrTfZTC0UH2DK5MOA+KjVfStAHUnGg=
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= github.com/charlievieth/fastwalk v1.0.10/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
@ -54,8 +54,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -64,8 +65,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@ -2,7 +2,7 @@
set -u set -u
version=0.59.0 version=0.62.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

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

View File

@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.59" var version = "0.62"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@ -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 "Feb 2025" "fzf 0.59.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "May 2025" "fzf 0.62.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

@ -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 "Feb 2025" "fzf 0.59.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "May 2025" "fzf 0.62.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -56,7 +56,9 @@ Case-insensitive match (default: smart-case match)
Case-sensitive match Case-sensitive match
.TP .TP
.B "\-\-smart\-case" .B "\-\-smart\-case"
Smart-case match (default) Smart-case match (default). In this mode, the search is case-insensitive by
default, but it becomes case-sensitive if the query contains any uppercase
letters.
.TP .TP
.B "\-\-literal" .B "\-\-literal"
Do not normalize latin script letters for matching. Do not normalize latin script letters for matching.
@ -117,8 +119,38 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
the original lines) because fzf doesn't allow searching against the hidden the original lines) because fzf doesn't allow searching against the hidden
fields. fields.
.TP .TP
.BI "\-\-with\-nth=" "N[,..]" .BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
Transform the presentation of each line using field index expressions Transform the presentation of each line using the field index expressions.
For advanced transformation, you can provide a template containing field index
expressions in curly braces. When you use a template, the trailing delimiter is
stripped from each expression, giving you more control over the output.
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
.RS
e.g.
# Single expression: drop the first field
echo foo bar baz | fzf --with-nth 2..
# Use template to rearrange fields
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
.RE
.TP
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
Define which fields to print on accept. The last delimiter is stripped from the
output. For advanced transformation, you can provide a template containing
field index expressions in curly braces. When you use a template, the trailing
delimiter is stripped from each expression, giving you more control over the
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
line.
.RS
e.g.
# Single expression
echo foo bar baz | fzf --accept-nth 2
# Template
echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
.RE
.TP .TP
.B "+s, \-\-no\-sort" .B "+s, \-\-no\-sort"
Do not sort the result Do not sort the result
@ -196,6 +228,13 @@ e.g. \fB# Avoid rendering both fzf instances at the same time
(sleep 1; seq 1000000; sleep 1) | (sleep 1; seq 1000000; sleep 1) |
fzf \-\-sync \-\-query 5 \-\-listen \-\-bind start:up,load:up,result:up,focus:change\-header:Ready\fR fzf \-\-sync \-\-query 5 \-\-listen \-\-bind start:up,load:up,result:up,focus:change\-header:Ready\fR
.RE .RE
.TP
.B "\-\-no\-tty\-default"
Make fzf search for the current TTY device via standard error instead of
defaulting to \fB/dev/tty\fR. This option avoids issues when launching
emacsclient from within fzf. Alternatively, you can change the default TTY
device by setting \fB--tty-default=DEVICE_NAME\fR.
.SS GLOBAL STYLE .SS GLOBAL STYLE
.TP .TP
.BI "\-\-style=" "PRESET" .BI "\-\-style=" "PRESET"
@ -203,7 +242,7 @@ Apply a style preset [default|minimal|full[:BORDER_STYLE]]
.TP .TP
.BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..." .BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. color mappings. Each entry is separated by a comma and/or whitespaces.
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
@ -231,6 +270,7 @@ color mappings.
\fBcurrent\-bg (bg+) \fRBackground (current line) \fBcurrent\-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line) \fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
\fBalt\-bg \fRAlternate background color to create striped lines
\fBquery (input\-fg) \fRQuery string \fBquery (input\-fg) \fRQuery string
\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)
@ -298,7 +338,19 @@ color mappings.
# Seoul256 theme with 24-bit colors # Seoul256 theme with 24-bit colors
fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\ fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
\-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\ \-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR \-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'
# Seoul256 light theme with 24-bit colors, each entry separated by whitespaces
fzf \-\-style full \-\-color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'\fR
.RE .RE
.TP .TP
.B "\-\-no\-color" .B "\-\-no\-color"
@ -677,6 +729,10 @@ ANSI color codes are supported.
Do not display horizontal separator on the info line. A synonym for Do not display horizontal separator on the info line. A synonym for
\fB\-\-separator=''\fB \fB\-\-separator=''\fB
.TP
.BI "\-\-ghost=" "TEXT"
Ghost text to display when the input is empty
.TP .TP
.B "\-\-filepath\-word" .B "\-\-filepath\-word"
Make word-wise movements and actions respect path separators. The following Make word-wise movements and actions respect path separators. The following
@ -732,6 +788,12 @@ e.g.
\fBfzf \-\-multi \-\-preview='head \-10 {+}' \fBfzf \-\-multi \-\-preview='head \-10 {+}'
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
Each expression expands to a quoted string, so that it's safe to pass it as an
argument to an external command. So you should not manually add quotes around
the curly braces. But if you don't want this behavior, you can put
\fBr\fR flag (raw) in the expression (e.g. \fB{r}\fR, \fB{r1}\fR, etc).
Use it with caution as unquoted output can lead to broken commands.
When using a field index expression, leading and trailing whitespace is stripped 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.
@ -1230,10 +1292,20 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
.br .br
.BR FZF_GHOST " Ghost string"
.br
.BR FZF_POINTER " Pointer string"
.br
.BR FZF_PREVIEW_LABEL " Preview label string" .BR FZF_PREVIEW_LABEL " Preview label string"
.br .br
.BR FZF_BORDER_LABEL " Border label string" .BR FZF_BORDER_LABEL " Border label string"
.br .br
.BR FZF_LIST_LABEL " List label string"
.br
.BR FZF_INPUT_LABEL " Input label string"
.br
.BR FZF_HEADER_LABEL " Header label string"
.br
.BR FZF_ACTION " The name of the last action performed" .BR FZF_ACTION " The name of the last action performed"
.br .br
.BR FZF_KEY " The name of the last key pressed" .BR FZF_KEY " The name of the last key pressed"
@ -1573,6 +1645,7 @@ A key or an event can be bound to one or more of the following actions.
\fBbell\fR (ring the terminal bell) \fBbell\fR (ring the terminal bell)
\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\-ghost(...)\fR (change ghost text to the given string)
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR) \fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string) \fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string) \fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
@ -1580,6 +1653,7 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-multi\fR (enable multi-select mode with no limit) \fBchange\-multi\fR (enable multi-select mode with no limit)
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0) \fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|') \fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option) \fBchange\-preview(...)\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 '|')
@ -1597,6 +1671,8 @@ A key or an event can be bound to one or more of the following actions.
\fBdown\fR \fIctrl\-j ctrl\-n down\fR \fBdown\fR \fIctrl\-j ctrl\-n down\fR
\fBenable\-search\fR (enable search functionality) \fBenable\-search\fR (enable search functionality)
\fBend\-of\-line\fR \fIctrl\-e end\fR \fBend\-of\-line\fR \fIctrl\-e end\fR
\fBexclude\fR (exclude the current item from the result)
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute\-silent(...)\fR (see below for the details) \fBexecute\-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR) \fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
@ -1666,11 +1742,13 @@ A key or an event can be bound to one or more of the following actions.
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes) \fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command) \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\-ghost(...)\fR (transform ghost text using an external command)
\fBtransform\-header(...)\fR (transform header using an external command) \fBtransform\-header(...)\fR (transform header using an external command)
\fBtransform\-header\-label(...)\fR (transform header label using an external command) \fBtransform\-header\-label(...)\fR (transform header label using an external command)
\fBtransform\-input\-label(...)\fR (transform input label using an external command) \fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command) \fBtransform\-list\-label(...)\fR (transform list label using an external command)
\fBtransform\-nth(...)\fR (transform nth using an external command) \fBtransform\-nth(...)\fR (transform nth using an external command)
\fBtransform\-pointer(...)\fR (transform pointer using an external command)
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command) \fBtransform\-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)
@ -1690,6 +1768,9 @@ e.g.
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
\fBabort\fR, is ignored.
.SS ACTION ARGUMENT .SS ACTION ARGUMENT
An action denoted with \fB(...)\fR suffix takes an argument. An action denoted with \fB(...)\fR suffix takes an argument.

View File

@ -358,7 +358,7 @@ endfunction
function! s:get_color(attr, ...) function! s:get_color(attr, ...)
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152) " Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors) let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
let fam = gui ? 'gui' : 'cterm' let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
@ -553,8 +553,15 @@ try
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --no-tmux --height='.height let optstr .= ' --no-tmux --height='.height
endif endif
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
" Add 1-column horizontal margin
let optstr = join(['--margin 0,1', optstr])
else
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr]) let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
endif
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term if use_term

View File

@ -31,9 +31,6 @@ if [[ $- =~ i ]]; then
########################################################### ###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
@ -311,12 +308,12 @@ __fzf_generic_path_completion() {
else else
if [[ $1 =~ dir ]]; then if [[ $1 =~ dir ]]; then
walker=dir,follow walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-} eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
else else
walker=file,dir,follow,hidden walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-} eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
fi fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest __fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
fi | while read -r item; do fi | while read -r item; do
printf "%q " "${item%$3}$3" printf "%q " "${item%$3}$3"
done done
@ -328,6 +325,8 @@ __fzf_generic_path_completion() {
else else
COMPREPLY=( "$cur" ) COMPREPLY=( "$cur" )
fi fi
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n' printf '\e[5n'
return 0 return 0
fi fi
@ -384,6 +383,7 @@ _fzf_complete() {
else else
COMPREPLY=("$cur") COMPREPLY=("$cur")
fi fi
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n' printf '\e[5n'
return 0 return 0
else else

View File

@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" echo -E "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_comprun() { __fzf_comprun() {

View File

@ -14,15 +14,120 @@
# Key bindings # Key bindings
# ------------ # ------------
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
# command substitution syntax $(cmd) should never be used, even behind a version
# check, otherwise the source command will fail on fish versions older than 3.4.0.
function fzf_key_bindings function fzf_key_bindings
# Check fish version
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
echo "This script requires fish version 3.1b1 or newer." >&2
return 1
else if not type -q fzf
echo "fzf was not found in path." >&2
return 1
end
function __fzf_defaults function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --min-height 20+ --bind=ctrl-z:ignore" $argv[1] string join ' ' -- \
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE "--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
echo $FZF_DEFAULT_OPTS $argv[2] (test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
# Set variables containing the major and minor fish version numbers, using
# a method compatible with all supported fish versions.
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.4.1
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
# fish v3.1b1 - v3.1.2
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end end
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
@ -31,40 +136,34 @@ function fzf_key_bindings
set -lx dir $commandline[1] set -lx dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3] set -l prefix $commandline[3]
set -l result
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
begin "--reverse --walker=file,dir,follow,hidden --scheme=path" \
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS") "$FZF_CTRL_T_OPTS --multi --print0")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE '' set -lx FZF_DEFAULT_OPTS_FILE
set result (eval (__fzfcmd) -m --query=$fzf_query)
end set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
if test -z "$result" and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
commandline -f repaint
return
else
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- $prefix
commandline -it -- (string escape -- $i)
commandline -it -- ' '
end
commandline -f repaint commandline -f repaint
end end
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% set -l -- command_line (commandline)
begin set -l -- current_line (commandline -L)
# merge history from other sessions before searching set -l -- total_lines (count $command_line)
test -z "$fish_private_mode"; and builtin history merge set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line +m $FZF_CTRL_R_OPTS") set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
set -lx FZF_DEFAULT_OPTS_FILE '' '--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
"--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND set -lx FZF_DEFAULT_COMMAND
set -a -- FZF_DEFAULT_OPTS --with-shell=(status fish-path)\\ -c
if type -q perl if type -q perl
set -a FZF_DEFAULT_OPTS '--tac' set -a FZF_DEFAULT_OPTS '--tac'
@ -76,9 +175,21 @@ function fzf_key_bindings
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \ 'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end' 'end'
end end
set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --read0 --print0 -q (commandline | string escape) "--bind=enter:become:'string replace -a -- \n\t \n {2..} | string collect'")
and commandline -- $result # Merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
if test "$total_lines" -eq 1
commandline -- (string replace -a -- \n\t \n $result)
else
set -l a (math $current_line - 1)
set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
commandline -a -- '' $command_line[$b..-1]
end end
end
commandline -f repaint commandline -f repaint
end end
@ -88,113 +199,32 @@ 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]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
begin "--reverse --walker=dir,follow,hidden --scheme=path" \
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS") "$FZF_ALT_C_OPTS --no-multi --print0")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
set -l result (eval (__fzfcmd) +m --query=$fzf_query)
if test -n "$result" if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
cd -- $result cd -- $result
commandline -rt -- $prefix
# Remove last token from commandline.
commandline -t ""
commandline -it -- $prefix
end
end end
commandline -f repaint commandline -f repaint
end end
function __fzfcmd
test -n "$FZF_TMUX"; or set FZF_TMUX 0
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
bind \cr fzf-history-widget bind \cr fzf-history-widget
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND" if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget bind \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
end
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind -M insert \ct fzf-file-widget bind -M insert \ct fzf-file-widget
end end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND" if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
bind -M insert \ec fzf-cd-widget bind -M insert \ec fzf-cd-widget
end end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# Enable home directory expansion of leading ~/
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
# escape special characters, except for the $ sign of valid variable names,
# so that after eval, the original string is returned, but with the
# variable names replaced by their values.
set commandline (string escape -n -- $commandline)
set commandline (string replace -r -a -- '\x5c\$(?=[\w])' '\$' $commandline)
# eval is used to do shell expansion on paths
eval set commandline $commandline
# Combine multiple consecutive slashes into one
set commandline (string replace -r -a -- '/+' '/' $commandline)
if test -z "$commandline"
# Default to current directory with no --query
set dir '.'
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
# BUG: on combined expressions, if a left argument is a single `!`, the
# builtin test command of fish will treat it as the ! operator. To
# overcome this, have the variable parts on the right.
if test "." = "$dir" -a "./" != (string sub -l 2 -- $commandline)
# if $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline
else
# Also remove trailing slash after dir, to "split" input properly
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
end
end
echo (string escape -- $dir)
echo (string escape -- $fzf_query)
echo $prefix
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
set dir $argv
# Strip trailing slash, unless $dir is root dir (/)
set dir (string replace -r -- '(?<!^)/$' '' $dir)
# Iteratively check if dir exists and strip tail end of path
while test ! -d "$dir"
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
set dir (dirname -- "$dir")
end
echo $dir
end
end end

View File

@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" echo -E "${FZF_DEFAULT_OPTS-} $2"
} }
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line

View File

@ -12,137 +12,145 @@ func _() {
_ = x[actStart-1] _ = x[actStart-1]
_ = x[actClick-2] _ = x[actClick-2]
_ = x[actInvalid-3] _ = x[actInvalid-3]
_ = x[actChar-4] _ = x[actBracketedPasteBegin-4]
_ = x[actMouse-5] _ = x[actBracketedPasteEnd-5]
_ = x[actBeginningOfLine-6] _ = x[actChar-6]
_ = x[actAbort-7] _ = x[actMouse-7]
_ = x[actAccept-8] _ = x[actBeginningOfLine-8]
_ = x[actAcceptNonEmpty-9] _ = x[actAbort-9]
_ = x[actAcceptOrPrintQuery-10] _ = x[actAccept-10]
_ = x[actBackwardChar-11] _ = x[actAcceptNonEmpty-11]
_ = x[actBackwardDeleteChar-12] _ = x[actAcceptOrPrintQuery-12]
_ = x[actBackwardDeleteCharEof-13] _ = x[actBackwardChar-13]
_ = x[actBackwardWord-14] _ = x[actBackwardDeleteChar-14]
_ = x[actCancel-15] _ = x[actBackwardDeleteCharEof-15]
_ = x[actChangeBorderLabel-16] _ = x[actBackwardWord-16]
_ = x[actChangeListLabel-17] _ = x[actCancel-17]
_ = x[actChangeInputLabel-18] _ = x[actChangeBorderLabel-18]
_ = x[actChangeHeader-19] _ = x[actChangeGhost-19]
_ = x[actChangeHeaderLabel-20] _ = x[actChangeHeader-20]
_ = x[actChangeMulti-21] _ = x[actChangeHeaderLabel-21]
_ = x[actChangePreviewLabel-22] _ = x[actChangeInputLabel-22]
_ = x[actChangePrompt-23] _ = x[actChangeListLabel-23]
_ = x[actChangeQuery-24] _ = x[actChangeMulti-24]
_ = x[actChangeNth-25] _ = x[actChangeNth-25]
_ = x[actClearScreen-26] _ = x[actChangePointer-26]
_ = x[actClearQuery-27] _ = x[actChangePreview-27]
_ = x[actClearSelection-28] _ = x[actChangePreviewLabel-28]
_ = x[actClose-29] _ = x[actChangePreviewWindow-29]
_ = x[actDeleteChar-30] _ = x[actChangePrompt-30]
_ = x[actDeleteCharEof-31] _ = x[actChangeQuery-31]
_ = x[actEndOfLine-32] _ = x[actClearScreen-32]
_ = x[actFatal-33] _ = x[actClearQuery-33]
_ = x[actForwardChar-34] _ = x[actClearSelection-34]
_ = x[actForwardWord-35] _ = x[actClose-35]
_ = x[actKillLine-36] _ = x[actDeleteChar-36]
_ = x[actKillWord-37] _ = x[actDeleteCharEof-37]
_ = x[actUnixLineDiscard-38] _ = x[actEndOfLine-38]
_ = x[actUnixWordRubout-39] _ = x[actFatal-39]
_ = x[actYank-40] _ = x[actForwardChar-40]
_ = x[actBackwardKillWord-41] _ = x[actForwardWord-41]
_ = x[actSelectAll-42] _ = x[actKillLine-42]
_ = x[actDeselectAll-43] _ = x[actKillWord-43]
_ = x[actToggle-44] _ = x[actUnixLineDiscard-44]
_ = x[actToggleSearch-45] _ = x[actUnixWordRubout-45]
_ = x[actToggleAll-46] _ = x[actYank-46]
_ = x[actToggleDown-47] _ = x[actBackwardKillWord-47]
_ = x[actToggleUp-48] _ = x[actSelectAll-48]
_ = x[actToggleIn-49] _ = x[actDeselectAll-49]
_ = x[actToggleOut-50] _ = x[actToggle-50]
_ = x[actToggleTrack-51] _ = x[actToggleSearch-51]
_ = x[actToggleTrackCurrent-52] _ = x[actToggleAll-52]
_ = x[actToggleHeader-53] _ = x[actToggleDown-53]
_ = x[actToggleWrap-54] _ = x[actToggleUp-54]
_ = x[actToggleMultiLine-55] _ = x[actToggleIn-55]
_ = x[actToggleHscroll-56] _ = x[actToggleOut-56]
_ = x[actTrackCurrent-57] _ = x[actToggleTrack-57]
_ = x[actToggleInput-58] _ = x[actToggleTrackCurrent-58]
_ = x[actHideInput-59] _ = x[actToggleHeader-59]
_ = x[actShowInput-60] _ = x[actToggleWrap-60]
_ = x[actUntrackCurrent-61] _ = x[actToggleMultiLine-61]
_ = x[actDown-62] _ = x[actToggleHscroll-62]
_ = x[actUp-63] _ = x[actTrackCurrent-63]
_ = x[actPageUp-64] _ = x[actToggleInput-64]
_ = x[actPageDown-65] _ = x[actHideInput-65]
_ = x[actPosition-66] _ = x[actShowInput-66]
_ = x[actHalfPageUp-67] _ = x[actUntrackCurrent-67]
_ = x[actHalfPageDown-68] _ = x[actDown-68]
_ = x[actOffsetUp-69] _ = x[actUp-69]
_ = x[actOffsetDown-70] _ = x[actPageUp-70]
_ = x[actOffsetMiddle-71] _ = x[actPageDown-71]
_ = x[actJump-72] _ = x[actPosition-72]
_ = x[actJumpAccept-73] _ = x[actHalfPageUp-73]
_ = x[actPrintQuery-74] _ = x[actHalfPageDown-74]
_ = x[actRefreshPreview-75] _ = x[actOffsetUp-75]
_ = x[actReplaceQuery-76] _ = x[actOffsetDown-76]
_ = x[actToggleSort-77] _ = x[actOffsetMiddle-77]
_ = x[actShowPreview-78] _ = x[actJump-78]
_ = x[actHidePreview-79] _ = x[actJumpAccept-79]
_ = x[actTogglePreview-80] _ = x[actPrintQuery-80]
_ = x[actTogglePreviewWrap-81] _ = x[actRefreshPreview-81]
_ = x[actTransform-82] _ = x[actReplaceQuery-82]
_ = x[actTransformBorderLabel-83] _ = x[actToggleSort-83]
_ = x[actTransformListLabel-84] _ = x[actShowPreview-84]
_ = x[actTransformInputLabel-85] _ = x[actHidePreview-85]
_ = x[actTransformHeader-86] _ = x[actTogglePreview-86]
_ = x[actTransformHeaderLabel-87] _ = x[actTogglePreviewWrap-87]
_ = x[actTransformNth-88] _ = x[actTransform-88]
_ = x[actTransformPreviewLabel-89] _ = x[actTransformBorderLabel-89]
_ = x[actTransformPrompt-90] _ = x[actTransformGhost-90]
_ = x[actTransformQuery-91] _ = x[actTransformHeader-91]
_ = x[actTransformSearch-92] _ = x[actTransformHeaderLabel-92]
_ = x[actSearch-93] _ = x[actTransformInputLabel-93]
_ = x[actPreview-94] _ = x[actTransformListLabel-94]
_ = x[actChangePreview-95] _ = x[actTransformNth-95]
_ = x[actChangePreviewWindow-96] _ = x[actTransformPointer-96]
_ = x[actPreviewTop-97] _ = x[actTransformPreviewLabel-97]
_ = x[actPreviewBottom-98] _ = x[actTransformPrompt-98]
_ = x[actPreviewUp-99] _ = x[actTransformQuery-99]
_ = x[actPreviewDown-100] _ = x[actTransformSearch-100]
_ = x[actPreviewPageUp-101] _ = x[actSearch-101]
_ = x[actPreviewPageDown-102] _ = x[actPreview-102]
_ = x[actPreviewHalfPageUp-103] _ = x[actPreviewTop-103]
_ = x[actPreviewHalfPageDown-104] _ = x[actPreviewBottom-104]
_ = x[actPrevHistory-105] _ = x[actPreviewUp-105]
_ = x[actPrevSelected-106] _ = x[actPreviewDown-106]
_ = x[actPrint-107] _ = x[actPreviewPageUp-107]
_ = x[actPut-108] _ = x[actPreviewPageDown-108]
_ = x[actNextHistory-109] _ = x[actPreviewHalfPageUp-109]
_ = x[actNextSelected-110] _ = x[actPreviewHalfPageDown-110]
_ = x[actExecute-111] _ = x[actPrevHistory-111]
_ = x[actExecuteSilent-112] _ = x[actPrevSelected-112]
_ = x[actExecuteMulti-113] _ = x[actPrint-113]
_ = x[actSigStop-114] _ = x[actPut-114]
_ = x[actFirst-115] _ = x[actNextHistory-115]
_ = x[actLast-116] _ = x[actNextSelected-116]
_ = x[actReload-117] _ = x[actExecute-117]
_ = x[actReloadSync-118] _ = x[actExecuteSilent-118]
_ = x[actDisableSearch-119] _ = x[actExecuteMulti-119]
_ = x[actEnableSearch-120] _ = x[actSigStop-120]
_ = x[actSelect-121] _ = x[actFirst-121]
_ = x[actDeselect-122] _ = x[actLast-122]
_ = x[actUnbind-123] _ = x[actReload-123]
_ = x[actRebind-124] _ = x[actReloadSync-124]
_ = x[actToggleBind-125] _ = x[actDisableSearch-125]
_ = x[actBecome-126] _ = x[actEnableSearch-126]
_ = x[actShowHeader-127] _ = x[actSelect-127]
_ = x[actHideHeader-128] _ = x[actDeselect-128]
_ = x[actBell-129] _ = x[actUnbind-129]
_ = x[actRebind-130]
_ = x[actToggleBind-131]
_ = x[actBecome-132]
_ = x[actShowHeader-133]
_ = x[actHideHeader-134]
_ = x[actBell-135]
_ = x[actExclude-136]
_ = x[actExcludeMulti-137]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBell" const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 318, 337, 355, 369, 381, 397, 413, 434, 456, 471, 485, 499, 512, 529, 537, 550, 566, 578, 586, 600, 614, 625, 636, 654, 671, 678, 697, 709, 723, 732, 747, 759, 772, 783, 794, 806, 820, 841, 856, 869, 887, 903, 918, 932, 944, 956, 973, 980, 985, 994, 1005, 1016, 1029, 1044, 1055, 1068, 1083, 1090, 1103, 1116, 1133, 1148, 1161, 1175, 1189, 1205, 1225, 1237, 1260, 1277, 1295, 1318, 1340, 1361, 1376, 1395, 1419, 1437, 1454, 1472, 1481, 1491, 1504, 1520, 1532, 1546, 1562, 1580, 1600, 1622, 1636, 1651, 1659, 1665, 1679, 1694, 1704, 1720, 1735, 1745, 1753, 1760, 1769, 1782, 1798, 1813, 1822, 1833, 1842, 1851, 1864, 1873, 1886, 1899, 1906, 1916, 1931}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward) pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_] pchar := pattern[pidx_]

View File

@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
bytes[math.MaxUint16] = 'z' bytes[math.MaxUint16] = 'z'
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive) assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
} }
func TestLongStringWithNormalize(t *testing.T) {
bytes := make([]byte, 30000)
for i := range bytes {
bytes[i] = 'x'
}
unicodeString := string(bytes) + " Minímal example"
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
}

View File

@ -26,7 +26,7 @@ const (
previewCancelWait = 500 * time.Millisecond previewCancelWait = 500 * time.Millisecond
previewChunkDelay = 100 * time.Millisecond previewChunkDelay = 100 * time.Millisecond
previewDelayed = 500 * time.Millisecond previewDelayed = 500 * time.Millisecond
maxPatternLength = 300 maxPatternLength = 1000
maxMulti = math.MaxInt32 maxMulti = math.MaxInt32
// Matcher // Matcher

View File

@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32 var itemIndex int32
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, byteString(data)) header = append(header, byteString(data))
@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
return true return true
}) })
} else { } else {
nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
@ -127,8 +128,7 @@ func Run(opts *Options) (int, error) {
} }
} }
} }
trans := Transform(tokens, opts.WithNth) transformed := nthTransformer(tokens, itemIndex)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, transformed) header = append(header, transformed)
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
@ -195,15 +195,30 @@ func Run(opts *Options) (int, error) {
} }
nth := opts.Nth nth := opts.Nth
nthRevision := 0
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, nthRevision, runes)
}
inputRevision := revision{} inputRevision := revision{}
snapshotRevision := revision{} snapshotRevision := revision{}
patternCache := make(map[string]*Pattern)
denyMutex := sync.Mutex{}
denylist := make(map[int32]struct{})
clearDenylist := func() {
denyMutex.Lock()
if len(denylist) > 0 {
patternCache = make(map[string]*Pattern)
}
denylist = make(map[int32]struct{})
denyMutex.Unlock()
}
patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock()
denylistCopy := make(map[int32]struct{})
for k, v := range denylist {
denylistCopy[k] = v
}
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
@ -280,6 +295,7 @@ func Run(opts *Options) (int, error) {
// Event coordination // Event coordination
reading := true reading := true
ticks := 0 ticks := 0
startTick := 0
var nextCommand *commandSpec var nextCommand *commandSpec
var nextEnviron []string var nextEnviron []string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
@ -302,7 +318,11 @@ func Run(opts *Options) (int, error) {
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command commandSpec, environ []string) { restart := func(command commandSpec, environ []string) {
if !useSnapshot {
clearDenylist()
}
reading = true reading = true
startTick = ticks
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision.bumpMajor() inputRevision.bumpMajor()
@ -348,7 +368,8 @@ func Run(opts *Options) (int, error) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
if useSnapshot && evt == EvtReadFin { if useSnapshot && evt == EvtReadFin { // reload-sync
clearDenylist()
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
@ -379,10 +400,21 @@ func Run(opts *Options) (int, error) {
command = val.command command = val.command
environ = val.environ environ = val.environ
changed = val.changed changed = val.changed
bump := false
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
denyMutex.Lock()
for _, itemIndex := range val.denylist {
denylist[itemIndex] = struct{}{}
}
denyMutex.Unlock()
bump = true
}
if val.nth != nil { if val.nth != nil {
// Change nth and clear caches // Change nth and clear caches
nth = *val.nth nth = *val.nth
nthRevision++ bump = true
}
if bump {
patternCache = make(map[string]*Pattern) patternCache = make(map[string]*Pattern)
cache.Clear() cache.Clear()
inputRevision.bumpMinor() inputRevision.bumpMinor()
@ -447,8 +479,17 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 { if len(opts.Expect) > 0 {
opts.Printer("") opts.Printer("")
} }
transformer := func(item *Item) string {
return item.AsString(opts.Ansi)
}
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
transformer = func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(transformer(val.Get(i).item))
} }
if count == 0 { if count == 0 {
exitCode = ExitNoMatch exitCode = ExitNoMatch
@ -470,7 +511,7 @@ func Run(opts *Options) (int, error) {
} }
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks-startTick)*coordinatorDelayStep,
0, coordinatorDelayMax) 0, coordinatorDelayMax)
time.Sleep(dur) time.Sleep(dur)
} }

View File

@ -9,7 +9,7 @@ import (
type transformed struct { type transformed struct {
// Because nth can be changed dynamically by change-nth action, we need to // Because nth can be changed dynamically by change-nth action, we need to
// keep the revision number at the time of transformation. // keep the revision number at the time of transformation.
revision int revision revision
tokens []Token tokens []Token
} }
@ -51,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
} }
return item.text.ToString() return item.text.ToString()
} }
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
transformed := transformer(tokens, item.Index())
return StripLastDelimiter(transformed, delimiter)
}

View File

@ -41,6 +41,7 @@ Usage: fzf [options]
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform the presentation of each line using --with-nth=N[,..] Transform the presentation of each line using
field index expressions field index expressions
--accept-nth=N[,..] Define which fields to print on accept
-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
--literal Do not normalize latin script letters --literal Do not normalize latin script letters
@ -135,6 +136,7 @@ Usage: fzf [options]
--separator=STR Draw horizontal separator on info line using the string --separator=STR Draw horizontal separator on info line using the string
(default: '─' or '-') (default: '─' or '-')
--no-separator Hide info line separator --no-separator Hide info line separator
--ghost=TEXT Ghost text to display when the input is empty
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section --input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
@ -543,7 +545,8 @@ type Options struct {
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
WithNth []Range WithNth func(Delimiter) func([]Token, int32) string
AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Track trackOption Track trackOption
@ -572,6 +575,7 @@ type Options struct {
InfoStyle infoStyle InfoStyle infoStyle
InfoPrefix string InfoPrefix string
InfoCommand string InfoCommand string
Ghost string
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
@ -627,6 +631,7 @@ type Options struct {
MEMProfile string MEMProfile string
BlockProfile string BlockProfile string
MutexProfile string MutexProfile string
TtyDefault string
} }
func filterNonEmpty(input []string) []string { func filterNonEmpty(input []string) []string {
@ -665,7 +670,6 @@ func defaultOptions() *Options {
Case: CaseSmart, Case: CaseSmart,
Normalize: true, Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Track: trackDisabled, Track: trackDisabled,
@ -688,6 +692,7 @@ func defaultOptions() *Options {
ScrollOff: 3, ScrollOff: 3,
FileWord: false, FileWord: false,
InfoStyle: infoDefault, InfoStyle: infoDefault,
Ghost: "",
Separator: nil, Separator: nil,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
@ -726,6 +731,7 @@ func defaultOptions() *Options {
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true}, WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
WalkerRoot: []string{"."}, WalkerRoot: []string{"."},
WalkerSkip: []string{".git", "node_modules"}, WalkerSkip: []string{".git", "node_modules"},
TtyDefault: tui.DefaultTtyDevice,
Help: false, Help: false,
Version: false} Version: false}
} }
@ -768,6 +774,70 @@ func splitNth(str string) ([]Range, error) {
return ranges, nil return ranges, nil
} }
func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
// ^[0-9,-.]+$"
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
nth, err := splitNth(str)
if err != nil {
return nil, err
}
return func(Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
return JoinTokens(Transform(tokens, nth))
}
}, nil
}
// {...} {...} ...
placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
indexes := placeholder.FindAllStringIndex(str, -1)
if indexes == nil {
return nil, errors.New("template should include at least 1 placeholder: " + str)
}
type NthParts struct {
str string
index bool
nth []Range
}
parts := make([]NthParts, len(indexes))
idx := 0
for _, index := range indexes {
if idx < index[0] {
parts = append(parts, NthParts{str: str[idx:index[0]]})
}
expr := str[index[0]+1 : index[1]-1]
if expr == "n" {
parts = append(parts, NthParts{index: true})
} else if nth, err := splitNth(expr); err == nil {
parts = append(parts, NthParts{nth: nth})
}
idx = index[1]
}
if idx < len(str) {
parts = append(parts, NthParts{str: str[idx:]})
}
return func(delimiter Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
str := ""
for _, holder := range parts {
if holder.nth != nil {
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
} else if holder.index {
if index >= 0 {
str += strconv.Itoa(int(index))
}
} else {
str += holder.str
}
}
return str
}
}, nil
}
func delimiterRegexp(str string) Delimiter { func delimiterRegexp(str string) Delimiter {
// Special handling of \t // Special handling of \t
str = strings.ReplaceAll(str, "\\t", "\t") str = strings.ReplaceAll(str, "\\t", "\t")
@ -1114,7 +1184,12 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
var err error var err error
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
for _, str := range strings.Split(strings.ToLower(str), ",") { comma := regexp.MustCompile(`[\s,]+`)
for _, str := range comma.Split(strings.ToLower(str), -1) {
str = strings.TrimSpace(str)
if len(str) == 0 {
continue
}
switch str { switch str {
case "dark": case "dark":
theme = dupeTheme(tui.Dark256) theme = dupeTheme(tui.Dark256)
@ -1225,6 +1300,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.Current) mergeAttr(&theme.Current)
case "current-bg", "bg+": case "current-bg", "bg+":
mergeAttr(&theme.DarkBg) mergeAttr(&theme.DarkBg)
case "alt-bg":
mergeAttr(&theme.AltBg)
case "selected-fg": case "selected-fg":
mergeAttr(&theme.SelectedFg) mergeAttr(&theme.SelectedFg)
case "selected-bg": case "selected-bg":
@ -1336,7 +1413,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@ -1600,6 +1677,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
} }
case "bell": case "bell":
appendAction(actBell) appendAction(actBell)
case "exclude":
appendAction(actExclude)
case "exclude-multi":
appendAction(actExcludeMulti)
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {
@ -1727,6 +1808,10 @@ func isExecuteAction(str string) actionType {
return actChangeInputLabel return actChangeInputLabel
case "change-header-label": case "change-header-label":
return actChangeHeaderLabel return actChangeHeaderLabel
case "change-ghost":
return actChangeGhost
case "change-pointer":
return actChangePointer
case "change-preview-window": case "change-preview-window":
return actChangePreviewWindow return actChangePreviewWindow
case "change-preview": case "change-preview":
@ -1765,8 +1850,12 @@ func isExecuteAction(str string) actionType {
return actTransformHeaderLabel return actTransformHeaderLabel
case "transform-header": case "transform-header":
return actTransformHeader return actTransformHeader
case "transform-ghost":
return actTransformGhost
case "transform-nth": case "transform-nth":
return actTransformNth return actTransformNth
case "transform-pointer":
return actTransformPointer
case "transform-prompt": case "transform-prompt":
return actTransformPrompt return actTransformPrompt
case "transform-query": case "transform-query":
@ -2256,6 +2345,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} }
case "--no-tmux": case "--no-tmux":
opts.Tmux = nil opts.Tmux = nil
case "--tty-default":
if opts.TtyDefault, err = nextString("tty device name required"); err != nil {
return err
}
case "--no-tty-default":
opts.TtyDefault = ""
case "--force-tty-in": case "--force-tty-in":
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't // NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
// work on Neovim. Same as '-' option of fzf-tmux. // work on Neovim. Same as '-' option of fzf-tmux.
@ -2380,7 +2475,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
if opts.WithNth, err = splitNth(str); err != nil { if opts.WithNth, err = nthTransformer(str); err != nil {
return err
}
case "--accept-nth":
str, err := nextString("nth expression required")
if err != nil {
return err
}
if opts.AcceptNth, err = nthTransformer(str); err != nil {
return err return err
} }
case "-s", "--sort": case "-s", "--sort":
@ -2520,6 +2623,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--no-separator": case "--no-separator":
nosep := "" nosep := ""
opts.Separator = &nosep opts.Separator = &nosep
case "--ghost":
if opts.Ghost, err = nextString("ghost text required"); err != nil {
return err
}
case "--scrollbar": case "--scrollbar":
given, bar := optionalNextString() given, bar := optionalNextString()
if given { if given {

View File

@ -333,7 +333,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized, _ = parseTheme(theme, "fg:231,dark,bg:232") customized, _ = parseTheme(theme, "fg:231,dark bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }

View File

@ -60,9 +60,10 @@ type Pattern struct {
cacheKey string cacheKey string
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
revision int revision revision
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{}
} }
var _splitRegex *regexp.Regexp var _splitRegex *regexp.Regexp
@ -73,7 +74,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision int, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
var asString string var asString string
if extended { if extended {
@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
revision: revision, revision: revision,
delimiter: delimiter, delimiter: delimiter,
cache: cache, cache: cache,
denylist: denylist,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// IsEmpty returns true if the pattern is effectively empty // IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool { func (p *Pattern) IsEmpty() bool {
if len(p.denylist) > 0 {
return false
}
if !p.extended { if !p.extended {
return len(p.text) == 0 return len(p.text) == 0
} }
@ -296,6 +301,8 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{} matches := []Result{}
if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
@ -312,6 +319,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
return matches return matches
} }
if space == nil {
for idx := 0; idx < chunk.count; idx++ {
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if _, prs := p.denylist[result.item.Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
}
return matches
}
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if p.extended { if p.extended {
@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
tokens := Tokenize(item.text.ToString(), p.delimiter) tokens := Tokenize(item.text.ToString(), p.delimiter)
ret := Transform(tokens, p.nth) ret := Transform(tokens, p.nth)
// Strip the last delimiter to allow suffix match
if len(ret) > 0 && !p.delimiter.IsAwk() {
chars := ret[len(ret)-1].text
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
newChars := util.ToChars(stringBytes(stripped))
ret[len(ret)-1].text = &newChars
}
item.transformed = &transformed{p.revision, ret} item.transformed = &transformed{p.revision, ret}
return ret return ret
} }

View File

@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern), return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward, fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, 0, runes) withPos, cacheable, nth, delimiter, revision{}, runes, nil)
} }
func TestExact(t *testing.T) { func TestExact(t *testing.T) {

View File

@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
}) })
}() }()
var command string var command, input string
commandPrefix += ` --no-force-tty-in --proxy-script "$0"` commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) { if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output) command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else { } else {
input, err := fifo("proxy-input") input, err = fifo("proxy-input")
if err != nil { if err != nil {
return ExitError, err return ExitError, err
} }
@ -90,9 +90,9 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
} }
} }
// To ensure that the options are processed by a POSIX-compliant shell, // * Write the command to a temporary file and run it with sh to ensure POSIX compliance.
// we need to write the command to a temporary file and execute it with sh. // * Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
var exports []string exports := []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
needBash := false needBash := false
if withExports { if withExports {
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
@ -144,10 +144,13 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
env = elems[1:] env = elems[1:]
} }
executor := util.NewExecutor(opts.WithShell) executor := util.NewExecutor(opts.WithShell)
ttyin, err := tui.TtyIn() ttyin, err := tui.TtyIn(opts.TtyDefault)
if err != nil { if err != nil {
return ExitError, err return ExitError, err
} }
os.Remove(temp)
os.Remove(input)
os.Remove(output)
executor.Become(ttyin, env, command) executor.Become(ttyin, env, command)
} }
return code, err return code, err

View File

@ -277,6 +277,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
ignoresFull := []string{} ignoresFull := []string{}
ignoresSuffix := []string{} ignoresSuffix := []string{}
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
if _, ok := os.LookupEnv("MSYSTEM"); ok {
sep = "/"
}
for _, ignore := range ignores { for _, ignore := range ignores {
if strings.ContainsRune(ignore, os.PathSeparator) { if strings.ContainsRune(ignore, os.PathSeparator) {
if strings.HasPrefix(ignore, sep) { if strings.HasPrefix(ignore, sep) {
@ -320,6 +323,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
return filepath.SkipDir return filepath.SkipDir
} }
} }
if path != sep {
path += sep
}
} }
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) { if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew)) atomic.StoreInt32(&r.event, int32(EvtReadNew))

View File

@ -119,7 +119,7 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
} }
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset { func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
itemColors := result.item.Colors() itemColors := result.item.Colors()
// No ANSI codes // No ANSI codes
@ -182,18 +182,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
fg := ansi.color.fg fg := ansi.color.fg
bg := ansi.color.bg bg := ansi.color.bg
if fg == -1 { if fg == -1 {
if current { fg = colBase.Fg()
fg = theme.Current.Color
} else {
fg = theme.Fg.Color
}
} }
if bg == -1 { if bg == -1 {
if current { bg = colBase.Bg()
bg = theme.DarkBg.Color
} else {
bg = theme.Bg.Color
}
} }
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base) return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
} }

View File

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

View File

@ -38,7 +38,7 @@ As such it is not useful for validation, but rather to generate test
cases for example. cases for example.
\\?(?: # escaped type \\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type {\+?s?f?r?RANGE(?:,RANGE)*} # token type
{q[:s?RANGE]} # query type {q[:s?RANGE]} # query type
|{\+?n?f?} # item type (notice no mandatory element inside brackets) |{\+?n?f?} # item type (notice no mandatory element inside brackets)
) )
@ -65,7 +65,7 @@ const maxFocusEvents = 10000
const blockDuration = 1 * time.Second const blockDuration = 1 * time.Second
func init() { func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`) placeholder = regexp.MustCompile(`\\?(?:{[+sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@ -234,6 +234,7 @@ type Terminal struct {
wrap bool wrap bool
wrapSign string wrapSign string
wrapSignWidth int wrapSignWidth int
ghost string
separator labelPrinter separator labelPrinter
separatorLen int separatorLen int
spinner []string spinner []string
@ -278,6 +279,7 @@ type Terminal struct {
yanked []rune yanked []rune
input []rune input []rune
inputOverride *[]rune inputOverride *[]rune
pasting *[]rune
multi int multi int
multiLine bool multiLine bool
sort bool sort bool
@ -305,6 +307,7 @@ type Terminal struct {
nthAttr tui.Attr nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range nthCurrent []Range
acceptNth func([]Token, int32) string
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
@ -378,6 +381,7 @@ type Terminal struct {
slab *util.Slab slab *util.Slab
theme *tui.ColorTheme theme *tui.ColorTheme
tui tui.Renderer tui tui.Renderer
ttyDefault string
ttyin *os.File ttyin *os.File
executing *util.AtomicBool executing *util.AtomicBool
termSize tui.TermSize termSize tui.TermSize
@ -390,6 +394,12 @@ type Terminal struct {
clickHeaderLine int clickHeaderLine int
clickHeaderColumn int clickHeaderColumn int
proxyScript string proxyScript string
numLinesCache map[int32]numLinesCacheValue
}
type numLinesCacheValue struct {
atMost int
numLines int
} }
type selectedItem struct { type selectedItem struct {
@ -451,6 +461,8 @@ const (
actStart actStart
actClick actClick
actInvalid actInvalid
actBracketedPasteBegin
actBracketedPasteEnd
actChar actChar
actMouse actMouse
actBeginningOfLine actBeginningOfLine
@ -464,15 +476,19 @@ const (
actBackwardWord actBackwardWord
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
actChangeListLabel actChangeGhost
actChangeInputLabel
actChangeHeader actChangeHeader
actChangeHeaderLabel actChangeHeaderLabel
actChangeInputLabel
actChangeListLabel
actChangeMulti actChangeMulti
actChangeNth
actChangePointer
actChangePreview
actChangePreviewLabel actChangePreviewLabel
actChangePreviewWindow
actChangePrompt actChangePrompt
actChangeQuery actChangeQuery
actChangeNth
actClearScreen actClearScreen
actClearQuery actClearQuery
actClearSelection actClearSelection
@ -531,19 +547,19 @@ const (
actTogglePreviewWrap actTogglePreviewWrap
actTransform actTransform
actTransformBorderLabel actTransformBorderLabel
actTransformListLabel actTransformGhost
actTransformInputLabel
actTransformHeader actTransformHeader
actTransformHeaderLabel actTransformHeaderLabel
actTransformInputLabel
actTransformListLabel
actTransformNth actTransformNth
actTransformPointer
actTransformPreviewLabel actTransformPreviewLabel
actTransformPrompt actTransformPrompt
actTransformQuery actTransformQuery
actTransformSearch actTransformSearch
actSearch actSearch
actPreview actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop actPreviewTop
actPreviewBottom actPreviewBottom
actPreviewUp actPreviewUp
@ -577,6 +593,8 @@ const (
actShowHeader actShowHeader
actHideHeader actHideHeader
actBell actBell
actExclude
actExcludeMulti
) )
func (a actionType) Name() string { func (a actionType) Name() string {
@ -611,6 +629,7 @@ type placeholderFlags struct {
number bool number bool
forceUpdate bool forceUpdate bool
file bool file bool
raw bool
} }
type searchRequest struct { type searchRequest struct {
@ -620,6 +639,8 @@ type searchRequest struct {
command *commandSpec command *commandSpec
environ []string environ []string
changed bool changed bool
denylist []int32
revision revision
} }
type previewRequest struct { type previewRequest struct {
@ -627,6 +648,7 @@ type previewRequest struct {
scrollOffset int scrollOffset int
list []*Item list []*Item
env []string env []string
query string
} }
type previewResult struct { type previewResult struct {
@ -655,6 +677,8 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.Fatal, actFatal) add(tui.Fatal, actFatal)
add(tui.Invalid, actInvalid) add(tui.Invalid, actInvalid)
add(tui.BracketedPasteBegin, actBracketedPasteBegin)
add(tui.BracketedPasteEnd, actBracketedPasteEnd)
add(tui.CtrlA, actBeginningOfLine) add(tui.CtrlA, actBeginningOfLine)
add(tui.CtrlB, actBackwardChar) add(tui.CtrlB, actBackwardChar)
add(tui.CtrlC, actAbort) add(tui.CtrlC, actAbort)
@ -786,7 +810,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
// when you run fzf multiple times in your Go program. Closing it is known to // when you run fzf multiple times in your Go program. Closing it is known to
// cause problems with 'become' action and invalid terminal state after exit. // cause problems with 'become' action and invalid terminal state after exit.
if ttyin == nil { if ttyin == nil {
if ttyin, err = tui.TtyIn(); err != nil { if ttyin, err = tui.TtyIn(opts.TtyDefault); err != nil {
return nil, err return nil, err
} }
} }
@ -794,7 +818,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
if tui.HasFullscreenRenderer() { if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else { } else {
renderer, err = tui.NewLightRenderer(ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h }) true, func(h int) int { return h })
} }
} else { } else {
@ -810,7 +834,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
effectiveMinHeight += borderLines(opts.BorderShape) effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight)) return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
} }
renderer, err = tui.NewLightRenderer(ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -835,6 +859,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
infoCommand: opts.InfoCommand, infoCommand: opts.InfoCommand,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
infoPrefix: opts.InfoPrefix, infoPrefix: opts.InfoPrefix,
ghost: opts.Ghost,
separator: nil, separator: nil,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
promptString: opts.Prompt, promptString: opts.Prompt,
@ -943,11 +968,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
keyChan: make(chan tui.Event), keyChan: make(chan tui.Event),
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize) eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
tui: renderer, tui: renderer,
ttyDefault: opts.TtyDefault,
ttyin: ttyin, ttyin: ttyin,
initFunc: func() error { return renderer.Init() }, initFunc: func() error { return renderer.Init() },
executing: util.NewAtomicBool(false), executing: util.NewAtomicBool(false),
lastAction: actStart, lastAction: actStart,
lastFocus: minItem.Index()} lastFocus: minItem.Index(),
numLinesCache: make(map[int32]numLinesCacheValue)}
if opts.AcceptNth != nil {
t.acceptNth = opts.AcceptNth(t.delimiter)
}
// This should be called before accessing tui.Color* // This should be called before accessing tui.Color*
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible()) tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
@ -1061,9 +1091,13 @@ func (t *Terminal) environImpl(forPreview bool) []string {
env = append(env, "FZF_ACTION="+t.lastAction.Name()) env = append(env, "FZF_ACTION="+t.lastAction.Name())
env = append(env, "FZF_KEY="+t.lastKey) env = append(env, "FZF_KEY="+t.lastKey)
env = append(env, "FZF_PROMPT="+string(t.promptString)) env = append(env, "FZF_PROMPT="+string(t.promptString))
env = append(env, "FZF_GHOST="+string(t.ghost))
env = append(env, "FZF_POINTER="+string(t.pointer))
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label) env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label) env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
env = append(env, "FZF_LIST_LABEL="+t.listLabelOpts.label) env = append(env, "FZF_LIST_LABEL="+t.listLabelOpts.label)
env = append(env, "FZF_INPUT_LABEL="+t.inputLabelOpts.label)
env = append(env, "FZF_HEADER_LABEL="+t.headerLabelOpts.label)
if len(t.nthCurrent) > 0 { if len(t.nthCurrent) > 0 {
env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent)) env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
} }
@ -1201,9 +1235,14 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
return nil, 0 return nil, 0
} }
printFn := func(window tui.Window, limit int) { printFn := func(window tui.Window, limit int) {
if length > limit { ellipsis := []rune{}
trimmedRunes, _ := t.trimRight(runes, limit) ellipsisWidth := 0
window.CPrint(*color, string(trimmedRunes)) if !fill {
ellipsis, ellipsisWidth = util.Truncate(t.ellipsis, limit)
}
if length > limit-ellipsisWidth {
trimmedRunes, _ := t.trimRight(runes, limit-ellipsisWidth)
window.CPrint(*color, string(trimmedRunes)+string(ellipsis))
} else if fill { } else if fill {
window.CPrint(*color, util.RepeatToFill(text, length, limit)) window.CPrint(*color, util.RepeatToFill(text, length, limit))
} else { } else {
@ -1224,7 +1263,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
printFn := func(window tui.Window, limit int) { printFn := func(window tui.Window, limit int) {
if offsets == nil { if offsets == nil {
// tui.Col* are not initialized until renderer.Init() // tui.Col* are not initialized until renderer.Init()
offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, false) offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr)
} }
for limit > 0 { for limit > 0 {
if length > limit { if length > limit {
@ -1283,8 +1322,11 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
t.wrap = false t.wrap = false
t.withWindow(t.inputWindow, func() { t.withWindow(t.inputWindow, func() {
line := t.promptLine() line := t.promptLine()
preTask := func(markerClass) int {
return 1
}
t.printHighlighted( t.printHighlighted(
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, nil, nil) Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, preTask, nil)
}) })
t.wrap = wrap t.wrap = wrap
} }
@ -1318,6 +1360,10 @@ func (t *Terminal) wrapCols() int {
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1) return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
} }
func (t *Terminal) clearNumLinesCache() {
t.numLinesCache = make(map[int32]numLinesCacheValue)
}
// Number of lines the item takes including the gap // Number of lines the item takes including the gap
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) { func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
var numLines int var numLines int
@ -1325,6 +1371,12 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
numLines = 1 + t.gap numLines = 1 + t.gap
return numLines, numLines > atMost return numLines, numLines > atMost
} }
if cached, prs := t.numLinesCache[item.Index()]; prs {
// Can we use this cache? Let's be conservative.
if cached.atMost <= atMost {
return cached.numLines, false
}
}
var overflow bool var overflow bool
if !t.wrap && t.multiLine { if !t.wrap && t.multiLine {
numLines, overflow = item.text.NumLines(atMost) numLines, overflow = item.text.NumLines(atMost)
@ -1334,6 +1386,9 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
numLines = len(lines) numLines = len(lines)
} }
numLines += t.gap numLines += t.gap
if !overflow {
t.numLinesCache[item.Index()] = numLinesCacheValue{atMost, numLines}
}
return numLines, overflow || numLines > atMost return numLines, overflow || numLines > atMost
} }
@ -1379,10 +1434,7 @@ func (t *Terminal) Input() (bool, []rune) {
t.mutex.Lock() t.mutex.Lock()
defer t.mutex.Unlock() defer t.mutex.Unlock()
paused := t.paused paused := t.paused
var src []rune src := t.input
if !t.inputless {
src = t.input
}
if t.inputOverride != nil { if t.inputOverride != nil {
paused = false paused = false
src = *t.inputOverride src = *t.inputOverride
@ -1461,6 +1513,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
if !t.revision.compatible(newRevision) { if !t.revision.compatible(newRevision) {
// Reloaded: clear selection // Reloaded: clear selection
t.selected = make(map[int32]selectedItem) t.selected = make(map[int32]selectedItem)
t.clearNumLinesCache()
} else { } else {
// Trimmed by --tail: filter selection by index // Trimmed by --tail: filter selection by index
filtered := make(map[int32]selectedItem) filtered := make(map[int32]selectedItem)
@ -1540,16 +1593,24 @@ func (t *Terminal) output() bool {
for _, s := range t.printQueue { for _, s := range t.printQueue {
t.printer(s) t.printer(s)
} }
transform := func(item *Item) string {
return item.AsString(t.ansi)
}
if t.acceptNth != nil {
transform = func(item *Item) string {
return item.acceptNth(t.ansi, t.delimiter, t.acceptNth)
}
}
found := len(t.selected) > 0 found := len(t.selected) > 0
if !found { if !found {
current := t.currentItem() current := t.currentItem()
if current != nil { if current != nil {
t.printer(current.AsString(t.ansi)) t.printer(transform(current))
found = true found = true
} }
} else { } else {
for _, sel := range t.sortSelected() { for _, sel := range t.sortSelected() {
t.printer(sel.item.AsString(t.ansi)) t.printer(transform(sel.item))
} }
} }
return found return found
@ -1712,6 +1773,7 @@ func (t *Terminal) hasHeaderLinesWindow() bool {
} }
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.clearNumLinesCache()
t.forcePreview = forcePreview t.forcePreview = forcePreview
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
width := screenWidth - marginInt[1] - marginInt[3] width := screenWidth - marginInt[1] - marginInt[3]
@ -1900,6 +1962,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
pwidth -= 1 pwidth -= 1
} }
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true) t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
t.pwindow.SetWrapSign(t.wrapSign, t.wrapSignWidth)
if !hadPreviewWindow { if !hadPreviewWindow {
t.pwindow.Erase() t.pwindow.Erase()
} }
@ -2246,7 +2309,11 @@ func (t *Terminal) move(y int, x int, clear bool) {
} }
func (t *Terminal) truncateQuery() { func (t *Terminal) truncateQuery() {
t.input, _ = t.trimRight(t.input, maxPatternLength) // We're limiting the length of the query not to make fzf unresponsive when
// the user accidentally pastes a huge chunk of text. Therefore, we're not
// interested in the exact display width of the query. We just limit the
// number of runes.
t.input = t.input[:util.Min(len(t.input), maxPatternLength)]
t.cx = util.Constrain(t.cx, 0, len(t.input)) t.cx = util.Constrain(t.cx, 0, len(t.input))
} }
@ -2290,15 +2357,18 @@ func (t *Terminal) placeCursor() {
if t.inputless { if t.inputless {
return return
} }
x := t.promptLen + t.queryLen[0]
if t.inputWindow != nil { if t.inputWindow != nil {
y := t.inputWindow.Height() - 1 y := t.inputWindow.Height() - 1
if t.layout == layoutReverse { if t.layout == layoutReverse {
y = 0 y = 0
} }
t.inputWindow.Move(y, t.promptLen+t.queryLen[0]) x = util.Min(x, t.inputWindow.Width()-1)
t.inputWindow.Move(y, x)
return return
} }
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false) x = util.Min(x, t.window.Width()-1)
t.move(t.promptLine(), x, false)
} }
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
@ -2315,6 +2385,11 @@ func (t *Terminal) printPrompt() {
t.prompt() t.prompt()
before, after := t.updatePromptOffset() before, after := t.updatePromptOffset()
if len(before) == 0 && len(after) == 0 && len(t.ghost) > 0 {
w.CPrint(tui.ColInput.WithAttr(tui.Dim), t.ghost)
return
}
color := tui.ColInput color := tui.ColInput
if t.paused { if t.paused {
color = tui.ColDisabled color = tui.ColDisabled
@ -2434,6 +2509,10 @@ func (t *Terminal) printInfoImpl() {
outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false) outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false)
} }
shiftLen := t.queryLen[0] + t.queryLen[1] + 1
if shiftLen == 1 && len(t.ghost) > 0 {
shiftLen = util.StringWidth(t.ghost)
}
switch t.infoStyle { switch t.infoStyle {
case infoDefault: case infoDefault:
if !move(line+1, 0, t.separatorLen == 0) { if !move(line+1, 0, t.separatorLen == 0) {
@ -2447,9 +2526,9 @@ func (t *Terminal) printInfoImpl() {
return return
} }
case infoInlineRight: case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 pos = t.promptLen + shiftLen
case infoInline: case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 pos = t.promptLen + shiftLen
printInfoPrefix() printInfoPrefix()
} }
@ -2579,6 +2658,9 @@ func (t *Terminal) headerIndent(borderShape tui.BorderShape) int {
} }
if borderShape.HasLeft() { if borderShape.HasLeft() {
indentSize -= 1 + t.borderWidth indentSize -= 1 + t.borderWidth
if indentSize < 0 {
indentSize = 0
}
} }
return indentSize return indentSize
} }
@ -2713,15 +2795,30 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
item := result.item item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := "" label := ""
extraWidth := 0
alt := false
altBg := t.theme.AltBg
selectedBg := selected && t.theme.SelectedBg != t.theme.ListBg
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
if index < len(t.jumpLabels) { if index < len(t.jumpLabels) {
// Striped // Striped
current = index%2 == 0 if !altBg.IsColorDefined() {
label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1) altBg = t.theme.DarkBg
alt = index%2 == 0
} else {
alt = index%2 == 1
} }
} else if current { label = t.jumpLabels[index:index+1] + strings.Repeat(" ", util.Max(0, t.pointerLen-1))
if t.pointerLen == 0 {
extraWidth = 1
}
}
} else {
if current {
label = t.pointer label = t.pointer
} }
alt = !selectedBg && altBg.IsColorDefined() && index%2 == 1
}
// Avoid unnecessary redraw // Avoid unnecessary redraw
numLines, _ := t.numItemLines(item, maxLine-line+1) numLines, _ := t.numItemLines(item, maxLine-line+1)
@ -2746,10 +2843,13 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) { postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) {
if (current || selected) && t.highlightLine { width += extraWidth
if (current || selected || alt) && t.highlightLine {
color := tui.ColSelected color := tui.ColSelected
if current { if current {
color = tui.ColCurrent color = tui.ColCurrent
} else if alt {
color = color.WithBg(altBg)
} }
fillSpaces := maxWidth - width fillSpaces := maxWidth - width
if wrapped { if wrapped {
@ -2847,6 +2947,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
base = tui.ColNormal base = tui.ColNormal
match = tui.ColMatch match = tui.ColMatch
} }
if alt {
base = base.WithBg(altBg)
match = match.WithBg(altBg)
}
finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask) finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
} }
for i := 0; i < t.gap && finalLineNum < maxLine; i++ { for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
@ -2932,7 +3036,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
} }
if !wholeCovered && t.nthAttr > 0 { if !wholeCovered && t.nthAttr > 0 {
var tokens []Token var tokens []Token
if item.transformed != nil { if item.transformed != nil && item.transformed.revision == t.merger.revision {
tokens = item.transformed.tokens tokens = item.transformed.tokens
} else { } else {
tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent) tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
@ -2945,7 +3049,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
sort.Sort(ByOrder(nthOffsets)) sort.Sort(ByOrder(nthOffsets))
} }
} }
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, current) allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr)
maxLines := 1 maxLines := 1
if t.canSpanMultiLines() { if t.canSpanMultiLines() {
@ -3058,13 +3162,20 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
maxWidth := t.window.Width() - (indentSize + 1) maxWidth := t.window.Width() - (indentSize + 1)
wasWrapped := false wasWrapped := false
if wrapped { if wrapped {
wrapSign := t.wrapSign
if maxWidth < t.wrapSignWidth {
runes, _ := util.Truncate(wrapSign, maxWidth)
wrapSign = string(runes)
maxWidth = 0
} else {
maxWidth -= t.wrapSignWidth maxWidth -= t.wrapSignWidth
t.window.CPrint(colBase.WithAttr(tui.Dim), t.wrapSign) }
t.window.CPrint(colBase.WithAttr(tui.Dim), wrapSign)
wrapped = false wrapped = false
wasWrapped = true wasWrapped = true
} }
if len(line) > 0 && line[len(line)-1] == '\n' { if len(line) > 0 && line[len(line)-1] == '\n' && lineOffset < len(lines)-1 {
line = line[:len(line)-1] line = line[:len(line)-1]
} else { } else {
wrapped = true wrapped = true
@ -3124,7 +3235,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
displayWidth = t.displayWidthWithLimit(line, 0, displayWidth) displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
} }
if maxWidth > 0 {
t.printColoredString(t.window, line, offsets, colBase) t.printColoredString(t.window, line, offsets, colBase)
}
if postTask != nil { if postTask != nil {
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw) postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw)
} else { } else {
@ -3333,8 +3446,10 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
wiped := false wiped := false
image := false image := false
wireframe := false wireframe := false
var index int
var line string
Loop: Loop:
for _, line := range lines { for index, line = range lines {
var lbg tui.Color = -1 var lbg tui.Color = -1
if ansi != nil { if ansi != nil {
ansi.lbg = -1 ansi.lbg = -1
@ -3477,6 +3592,7 @@ Loop:
} }
lineNo++ lineNo++
} }
t.previewer.scrollable = t.previewer.scrollable || index < len(lines)-1
t.previewed.image = image t.previewed.image = image
t.previewed.wireframe = wireframe t.previewed.wireframe = wireframe
} }
@ -3698,6 +3814,8 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags.number = true flags.number = true
case 'f': case 'f':
flags.file = true flags.file = true
case 'r':
flags.raw = true
case 'q': case 'q':
flags.forceUpdate = true flags.forceUpdate = true
trimmed += string(char) trimmed += string(char)
@ -3825,7 +3943,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
elems, prefixLength := awkTokenizer(params.query) elems, prefixLength := awkTokenizer(params.query)
tokens := withPrefixLengths(elems, prefixLength) tokens := withPrefixLengths(elems, prefixLength)
trans := Transform(tokens, nth) trans := Transform(tokens, nth)
result := joinTokens(trans) result := JoinTokens(trans)
if !flags.preserveSpace { if !flags.preserveSpace {
result = strings.TrimSpace(result) result = strings.TrimSpace(result)
} }
@ -3849,7 +3967,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
return "''" return "''"
} }
return strconv.Itoa(int(n)) return strconv.Itoa(int(n))
case flags.file: case flags.file || flags.raw:
return item.AsString(params.stripAnsi) return item.AsString(params.stripAnsi)
default: default:
return params.executor.QuoteEntry(item.AsString(params.stripAnsi)) return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
@ -3875,7 +3993,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
replace = func(item *Item) string { replace = func(item *Item) string {
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter) tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
trans := Transform(tokens, ranges) trans := Transform(tokens, ranges)
str := joinTokens(trans) str := JoinTokens(trans)
// trim the last delimiter // trim the last delimiter
if params.delimiter.str != nil { if params.delimiter.str != nil {
@ -3891,7 +4009,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
if !flags.preserveSpace { if !flags.preserveSpace {
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
} }
if !flags.file { if !flags.file && !flags.raw {
str = params.executor.QuoteEntry(str) str = params.executor.QuoteEntry(str)
} }
return str return str
@ -3952,7 +4070,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.executing.Set(true) t.executing.Set(true)
if !background { if !background {
// Open a separate handle for tty input // Open a separate handle for tty input
if in, _ := tui.TtyIn(); in != nil { if in, _ := tui.TtyIn(t.ttyDefault); in != nil {
cmd.Stdin = in cmd.Stdin = in
if in != os.Stdin { if in != os.Stdin {
defer in.Close() defer in.Close()
@ -3961,7 +4079,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
if !util.IsTty(os.Stdout) { if !util.IsTty(os.Stdout) {
if out, _ := tui.TtyOut(); out != nil { if out, _ := tui.TtyOut(t.ttyDefault); out != nil {
cmd.Stdout = out cmd.Stdout = out
defer out.Close() defer out.Close()
} }
@ -3969,7 +4087,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if !util.IsTty(os.Stderr) { if !util.IsTty(os.Stderr) {
if out, _ := tui.TtyOut(); out != nil { if out, _ := tui.TtyOut(t.ttyDefault); out != nil {
cmd.Stderr = out cmd.Stderr = out
defer out.Close() defer out.Close()
} }
@ -4335,6 +4453,7 @@ func (t *Terminal) Loop() error {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var env []string var env []string
var query string
initialOffset := 0 initialOffset := 0
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
@ -4348,6 +4467,7 @@ func (t *Terminal) Loop() error {
initialOffset = request.scrollOffset initialOffset = request.scrollOffset
items = request.list items = request.list
env = request.env env = request.env
query = request.query
} }
} }
events.Clear() events.Clear()
@ -4361,8 +4481,7 @@ func (t *Terminal) Loop() error {
version++ version++
// We don't display preview window if no match // We don't display preview window if no match
if items[0] != nil { if items[0] != nil {
_, query := t.Input() command, tempFiles := t.replacePlaceholder(commandTemplate, false, query, items)
command, tempFiles := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := t.executor.ExecCommand(command, true) cmd := t.executor.ExecCommand(command, true)
cmd.Env = env cmd.Env = env
@ -4490,7 +4609,7 @@ func (t *Terminal) Loop() error {
if len(command) > 0 && t.canPreview() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview()}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
} }
} }
@ -4544,11 +4663,7 @@ func (t *Terminal) Loop() error {
// U t.uiMutex | // U t.uiMutex |
t.uiMutex.Lock() t.uiMutex.Lock()
t.mutex.Lock() t.mutex.Lock()
printInfo := util.RunOnce(func() { info := false
if !t.resizeIfNeeded() {
t.printInfo()
}
})
for _, key := range keys { for _, key := range keys {
req := util.EventType(key) req := util.EventType(key)
value := (*events)[req] value := (*events)[req]
@ -4556,16 +4671,15 @@ func (t *Terminal) Loop() error {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.infoStyle == infoInline || t.infoStyle == infoInlineRight { if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
printInfo() info = true
} }
case reqInfo: case reqInfo:
printInfo() info = true
case reqList: case reqList:
t.printList() t.printList()
currentIndex := t.currentIndex() currentIndex := t.currentIndex()
focusChanged := focusedIndex != currentIndex focusChanged := focusedIndex != currentIndex
info := false if focusChanged && focusedIndex >= 0 && t.track == trackCurrent {
if focusChanged && t.track == trackCurrent {
t.track = trackDisabled t.track = trackDisabled
info = true info = true
} }
@ -4576,9 +4690,6 @@ func (t *Terminal) Loop() error {
info = true info = true
} }
} }
if info {
printInfo()
}
if focusChanged || version != t.version { if focusChanged || version != t.version {
version = t.version version = t.version
focusedIndex = currentIndex focusedIndex = currentIndex
@ -4670,6 +4781,9 @@ func (t *Terminal) Loop() error {
return return
} }
} }
if info && !t.resizeIfNeeded() {
t.printInfo()
}
t.flush() t.flush()
t.mutex.Unlock() t.mutex.Unlock()
t.uiMutex.Unlock() t.uiMutex.Unlock()
@ -4719,6 +4833,7 @@ func (t *Terminal) Loop() error {
changed := false changed := false
beof := false beof := false
queryChanged := false queryChanged := false
denylist := []int32{}
// Special handling of --sync. Activate the interface on the second tick. // Special handling of --sync. Activate the interface on the second tick.
if loopIndex == 1 && t.deferActivation() { if loopIndex == 1 && t.deferActivation() {
@ -4852,6 +4967,14 @@ func (t *Terminal) Loop() error {
return true return true
} }
doAction = func(a *action) bool { doAction = func(a *action) bool {
// Keep track of the current query before the action is executed,
// so we can restore it when the input section is hidden (--no-input).
// * By doing this, we don't have to add a conditional branch to each
// query modifying action.
// * We restore the query after each action instead of after a set of
// actions to allow changing the query even when the input is hidden
// e.g. fzf --no-input --bind 'space:show-input+change-query(foo)+hide-input'
currentInput := t.input
Action: Action:
switch a.t { switch a.t {
case actIgnore, actStart, actClick: case actIgnore, actStart, actClick:
@ -4875,6 +4998,27 @@ func (t *Terminal) Loop() error {
} }
case actBell: case actBell:
t.tui.Bell() t.tui.Bell()
case actExcludeMulti:
if len(t.selected) > 0 {
for _, item := range t.sortSelected() {
denylist = append(denylist, item.item.Index())
}
// Clear selected items
t.selected = make(map[int32]selectedItem)
t.version++
} else {
item := t.currentItem()
if item != nil {
denylist = append(denylist, item.Index())
}
}
changed = true
case actExclude:
if item := t.currentItem(); item != nil {
denylist = append(denylist, item.Index())
t.deselectItem(item)
changed = true
}
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "") t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
case actExecuteMulti: case actExecuteMulti:
@ -4882,6 +5026,14 @@ func (t *Terminal) Loop() error {
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actBracketedPasteBegin:
current := []rune(t.input)
t.pasting = &current
case actBracketedPasteEnd:
if t.pasting != nil {
queryChanged = string(t.input) != string(*t.pasting)
t.pasting = nil
}
case actTogglePreview, actShowPreview, actHidePreview: case actTogglePreview, actShowPreview, actHidePreview:
var act bool var act bool
switch a.t { switch a.t {
@ -4900,7 +5052,7 @@ func (t *Terminal) Loop() error {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview()}) previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
} }
} else { } else {
// Discard the preview content so that it won't accidentally appear // Discard the preview content so that it won't accidentally appear
@ -5017,38 +5169,61 @@ func (t *Terminal) Loop() error {
header = t.captureLines(a.a) header = t.captureLines(a.a)
} }
if t.changeHeader(header) { if t.changeHeader(header) {
if t.headerWindow != nil {
// Need to resize header window
req(reqFullRedraw)
} else {
req(reqHeader, reqList, reqPrompt, reqInfo) req(reqHeader, reqList, reqPrompt, reqInfo)
}
} else { } else {
req(reqHeader) req(reqHeader)
} }
case actChangeHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel:
t.headerLabelOpts.label = a.a label := a.a
if t.headerBorder != nil { if a.t == actTransformHeaderLabel {
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(a.a, &tui.ColHeaderLabel, false) label = t.captureLine(a.a)
req(reqRedrawHeaderLabel)
} }
case actChangeInputLabel: t.headerLabelOpts.label = label
t.inputLabelOpts.label = a.a t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel)
case actChangeInputLabel, actTransformInputLabel:
label := a.a
if a.t == actTransformInputLabel {
label = t.captureLine(a.a)
}
t.inputLabelOpts.label = label
if t.inputBorder != nil { if t.inputBorder != nil {
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(a.a, &tui.ColInputLabel, false) t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
req(reqRedrawInputLabel) req(reqRedrawInputLabel)
} }
case actChangeListLabel: case actChangeListLabel, actTransformListLabel:
t.listLabelOpts.label = a.a label := a.a
if a.t == actTransformListLabel {
label = t.captureLine(a.a)
}
t.listLabelOpts.label = label
if t.wborder != nil { if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(a.a, &tui.ColListLabel, false) t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel) req(reqRedrawListLabel)
} }
case actChangeBorderLabel: case actChangeBorderLabel, actTransformBorderLabel:
t.borderLabelOpts.label = a.a label := a.a
if a.t == actTransformBorderLabel {
label = t.captureLine(a.a)
}
t.borderLabelOpts.label = label
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actChangePreviewLabel: case actChangePreviewLabel, actTransformPreviewLabel:
t.previewLabelOpts.label = a.a label := a.a
if a.t == actTransformPreviewLabel {
label = t.captureLine(a.a)
}
t.previewLabelOpts.label = label
if t.pborder != nil { if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actTransform: case actTransform:
@ -5056,41 +5231,6 @@ func (t *Terminal) Loop() error {
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
} }
case actTransformHeaderLabel:
label := t.captureLine(a.a)
t.headerLabelOpts.label = label
if t.headerBorder != nil {
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel)
}
case actTransformInputLabel:
label := t.captureLine(a.a)
t.inputLabelOpts.label = label
if t.inputBorder != nil {
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
req(reqRedrawInputLabel)
}
case actTransformListLabel:
label := t.captureLine(a.a)
t.listLabelOpts.label = label
if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel)
}
case actTransformBorderLabel:
label := t.captureLine(a.a)
t.borderLabelOpts.label = label
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actTransformPreviewLabel:
label := t.captureLine(a.a)
t.previewLabelOpts.label = label
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actChangePrompt: case actChangePrompt:
t.promptString = a.a t.promptString = a.a
t.prompt, t.promptLen = t.parsePrompt(a.a) t.prompt, t.promptLen = t.parsePrompt(a.a)
@ -5395,6 +5535,7 @@ func (t *Terminal) Loop() error {
t.scrollOff = t.window.Height() t.scrollOff = t.window.Height()
t.constrain() t.constrain()
t.scrollOff = soff t.scrollOff = soff
req(reqList)
case actJump: case actJump:
t.jumping = jumpEnabled t.jumping = jumpEnabled
req(reqJump) req(reqJump)
@ -5464,9 +5605,11 @@ func (t *Terminal) Loop() error {
req(reqList, reqInfo, reqPrompt, reqHeader) req(reqList, reqInfo, reqPrompt, reqHeader)
case actToggleWrap: case actToggleWrap:
t.wrap = !t.wrap t.wrap = !t.wrap
t.clearNumLinesCache()
req(reqList, reqHeader) req(reqList, reqHeader)
case actToggleMultiLine: case actToggleMultiLine:
t.multiLine = !t.multiLine t.multiLine = !t.multiLine
t.clearNumLinesCache()
req(reqList) req(reqList)
case actToggleHscroll: case actToggleHscroll:
// Force re-rendering of the list // Force re-rendering of the list
@ -5767,7 +5910,7 @@ func (t *Terminal) Loop() error {
if me.Down { if me.Down {
mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input)) mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
if t.inputWindow == nil && my == t.promptLine() && mxCons >= 0 { if !t.inputless && t.inputWindow == nil && my == t.promptLine() && mxCons >= 0 {
// Prompt // Prompt
t.cx = mxCons + t.xoffset t.cx = mxCons + t.xoffset
} else if my >= min { } else if my >= min {
@ -5851,6 +5994,30 @@ func (t *Terminal) Loop() error {
} }
} }
} }
case actChangeGhost, actTransformGhost:
ghost := a.a
if a.t == actTransformGhost {
ghost = t.captureLine(a.a)
}
t.ghost = ghost
if len(t.input) == 0 {
req(reqPrompt)
}
case actChangePointer, actTransformPointer:
pointer := a.a
if a.t == actTransformPointer {
pointer = t.captureLine(a.a)
}
length := uniseg.StringWidth(pointer)
if length <= 2 {
if length != t.pointerLen {
t.forceRerenderList()
}
t.pointer = pointer
t.pointerLen = length
t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
req(reqList)
}
case actChangePreview: case actChangePreview:
if t.previewOpts.command != a.a { if t.previewOpts.command != a.a {
t.previewOpts.command = a.a t.previewOpts.command = a.a
@ -5928,6 +6095,15 @@ func (t *Terminal) Loop() error {
if !processExecution(a.t) { if !processExecution(a.t) {
t.lastAction = a.t t.lastAction = a.t
} }
if t.inputless {
// Always just discard the change
t.input = currentInput
t.cx = len(t.input)
beof = false
} else if string(t.input) != string(currentInput) {
t.inputOverride = nil
}
return true return true
} }
@ -5948,18 +6124,10 @@ func (t *Terminal) Loop() error {
} else if !doActions(actions) { } else if !doActions(actions) {
continue continue
} }
if t.inputless { if !t.inputless {
// Always just discard the change
t.input = previousInput
t.cx = len(t.input)
beof = false
} else {
t.truncateQuery() t.truncateQuery()
} }
queryChanged = string(previousInput) != string(t.input) queryChanged = queryChanged || t.pasting == nil && string(previousInput) != string(t.input)
if queryChanged {
t.inputOverride = nil
}
changed = changed || queryChanged changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) { if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
continue continue
@ -5999,7 +6167,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil reload := changed || newCommand != nil
var reloadRequest *searchRequest var reloadRequest *searchRequest
if reload { if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed} reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()}
} }
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox

View File

@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
result = replacePlaceholderTest("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}}")
// {r}, strip ansi
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {r..}, strip ansi
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholderTest("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}}")
@ -565,7 +573,7 @@ func (item *Item) String() string {
} }
// Helper function to parse, execute and convert "text/template" to string. Panics on error. // Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string { func templateToString(format string, data any) string {
bb := &bytes.Buffer{} bb := &bytes.Buffer{}
err := template.Must(template.New("").Parse(format)).Execute(bb, data) err := template.Must(template.New("").Parse(format)).Execute(bb, data)

View File

@ -6,6 +6,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@ -77,6 +78,11 @@ type Delimiter struct {
str *string str *string
} }
// IsAwk returns true if the delimiter is an AWK-style delimiter
func (d Delimiter) IsAwk() bool {
return d.regex == nil && d.str == nil
}
// String returns the string representation of a Delimiter. // String returns the string representation of a Delimiter.
func (d Delimiter) String() string { func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
@ -211,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
return withPrefixLengths(tokens, 0) return withPrefixLengths(tokens, 0)
} }
func joinTokens(tokens []Token) string { // StripLastDelimiter removes the trailing delimiter and whitespaces
func StripLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 {
lastLoc := locs[len(locs)-1]
if lastLoc[1] == len(str) {
str = str[:lastLoc[0]]
}
}
}
return strings.TrimRightFunc(str, unicode.IsSpace)
}
// JoinTokens concatenates the tokens into a single string
func JoinTokens(tokens []Token) string {
var output bytes.Buffer var output bytes.Buffer
for _, token := range tokens { for _, token := range tokens {
output.WriteString(token.text.ToString()) output.WriteString(token.text.ToString())
@ -229,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars(stringBytes(joinTokens(tokens))) chars := util.ToChars(stringBytes(JoinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {

View File

@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
{ {
ranges, _ := splitNth("1,2,3") ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " { if JoinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges, _ := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
{ {
ranges, _ := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||

View File

@ -84,35 +84,37 @@ func _() {
_ = x[CtrlAlt-73] _ = x[CtrlAlt-73]
_ = x[Invalid-74] _ = x[Invalid-74]
_ = x[Fatal-75] _ = x[Fatal-75]
_ = x[Mouse-76] _ = x[BracketedPasteBegin-76]
_ = x[DoubleClick-77] _ = x[BracketedPasteEnd-77]
_ = x[LeftClick-78] _ = x[Mouse-78]
_ = x[RightClick-79] _ = x[DoubleClick-79]
_ = x[SLeftClick-80] _ = x[LeftClick-80]
_ = x[SRightClick-81] _ = x[RightClick-81]
_ = x[ScrollUp-82] _ = x[SLeftClick-82]
_ = x[ScrollDown-83] _ = x[SRightClick-83]
_ = x[SScrollUp-84] _ = x[ScrollUp-84]
_ = x[SScrollDown-85] _ = x[ScrollDown-85]
_ = x[PreviewScrollUp-86] _ = x[SScrollUp-86]
_ = x[PreviewScrollDown-87] _ = x[SScrollDown-87]
_ = x[Resize-88] _ = x[PreviewScrollUp-88]
_ = x[Change-89] _ = x[PreviewScrollDown-89]
_ = x[BackwardEOF-90] _ = x[Resize-90]
_ = x[Start-91] _ = x[Change-91]
_ = x[Load-92] _ = x[BackwardEOF-92]
_ = x[Focus-93] _ = x[Start-93]
_ = x[One-94] _ = x[Load-94]
_ = x[Zero-95] _ = x[Focus-95]
_ = x[Result-96] _ = x[One-96]
_ = x[Jump-97] _ = x[Zero-97]
_ = x[JumpCancel-98] _ = x[Result-98]
_ = x[ClickHeader-99] _ = x[Jump-99]
_ = x[JumpCancel-100]
_ = x[ClickHeader-101]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648} var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@ -8,6 +8,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -27,9 +28,9 @@ const (
maxInputBuffer = 1024 * 1024 maxInputBuffer = 1024 * 1024
) )
const consoleDevice string = "/dev/tty" const DefaultTtyDevice string = "/dev/tty"
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) Bell() { func (r *LightRenderer) Bell() {
@ -44,8 +45,9 @@ func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true, "") r.stderrInternal(str, true, "")
} }
const CR string = "\x1b[2m␍" const DIM string = "\x1b[2m"
const LF string = "\x1b[2m␊" const CR string = DIM + "␍"
const LF string = DIM + "␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) { func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
@ -94,7 +96,6 @@ func (r *LightRenderer) flushRaw(sequence string) {
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
@ -119,6 +120,7 @@ type LightRenderer struct {
showCursor bool showCursor bool
// Windows only // Windows only
mutex sync.Mutex
ttyinChannel chan byte ttyinChannel chan byte
inHandle uintptr inHandle uintptr
outHandle uintptr outHandle uintptr
@ -140,15 +142,16 @@ type LightWindow struct {
tabstop int tabstop int
fg Color fg Color
bg Color bg Color
wrapSign string
wrapSignWidth int
} }
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) { func NewLightRenderer(ttyDefault string, ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
out, err := openTtyOut() out, err := openTtyOut(ttyDefault)
if err != nil { if err != nil {
out = os.Stderr out = os.Stderr
} }
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
@ -210,7 +213,7 @@ func (r *LightRenderer) Init() error {
} }
} }
r.enableMouse() r.enableModes()
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
@ -268,7 +271,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
c, ok := r.getch(nonblock) c, ok := r.getch(nonblock)
if !nonblock && !ok { if !nonblock && !ok {
r.Close() r.Close()
return nil, errors.New("failed to read " + consoleDevice) return nil, errors.New("failed to read " + DefaultTtyDevice)
} }
retries := 0 retries := 0
@ -459,10 +462,11 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
// Bracketed paste mode: \e[200~ ... \e[201~ // Bracketed paste mode: \e[200~ ... \e[201~
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' { if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
// Immediately discard the sequence from the buffer and reread input *sz = 6
r.buffer = r.buffer[6:] if r.buffer[4] == '0' {
*sz = 0 return Event{BracketedPasteBegin, 0, nil}
return r.GetChar() }
return Event{BracketedPasteEnd, 0, nil}
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
@ -678,7 +682,7 @@ func (r *LightRenderer) rmcup() {
} }
func (r *LightRenderer) Pause(clear bool) { func (r *LightRenderer) Pause(clear bool) {
r.disableMouse() r.disableModes()
r.restoreTerminal() r.restoreTerminal()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
@ -691,12 +695,13 @@ func (r *LightRenderer) Pause(clear bool) {
} }
} }
func (r *LightRenderer) enableMouse() { func (r *LightRenderer) enableModes() {
if r.mouse { if r.mouse {
r.csi("?1000h") r.csi("?1000h")
r.csi("?1002h") r.csi("?1002h")
r.csi("?1006h") r.csi("?1006h")
} }
r.csi("?2004h") // Enable bracketed paste mode
} }
func (r *LightRenderer) disableMouse() { func (r *LightRenderer) disableMouse() {
@ -707,6 +712,11 @@ func (r *LightRenderer) disableMouse() {
} }
} }
func (r *LightRenderer) disableModes() {
r.disableMouse()
r.csi("?2004l")
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
if clear { if clear {
@ -715,7 +725,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else { } else {
r.rmcup() r.rmcup()
} }
r.enableMouse() r.enableModes()
r.flush() r.flush()
} else if sigcont && !r.fullscreen && r.mouse { } else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z): // NOTE: SIGCONT (Coming back from CTRL-Z):
@ -770,11 +780,10 @@ func (r *LightRenderer) Close() {
if !r.showCursor { if !r.showCursor {
r.csi("?25h") r.csi("?25h")
} }
r.disableMouse() r.disableModes()
r.flush() r.flush()
r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true) r.closePlatform()
} }
func (r *LightRenderer) Top() int { func (r *LightRenderer) Top() int {
@ -1105,11 +1114,12 @@ type wrappedLine struct {
displayWidth int displayWidth int
} }
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
lines := []wrappedLine{} lines := []wrappedLine{}
width := 0 width := 0
line := "" line := ""
gr := uniseg.NewGraphemes(input) gr := uniseg.NewGraphemes(input)
max := initialMax
for gr.Next() { for gr.Next() {
rs := gr.Runes() rs := gr.Runes()
str := string(rs) str := string(rs)
@ -1131,6 +1141,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
line = str line = str
prefixLength = 0 prefixLength = 0
width = w width = w
max = initialMax - wrapSignWidth
} }
} }
lines = append(lines, wrappedLine{string(line), width}) lines = append(lines, wrappedLine{string(line), width})
@ -1140,7 +1151,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
func (w *LightWindow) fill(str string, resetCode string) 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, w.wrapSignWidth)
for j, wl := range lines { for j, wl := range lines {
w.stderrInternal(wl.text, false, resetCode) w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth w.posx += wl.displayWidth
@ -1153,6 +1164,18 @@ func (w *LightWindow) fill(str string, resetCode string) 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)
w.renderer.stderr(resetCode) w.renderer.stderr(resetCode)
if len(lines) > 1 {
sign := w.wrapSign
width := w.wrapSignWidth
if width > w.width {
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
sign = string(runes)
width = truncatedWidth
}
w.stderrInternal(DIM+sign, false, resetCode)
w.renderer.stderr(resetCode)
w.Move(w.posy, width)
}
} }
} }
} }
@ -1226,6 +1249,11 @@ func (w *LightWindow) EraseMaybe() bool {
return false return false
} }
func (w *LightWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (r *LightRenderer) HideCursor() { func (r *LightRenderer) HideCursor() {
r.showCursor = false r.showCursor = false
r.csi("?25l") r.csi("?25l")

View File

@ -42,26 +42,35 @@ func (r *LightRenderer) closePlatform() {
r.ttyout.Close() r.ttyout.Close()
} }
func openTty(mode int) (*os.File, error) { func openTty(ttyDefault string, mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, mode, 0) var in *os.File
if err != nil { var err error
if len(ttyDefault) > 0 {
in, err = os.OpenFile(ttyDefault, mode, 0)
}
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, mode, 0); err == nil { if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil return in, nil
} }
} }
return nil, errors.New("failed to open " + consoleDevice) if ttyDefault != DefaultTtyDevice {
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
return in, nil
}
}
return nil, errors.New("failed to open " + DefaultTtyDevice)
} }
return in, nil return in, nil
} }
func openTtyIn() (*os.File, error) { func openTtyIn(ttyDefault string) (*os.File, error) {
return openTty(syscall.O_RDONLY) return openTty(ttyDefault, syscall.O_RDONLY)
} }
func openTtyOut() (*os.File, error) { func openTtyOut(ttyDefault string) (*os.File, error) {
return openTty(syscall.O_WRONLY) return openTty(ttyDefault, syscall.O_WRONLY)
} }
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {

View File

@ -18,6 +18,7 @@ const (
var ( var (
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
counter = uint64(0)
) )
// IsLightRendererSupported checks to see if the Light renderer is supported // IsLightRendererSupported checks to see if the Light renderer is supported
@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
} }
r.inHandle = uintptr(inHandle) r.inHandle = uintptr(inHandle)
r.setupTerminal()
// channel for non-blocking reads. Buffer to make sure // channel for non-blocking reads. Buffer to make sure
// we get the ESC sets: // we get the ESC sets:
r.ttyinChannel = make(chan byte, 1024) r.ttyinChannel = make(chan byte, 1024)
// the following allows for non-blocking IO. r.setupTerminal()
// syscall.SetNonblock() is a NOOP under Windows.
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
_, err := util.Read(fd, b)
if err == nil {
r.ttyinChannel <- b[0]
}
}
}()
return nil return nil
} }
@ -91,27 +76,51 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
} }
func openTtyIn() (*os.File, error) { func openTtyIn(ttyDefault string) (*os.File, error) {
// not used // not used
return nil, nil return nil, nil
} }
func openTtyOut() (*os.File, error) { func openTtyOut(ttyDefault string) (*os.File, error) {
return os.Stderr, nil return os.Stderr, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() {
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
return err windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
// The following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows.
current := counter
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
if _, err := util.Read(fd, b); err == nil {
r.mutex.Lock()
// This condition prevents the goroutine from running after the renderer
// has been closed or paused.
if current != counter {
r.mutex.Unlock()
break
} }
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) r.ttyinChannel <- b[0]
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
r.mutex.Unlock()
}
}
}()
} }
func (r *LightRenderer) restoreTerminal() error { func (r *LightRenderer) restoreTerminal() {
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil { r.mutex.Lock()
return err counter++
} // We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) // e.g. fzf --bind 'enter:execute:less {}'
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
r.mutex.Unlock()
} }
func (r *LightRenderer) Size() TermSize { func (r *LightRenderer) Size() TermSize {

View File

@ -53,6 +53,8 @@ type TcellWindow struct {
uri *string uri *string
params *string params *string
showCursor bool showCursor bool
wrapSign string
wrapSignWidth int
} }
func (w *TcellWindow) Top() int { func (w *TcellWindow) Top() int {
@ -195,6 +197,7 @@ func (r *FullscreenRenderer) initScreen() error {
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
return e return e
} }
s.EnablePaste()
if r.mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
} else { } else {
@ -264,6 +267,11 @@ func (r *FullscreenRenderer) Size() TermSize {
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
case *tcell.EventPaste:
if ev.Start() {
return Event{BracketedPasteBegin, 0, nil}
}
return Event{BracketedPasteEnd, 0, nil}
case *tcell.EventResize: case *tcell.EventResize:
// Ignore the first resize event // Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18 // https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
@ -629,6 +637,11 @@ func (w *TcellWindow) EraseMaybe() bool {
return true return true
} }
func (w *TcellWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (w *TcellWindow) EncloseX(x int) bool { func (w *TcellWindow) EncloseX(x int) bool {
return x >= w.left && x < (w.left+w.width) return x >= w.left && x < (w.left+w.width)
} }
@ -757,11 +770,26 @@ Loop:
// word wrap: // word wrap:
xPos := w.left + w.lastX + lx xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) { if xPos >= w.left+w.width {
w.lastY++ w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
xPos = w.left xPos = w.left
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
wgr := uniseg.NewGraphemes(sign)
for wgr.Next() {
rs := wgr.Runes()
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
lx += uniseg.StringWidth(string(rs))
}
xPos = w.left + lx
} }
yPos := w.top + w.lastY yPos := w.top + w.lastY

View File

@ -10,7 +10,7 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func assert(t *testing.T, context string, got interface{}, want interface{}) bool { func assert(t *testing.T, context string, got any, want any) bool {
if got == want { if got == want {
return true return true
} else { } else {

View File

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

View File

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

View File

@ -103,6 +103,8 @@ const (
Invalid Invalid
Fatal Fatal
BracketedPasteBegin
BracketedPasteEnd
Mouse Mouse
DoubleClick DoubleClick
@ -306,6 +308,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
return dup return dup
} }
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
dup := p
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
return dup.Merge(bgPair)
}
func (p ColorPair) MergeAttr(other ColorPair) ColorPair { func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
return p.WithAttr(other.attr) return p.WithAttr(other.attr)
} }
@ -326,6 +334,7 @@ type ColorTheme struct {
Bg ColorAttr Bg ColorAttr
ListFg ColorAttr ListFg ColorAttr
ListBg ColorAttr ListBg ColorAttr
AltBg ColorAttr
Nth ColorAttr Nth ColorAttr
SelectedFg ColorAttr SelectedFg ColorAttr
SelectedBg ColorAttr SelectedBg ColorAttr
@ -659,6 +668,8 @@ type Window interface {
LinkEnd() LinkEnd()
Erase() Erase()
EraseMaybe() bool EraseMaybe() bool
SetWrapSign(string, int)
} }
type FullscreenRenderer struct { type FullscreenRenderer struct {
@ -731,6 +742,7 @@ func EmptyTheme() *ColorTheme {
Bg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@ -776,6 +788,7 @@ func NoColorTheme() *ColorTheme {
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colDefault, AttrUndefined}, ListFg: ColorAttr{colDefault, AttrUndefined},
ListBg: ColorAttr{colDefault, AttrUndefined}, ListBg: ColorAttr{colDefault, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colDefault, AttrUndefined},
@ -821,6 +834,7 @@ func init() {
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@ -860,6 +874,7 @@ func init() {
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@ -899,6 +914,7 @@ func init() {
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},

View File

@ -189,6 +189,27 @@ func (chars *Chars) TrimTrailingWhitespaces() {
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
} }
func (chars *Chars) TrimSuffix(runes []rune) {
lastIdx := len(chars.slice)
firstIdx := lastIdx - len(runes)
if firstIdx < 0 {
return
}
for i := firstIdx; i < lastIdx; i++ {
char := chars.Get(i)
if char != runes[i-firstIdx] {
return
}
}
chars.slice = chars.slice[0:firstIdx]
}
func (chars *Chars) SliceRight(last int) {
chars.slice = chars.slice[:last]
}
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(runes) return string(runes)
@ -273,9 +294,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
line = line[:len(line)-1] line = line[:len(line)-1]
} }
hasWrapSign := false
for { for {
cols := wrapCols cols := wrapCols
if len(wrapped) > 0 { if hasWrapSign {
cols -= wrapSignWidth cols -= wrapSignWidth
} }
_, overflowIdx := RunesWidth(line, 0, tabstop, cols) _, overflowIdx := RunesWidth(line, 0, tabstop, cols)
@ -288,9 +310,11 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
return wrapped, true return wrapped, true
} }
wrapped = append(wrapped, line[:overflowIdx]) wrapped = append(wrapped, line[:overflowIdx])
hasWrapSign = true
line = line[overflowIdx:] line = line[overflowIdx:]
continue continue
} }
hasWrapSign = false
// Restore trailing '\n' // Restore trailing '\n'
if newline { if newline {

View File

@ -76,7 +76,7 @@ func TestCharsLines(t *testing.T) {
check(true, 100, 3, 1, 1, 8, false) check(true, 100, 3, 1, 1, 8, false)
// With wrap sign (3 + 2) // With wrap sign (3 + 2)
check(true, 100, 3, 2, 1, 12, false) check(true, 100, 3, 2, 1, 10, false)
// With wrap sign (3 + 2) and no multi-line // With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false) check(false, 100, 3, 2, 1, 13, false)

View File

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

View File

@ -238,6 +238,11 @@ class TestCore < TestInteractive
assert_equal %w[5555 55], fzf_output_lines assert_equal %w[5555 55], fzf_output_lines
end end
def test_select_1_accept_nth
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 5555, :'1', :accept_nth, '"{1} // {1}"')}", :Enter
assert_equal ['5555', '55 // 55'], fzf_output_lines
end
def test_exit_0 def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter
assert_equal %w[555555], fzf_output_lines assert_equal %w[555555], fzf_output_lines
@ -827,6 +832,24 @@ class TestCore < TestInteractive
tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) } tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) }
end end
def test_jump_no_pointer
tmux.send_keys "seq 100 | #{FZF} --pointer= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '5 5', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal ' 5', lines[-7] }
end
def test_jump_no_pointer_no_marker
tmux.send_keys "seq 100 | #{FZF} --pointer= --marker= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '55', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal '5', lines[-7] }
end
def test_pointer def test_pointer
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed # Assert that specified pointer is displayed
@ -1609,14 +1632,16 @@ class TestCore < TestInteractive
end end
def test_env_vars def test_env_vars
def to_vars(lines) def env_vars
lines.select { it.start_with?('FZF_') }.to_h do return {} unless File.exist?(tempname)
key, val = it.split('=', 2)
File.readlines(tempname).select { it.start_with?('FZF_') }.to_h do
key, val = it.chomp.split('=', 2)
[key.to_sym, val] [key.to_sym, val]
end end
end end
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window up,99%,noborder --preview 'env | grep ^FZF_ | sort' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
expected = { expected = {
FZF_TOTAL_COUNT: '100', FZF_TOTAL_COUNT: '100',
FZF_MATCH_COUNT: '100', FZF_MATCH_COUNT: '100',
@ -1625,31 +1650,32 @@ class TestCore < TestInteractive
FZF_KEY: '', FZF_KEY: '',
FZF_POS: '1', FZF_POS: '1',
FZF_QUERY: '', FZF_QUERY: '',
FZF_POINTER: '>',
FZF_PROMPT: '> ', FZF_PROMPT: '> ',
FZF_INPUT_STATE: 'hidden' FZF_INPUT_STATE: 'hidden'
} }
tmux.until do |lines| tmux.until do
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do
expected.merge!(FZF_INPUT_STATE: 'enabled', FZF_ACTION: 'show-input', FZF_KEY: 'enter') expected.merge!(FZF_INPUT_STATE: 'enabled', FZF_ACTION: 'show-input', FZF_KEY: 'enter')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.until do |lines| tmux.until do
expected.merge!(FZF_ACTION: 'toggle-down', FZF_KEY: 'tab', FZF_POS: '3', FZF_SELECT_COUNT: '2') expected.merge!(FZF_ACTION: 'toggle-down', FZF_KEY: 'tab', FZF_POS: '3', FZF_SELECT_COUNT: '2')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys '99' tmux.send_keys '99'
tmux.until do |lines| tmux.until do
expected.merge!(FZF_ACTION: 'char', FZF_KEY: '9', FZF_QUERY: '99', FZF_MATCH_COUNT: '1', FZF_POS: '1') expected.merge!(FZF_ACTION: 'char', FZF_KEY: '9', FZF_QUERY: '99', FZF_MATCH_COUNT: '1', FZF_POS: '1')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Space tmux.send_keys :Space
tmux.until do |lines| tmux.until do
expected.merge!(FZF_INPUT_STATE: 'disabled', FZF_ACTION: 'disable-search', FZF_KEY: 'space') expected.merge!(FZF_INPUT_STATE: 'disabled', FZF_ACTION: 'disable-search', FZF_KEY: 'space')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
end end
@ -1665,4 +1691,252 @@ class TestCore < TestInteractive
assert_equal '', File.read(tempname).chomp assert_equal '', File.read(tempname).chomp
end end
end end
def test_exclude_multi
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude-multi,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 1'
end
tmux.send_keys :a
tmux.until do |lines|
assert_includes lines, '> 2'
assert_equal 999, lines.match_count
end
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
tmux.until do |lines|
assert_equal 996, lines.match_count
assert_includes lines, '> 9'
end
tmux.send_keys :b
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :Tab, :Tab, :Tab, :a
tmux.until do |lines|
assert_equal 997, lines.match_count
assert_includes lines, '> 2'
end
tmux.send_keys :c
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 2'
end
# TODO: We should also check the behavior of 'exclude' during reloads
end
def test_exclude
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 1'
end
tmux.send_keys :a
tmux.until do |lines|
assert_includes lines, '> 2'
assert_equal 999, lines.match_count
end
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
tmux.until do |lines|
assert_equal 998, lines.match_count
assert_equal 3, lines.select_count
assert_includes lines, '> 7'
end
tmux.send_keys :b
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal 0, lines.select_count
assert_includes lines, '> 5'
end
tmux.send_keys :Tab, :Tab, :Tab, :a
tmux.until do |lines|
assert_equal 999, lines.match_count
assert_equal 3, lines.select_count
assert_includes lines, '>>3'
end
tmux.send_keys :a
tmux.until do |lines|
assert_equal 998, lines.match_count
assert_equal 2, lines.select_count
assert_includes lines, '>>4'
end
tmux.send_keys :c
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 2'
end
# TODO: We should also check the behavior of 'exclude' during reloads
end
def test_accept_nth
tmux.send_keys %((echo "foo bar baz"; echo "bar baz foo") | #{FZF} --multi --accept-nth 2,2 --sync --bind start:select-all+accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
assert_equal ['bar bar', 'baz baz'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_string_delimiter
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar,bar,foo ,bazfoo'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_regex_delimiter
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter='[:,]+' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_regex_delimiter_strip_last
tmux.send_keys %((echo "foo:,bar:,baz"; echo "foo:,bar:,baz:,qux:,") | #{FZF} --multi --delimiter='[:,]+' --accept-nth 2.. --sync --bind 'load:select-all+accept' > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar:,baz', 'bar:,baz:,qux'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_template
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['[0] 1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
end
end
def test_ghost
tmux.send_keys %(seq 100 | #{FZF} --prompt 'X ' --ghost 'Type in query ...' --bind 'space:change-ghost:Y Z' --bind 'enter:transform-ghost:echo Z Y'), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_includes lines, 'X Type in query ...'
end
tmux.send_keys '100'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, 'X 100'
end
tmux.send_keys 'C-u'
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_includes lines, 'X Type in query ...'
end
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, 'X Y Z' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, 'X Z Y' }
end
def test_ghost_inline
tmux.send_keys %(seq 100 | #{FZF} --info 'inline: Y' --no-separator --prompt 'X ' --ghost 'Type in query ...'), :Enter
tmux.until do |lines|
assert_includes lines, 'X Type in query ... Y100/100'
end
tmux.send_keys '100'
tmux.until do |lines|
assert_includes lines, 'X 100 Y1/100'
end
tmux.send_keys 'C-u'
tmux.until do |lines|
assert_includes lines, 'X Type in query ... Y100/100'
end
end
def test_offset_middle
tmux.send_keys %(seq 1000 | #{FZF} --sync --no-input --reverse --height 5 --scroll-off 0 --bind space:offset-middle), :Enter
line = nil
tmux.until { |lines| line = lines.index('> 1') }
tmux.send_keys :PgDn
tmux.until { |lines| assert_includes lines[line + 4], '> 5' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[line + 2], '> 5' }
end
def test_no_input_query
tmux.send_keys %(seq 1000 | #{FZF} --no-input --query 555 --bind space:toggle-input), :Enter
tmux.until { |lines| assert_includes lines, '> 555' }
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
end
def test_no_input_change_query
tmux.send_keys %(seq 1000 | #{FZF} --multi --query 999 --no-input --bind 'enter:show-input+change-query(555)+hide-input,space:change-query(555)+select'), :Enter
tmux.until { |lines| assert_includes lines, '> 999' }
tmux.send_keys :Space
tmux.until do |lines|
assert_includes lines, '>>999'
refute_includes lines, '> 555'
end
tmux.send_keys :Enter
tmux.until do |lines|
refute_includes lines, '>>999'
assert_includes lines, '> 555'
end
end
def test_search_override_query_in_no_input_mode
tmux.send_keys %(seq 1000 | #{FZF} --sync --no-input --bind 'enter:show-input+change-query(555)+hide-input+search(999),space:search(111)+show-input+change-query(777)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '> 999' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '> 777' }
end
def test_change_pointer
tmux.send_keys %(seq 2 | #{FZF} --bind 'a:change-pointer(a),b:change-pointer(bb),c:change-pointer(),d:change-pointer(ddd)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines, 'a 1' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines, 'bb 1' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines, ' 1' }
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines, 'ddd 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, ' 2' }
end
def test_transform_pointer
tmux.send_keys %(seq 2 | #{FZF} --bind 'a:transform-pointer(echo a),b:transform-pointer(echo bb),c:transform-pointer(),d:transform-pointer(echo ddd)'), :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines, 'a 1' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines, 'bb 1' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines, ' 1' }
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines, 'ddd 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, ' 2' }
end
def test_change_header_on_header_window
tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter
tmux.until { |lines| assert lines.any_include?('foo') }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('bar') }
end
def test_trailing_new_line
tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter
tmux.until { |lines| assert_includes lines, '> foo␊' }
end
end end

View File

@ -52,6 +52,12 @@ class TestFilter < TestBase
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
end end
def test_nth_suffix_match
assert_equal \
'foo,bar,baz',
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
end
def test_with_nth_basic def test_with_nth_basic
writelines(['hello world ', 'byebye']) writelines(['hello world ', 'byebye'])
assert_equal \ assert_equal \
@ -59,6 +65,13 @@ class TestFilter < TestBase
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
end end
def test_with_nth_template
writelines(['hello world ', 'byebye'])
assert_equal \
'hello world ',
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
end
def test_with_nth_ansi def test_with_nth_ansi
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']) writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
assert_equal \ assert_equal \

View File

@ -978,4 +978,59 @@ class TestLayout < TestInteractive
setup setup
end end
end end
def test_change_header_and_label_at_once
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
block = <<~BLOCK
label
header
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_label_trunction
command = <<~CMD
seq 10 | #{FZF} --style full --border --header-lines=1 --preview ':' \\
--border-label "#{'b' * 1000}" \\
--preview-label "#{'p' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--input-label "#{'i' * 1000}" \\
--list-label "#{'l' * 1000}"
CMD
writelines(command.lines.map(&:chomp))
tmux.send_keys("sh #{tempname}", :Enter)
tmux.until do |lines|
text = lines.join
assert_includes text, 'b··'
assert_includes text, 'l··p'
assert_includes text, 'p··'
assert_includes text, 'h··'
assert_includes text, 'i··'
end
end
def test_separator_no_ellipsis
tmux.send_keys %(seq 10 | #{FZF} --separator "$(seq 1000 | tr '\\n' ' ')"), :Enter
tmux.until do |lines|
assert_equal 10, lines.match_count
refute_includes lines.join, '··'
end
end
def test_header_border_no_pointer_and_marker
tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter
block = <<~BLOCK
1
9/9
>
BLOCK
tmux.until { assert_block(block, it) }
end
end end

View File

@ -453,7 +453,7 @@ class TestPreview < TestInteractive
tmux.send_keys 'f' tmux.send_keys 'f'
tmux.until do |lines| tmux.until do |lines|
assert_equal '::', lines[0] assert_equal '::', lines[0]
assert_equal ' 3', lines[1] assert_equal ' 3', lines[1]
end end
end end
@ -527,7 +527,7 @@ class TestPreview < TestInteractive
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count } tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.until do |lines| tmux.until do |lines|
assert_equal ['╭────╮', '│ 10 │', '│ 0 │', '│ 10 │', '│ 1 │'], lines.take(5).map(&:strip) assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
end end
end end
@ -544,4 +544,18 @@ class TestPreview < TestInteractive
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' } tmux.until { |lines| assert_includes lines, '> 2' }
end end
def test_preview_query_should_not_be_affected_by_search
tmux.send_keys "seq 1 | #{FZF} --bind 'change:transform-search(echo {q:1})' --preview 'echo [{q}/{}]'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert lines.any_include?('[1/1]') }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('[1 /1]') }
tmux.send_keys '2'
tmux.until do |lines|
assert lines.any_include?('[1 2/1]')
assert_equal 1, lines.match_count
end
end
end end

View File

@ -73,7 +73,7 @@ module TestShell
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 } lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..] expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
@ -241,7 +241,7 @@ module CompletionTest
tmux.until do |lines| tmux.until do |lines|
assert_equal 1, lines.match_count assert_equal 1, lines.match_count
assert_includes lines, '> 55' assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55' assert_includes lines, '> /tmp/fzf-test/d55/'
end end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] } tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
@ -482,4 +482,36 @@ class TestFish < TestBase
tmux.send_keys "set -g #{name} '#{val}'", :Enter tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare tmux.prepare
end end
def test_ctrl_r_multi
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
block = <<~BLOCK
echo "foo
bar"
echo "bar
foo"
BLOCK
tmux.until do |lines|
block.lines.each_with_index do |line, idx|
assert_includes lines[-6 + idx], line.chomp
end
end
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
block = <<~BLOCK
echo "bar
foo"
echo "foo
bar"
BLOCK
tmux.until do |lines|
assert_equal block.lines.map(&:chomp), lines
end
end
end end