Compare commits

...

317 Commits

Author SHA1 Message Date
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 af8fe918d8.

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
Junegunn Choi
bbe1721a18 0.59.0 2025-02-02 23:39:47 +09:00
Junegunn Choi
c1470a51b8 Update Dockerfile 2025-02-02 23:10:53 +09:00
Junegunn Choi
6ee31d5dc5 Fix failing test case 2025-02-02 17:46:14 +09:00
Junegunn Choi
65d74387e7 Stop processing more actions after a terminal action (accept, abort, etc.) 2025-02-02 16:28:32 +09:00
junegunn
7d0ea599c4 Deploying to master from @ junegunn/fzf@b7795a3dea 🚀 2025-02-02 00:02:12 +00:00
Junegunn Choi
b7795a3dea Fix RuboCop errors 2025-02-02 02:48:04 +09:00
Junegunn Choi
323f6f6202 Fix mode switching example in CHANGELOG 2025-02-02 02:26:13 +09:00
Junegunn Choi
0c61223884 Fix tcell renderer's pause and resume 2025-02-02 02:23:48 +09:00
Junegunn Choi
32234be7a2 FZF_KEY enhancements
* 'enter' instead of 'ctrl-m'
* 'space' instead of ' '
2025-02-02 02:23:47 +09:00
Junegunn Choi
178b49832e Fix {show,hide,toggle}-input and add test cases 2025-02-01 17:23:22 +09:00
Junegunn Choi
18cbb4a84d Display header lines at the top in 'reverse-list' layout 2025-02-01 17:03:59 +09:00
Junegunn Choi
e84afe196a Add {show,hide,toggle}-input and expose $FZF_INPUT_STATE 2025-02-01 17:03:59 +09:00
Junegunn Choi
e1e171a3c4 Add toggle-bind 2025-02-01 17:03:59 +09:00
Junegunn Choi
d075c00015 Fix --layout reverse-list --no-input 2025-02-01 09:28:02 +09:00
Junegunn Choi
6c0ca4a64a Add --no-input to hide the input section (#4210)
Close #2890
Close #1396
 
You can't type in queries in this mode, and the only way to trigger an
fzf search is to use `search(...)` action.

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

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

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

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

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

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #4156

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

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

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

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

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

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

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

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

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

Resolves #3632

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

* Refactor append_line function to improve line existence check.

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

* Fix indentation

---------

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

Close #4107
2024-11-26 17:26:16 +09:00
msabathier
bee80a730f Allow walking multiple root directories (#4109)
Co-authored-by: Martin Sabathier <martin.sabathier.ext@corys.fr>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-11-25 19:25:30 +09:00
Junegunn Choi
ac3e24c99c Export FZF_PREVIEW_* variables to other processes as well
Close #4098
2024-11-24 18:49:10 +09:00
junegunn
e7e852bdb3 Deploying to master from @ junegunn/fzf@2b7f168571 🚀 2024-11-24 00:03:09 +00:00
bitraid
2b7f168571 [fish] Enable keys for scripts that use read
Remove the check that exits when the shell is non-interactive, so that
the key bindings will work for the read command, when used by scripts.
2024-11-18 19:08:34 +09:00
bitraid
5b3da1d878 [fish] Use more native syntax
Mainly, replace [ with test. Also, change $FZF_TMUX check from numeric
to string, so that it won't show error if doesn't contain a number.
2024-11-18 19:08:34 +09:00
bitraid
99f1bc0177 [fish] Format history using builtins if perl is missing 2024-11-18 19:08:34 +09:00
bitraid
ed76f076dd [fish] Replace external commands with builtins
- Use `string collect` instead of cat to get the contents of
  $FZF_DEFAULT_OPTS_FILE. Also, check if the file is readable first.
- Use `string split` instead of cut to set $FISH_MAJOR, $FISH_MINOR.
- Use `string replace` instead of perl to strip leading tabs.
2024-11-18 19:08:34 +09:00
bitraid
4d357d1063 [fish] Improve commandline parsing
- Enable using unescaped quotes for exact-match, exact-boundary-match.
- Enable suffix-exact-match.
- Enable inverse-exact-match, inverse-prefix/suffix-exact-match.
- Allow searching for double quotes and backslashes.
- Combine multiple consecutive slashes into one.
- Workaround for test command bug, allowing $dir or $commandline be a
  single `!`.
2024-11-18 19:08:34 +09:00
junegunn
961ae1541c Deploying to master from @ junegunn/fzf@add1aec685 🚀 2024-11-17 00:02:20 +00:00
Junegunn Choi
add1aec685 0.56.3 2024-11-15 10:06:01 +09:00
LangLangBart
03d6ba7496 fix(zsh): handle backtick trigger edge case (#4090) 2024-11-14 16:07:52 +09:00
LangLangBart
71e4d5cc51 revert(zsh): remove 'fc -RI' call in the history widget (#4093) 2024-11-14 10:38:05 +09:00
Junegunn Choi
215ab48222 0.56.2 2024-11-12 00:57:55 +09:00
林千里
0c64c68781 Fix zsh $+name syntax does not work with setopt ksh_arrays (#4084) 2024-11-12 00:53:36 +09:00
Junegunn Choi
3ec035c68b Fix incorrect overflow detection when --wrap is set
Fix #4083
2024-11-12 00:33:07 +09:00
dependabot[bot]
20c7dcfbca Bump golang.org/x/term from 0.25.0 to 0.26.0 (#4085)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/term/compare/v0.25.0...v0.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-12 00:32:37 +09:00
dependabot[bot]
c1b8780b9c Bump crate-ci/typos from 1.26.0 to 1.27.3 (#4087)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.0 to 1.27.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.26.0...v1.27.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-12 00:32:12 +09:00
Junegunn Choi
64c61603e9 0.56.1 2024-11-10 23:15:35 +09:00
LangLangBart
57c08d925f Enhance command extraction in zsh completion (#4082)
Fix #1992
2024-11-10 17:40:59 +09:00
junegunn
51623a5f6a Deploying to master from @ junegunn/fzf@ca3f6181d7 🚀 2024-11-10 00:02:06 +00:00
Junegunn Choi
ca3f6181d7 page-up/down: undo last up/down if items are skipped
Fix #4069
2024-11-09 11:54:41 +09:00
Junegunn Choi
9c94f9c3d0 Another attempt to fix (half-)page-up/down for multi-line items
Fix #4069
2024-11-08 20:18:42 +09:00
Junegunn Choi
4a85843bcf Fix (half-)page-up/down in the presence of multi-line items
Fix #4069
2024-11-07 22:21:07 +09:00
jaydee-coder
d4d9b99879 Allow specifying '{n}' as the OFFSET in the preview-window flag (#4079)
* fzf: Allow '{n}' to be used as the OFFSET in the preview-window flag

* man: Document using '{n}' as the OFFSET in the preview-window flag
2024-11-04 22:27:59 +09:00
jaydee-coder
6816b7d95b docker: fix dockerfile warnings (#4080)
The following warnings were emitted when running `make docker-test`:
```
 2 warnings found (use docker --debug to expand):
 - LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format (line 11)
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 12)
```

This change fixes both of these 2 trivial warnings.
2024-11-04 22:26:19 +09:00
Junegunn Choi
acdf265d7a Fix reader regression (#4070) 2024-11-03 20:32:26 +09:00
Junegunn Choi
19495eb9bb Remove possible races (#4070) 2024-11-03 20:12:47 +09:00
Junegunn Choi
bacc8609ee Fix characters from previous preview not being cleared
Fix #4075
2024-11-03 15:07:17 +09:00
junegunn
99163f5afa Deploying to master from @ junegunn/fzf@0607227730 🚀 2024-11-03 00:02:14 +00:00
LangLangBart
0607227730 fix(zsh): move 'fc -RI' inside command substitution (#4073)
* fix(zsh): move 'fc -RI' inside command substitution

* chore: follow existing option check format
2024-11-02 10:41:17 +09:00
LangLangBart
d938fdc496 fix(zsh): history loading with shared option (#4071)
Fix #4061
2024-11-01 00:19:47 +09:00
Junegunn Choi
dcb4c3d84a Fix race in reload action
Fix #4070
2024-10-31 19:40:40 +09:00
Junegunn Choi
82ebcd9209 Fix (half-)page-up/down in the presence of multi-line items
Fix #4069
2024-10-30 16:52:42 +09:00
Junegunn Choi
ff1687744d 0.56.0 2024-10-27 12:03:01 +09:00
junegunn
782c870fb2 Deploying to master from @ junegunn/fzf@71fad63829 🚀 2024-10-27 00:02:14 +00:00
Charlie Vieth
71fad63829 Update fastwalk to v1.0.9 to fix handling of disk root paths on Windows (#4063)
Fixes: https://github.com/junegunn/fzf/issues/4027
2024-10-25 23:57:46 +09:00
Junegunn Choi
d65c6101a8 walker: Do not treat '..' as a hidden entry
Thanks to @LangLangBart for the suggested fix

Fix #4048
2024-10-25 13:50:58 +09:00
junegunn
3c40b1bd51 Deploying to master from @ junegunn/fzf@90a8800bb5 🚀 2024-10-20 00:02:15 +00:00
Junegunn Choi
90a8800bb5 Avoid selecting an outdated merger from cache
We cache a merger for partial input as well, because it is automatically
invalidated as soon as the new data comes in.

However, there was a race condition where a cached merger for a partial
input is used even after the input stream was complete. This commit
fixes the problem.

Fix #4034
2024-10-16 00:45:12 +09:00
Thomas Martitz
97f1dae2d1 Use eval to evaluate $post variable as command. (#4023) 2024-10-15 18:00:27 +09:00
dependabot[bot]
e54ec05709 Bump crate-ci/typos from 1.24.1 to 1.26.0 (#4036)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.24.1 to 1.26.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.24.1...v1.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 17:38:14 +09:00
Junegunn Choi
a24eb99679 Fix full line background in preview window 2024-10-15 17:35:11 +09:00
dependabot[bot]
ad113d00b7 Bump golang.org/x/term from 0.24.0 to 0.25.0 (#4031)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/term/compare/v0.24.0...v0.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 16:56:37 +09:00
junegunn
7bd5884d12 Deploying to master from @ junegunn/fzf@c3505858a6 🚀 2024-10-13 00:02:13 +00:00
junegunn
c3505858a6 Deploying to master from @ junegunn/fzf@e76aa37fd4 🚀 2024-10-06 00:02:11 +00:00
Junegunn Choi
e76aa37fd4 Make RuboCop happy 2024-10-01 19:45:53 +09:00
Junegunn Choi
1a32220ca9 Add --gap option to put empty lines between items 2024-10-01 19:15:17 +09:00
75 changed files with 10901 additions and 6057 deletions

View File

@@ -16,7 +16,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -30,19 +30,19 @@ jobs:
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.0
ruby-version: 3.4.1
- name: Install packages
run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
run: bundle install
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
run: make lint
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose

View File

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

2
.gitignore vendored
View File

@@ -3,7 +3,6 @@ bin/fzf.exe
dist
target
pkg
Gemfile.lock
.DS_Store
doc/tags
vendor
@@ -12,3 +11,4 @@ gopath
fzf
tmp
*.patch
.idea

View File

@@ -1,9 +1,13 @@
AllCops:
NewCops: enable
Layout/LineLength:
Enabled: false
Metrics:
Enabled: false
Lint/ShadowingOuterLocalVariable:
Enabled: false
Lint/NestedMethodDefinition:
Enabled: false
Style/MethodCallWithArgsParentheses:
Enabled: true
AllowedMethods:
@@ -28,5 +32,11 @@ Style/WordArray:
MinSize: 1
Minitest/AssertEqual:
Enabled: false
Minitest/EmptyLineBeforeAssertionMethods:
Enabled: false
Naming/VariableNumber:
Enabled: false
Lint/EmptyBlock:
Enabled: false
Style/SafeNavigationChainLength:
Enabled: false

View File

@@ -1 +1,2 @@
golang 1.20.13
ruby 3.4.1

View File

@@ -1,8 +1,8 @@
Advanced fzf examples
======================
* *Last update: 2024/06/24*
* *Requires fzf 0.54.0 or later*
* *Last update: 2025/02/02*
* *Requires fzf 0.59.0 or later*
---
@@ -22,6 +22,7 @@ Advanced fzf examples
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
* [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status)
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
![image](https://user-images.githubusercontent.com/700826/113379932-dfeac200-93b5-11eb-9e28-df1a2ee71f90.png)
*(See `Layout` section of the man page to see the full list of options)*
*(See man page to see the full list of options)*
But you definitely don't want to repeat `--height=40% --layout=reverse
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
@@ -128,7 +129,7 @@ fzf --height 70% --tmux 70%
You can also specify the position, width, and height of the popup window in
the following format:
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]`
```sh
# 100% width and 60% height
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind 'enter:become(vim {1} +{2})'
```
### Controlling Ripgrep search and fzf search simultaneously
`search` and `transform-search` action allow you to trigger an fzf search with
an arbitrary query string. This frees fzf from strictly following the prompt
input, enabling custom search syntax.
In the example below, `transform` action is used to conditionally trigger
`reload` for ripgrep, followed by `search` for fzf. The first word of the
query initiates the Ripgrep process to generate the initial results, while the
remainder of the query is passed to fzf for secondary filtering.
```sh
#!/usr/bin/env bash
export TEMP=$(mktemp -u)
trap 'rm -f "$TEMP"' EXIT
INITIAL_QUERY="${*:-}"
TRANSFORMER='
rg_pat={q:1} # The first word is passed to ripgrep
fzf_pat={q:2..} # The rest are passed to fzf
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
echo "$rg_pat" > "$TEMP"
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
fi
echo "+search:$fzf_pat"
'
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--with-shell 'bash -c' \
--bind "start,change:transform:$TRANSFORMER" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
```
Log tailing
-----------
@@ -529,8 +568,7 @@ pods() {
--info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \
--bind 'start,ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \

View File

@@ -1,6 +1,376 @@
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
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
- Prioritizing file name matches (#4192)
- Added a new tiebreak option `pathname` for prioritizing file name matches
- `--scheme=path` now sets `--tiebreak=pathname,length`
- fzf will automatically choose `path` scheme
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
```sh
# Use --header-lines-border to separate two headers
ps -ef | fzf --style full --layout reverse --header-lines 1 \
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
--header-lines-border bottom --no-list-border
```
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
```sh
# Click on the header line to limit search scope
ps -ef | fzf --style full --layout reverse --header-lines 1 \
--header-lines-border bottom --no-list-border \
--color fg:dim,nth:regular \
--bind 'click-header:transform-nth(
echo $FZF_CLICK_HEADER_NTH
)+transform-prompt(
echo "$FZF_CLICK_HEADER_WORD> "
)'
```
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
- `kill` completion for bash and zsh were updated to use this feature
- Added `--no-input` option to completely disable and hide the input section
```sh
# Click header to trigger search
fzf --header '[src] [test]' --no-input --layout reverse \
--header-border bottom --input-border \
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
# Vim-like mode switch
fzf --layout reverse-list --no-input \
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
--bind 'enter,esc,ctrl-c:transform:
if [[ $FZF_INPUT_STATE = enabled ]]; then
echo "rebind(j,k,/)+hide-input"
elif [[ $FZF_KEY = enter ]]; then
echo accept
else
echo abort
fi
'
```
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
```sh
export TEMP=$(mktemp -u)
trap 'rm -f "$TEMP"' EXIT
TRANSFORMER='
rg_pat={q:1} # The first word is passed to ripgrep
fzf_pat={q:2..} # The rest are passed to fzf
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
echo "$rg_pat" > "$TEMP"
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
fi
echo "+search:$fzf_pat"
'
fzf --ansi --disabled \
--with-shell 'bash -c' \
--bind "start,change:transform:$TRANSFORMER"
```
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
```sh
# Load 'ps -ef' output on start and reload it on CTRL-R
fzf --bind 'start,ctrl-r:reload:ps -ef'
```
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
```sh
# You will only see the input section which takes 3 lines
fzf --style=full --height 1% --min-height 3
# You will see 3 items in the list section
fzf --style full --height 1% --min-height 3+
```
- Shell integration scripts were updated to use `--min-height 20+` by default
- `--header-lines` will be displayed at the top in `reverse-list` layout
- Added `bell` action to ring the terminal bell
```sh
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
```
- Added `toggle-bind` action
- Bug fixes and improvements
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
0.58.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
This version introduces three new border types, `--list-border`, `--input-border`, and `--header-border`, offering much greater flexibility for customizing the user interface.
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-4-borders.png" />
Also, fzf now offers "style presets" for quick customization, which can be activated using the `--style` option.
| Preset | Screenshot |
| :--- | :--- |
| `default` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-default.png"/> |
| `full` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-full.png"/> |
| `minimal` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-minimal.png"/> |
- Style presets (#4160)
- `--style=full[:BORDER_STYLE]`
- `--style=default`
- `--style=minimal`
- Border and label for the list section (#4148)
- Options
- `--list-border[=STYLE]`
- `--list-label=LABEL`
- `--list-label-pos=COL[:bottom]`
- Colors
- `list-fg`
- `list-bg`
- `list-border`
- `list-label`
- Actions
- `change-list-label`
- `transform-list-label`
- Border and label for the input section (prompt line and info line) (#4154)
- Options
- `--input-border[=STYLE]`
- `--input-label=LABEL`
- `--input-label-pos=COL[:bottom]`
- Colors
- `input-fg` (`query`)
- `input-bg`
- `input-border`
- `input-label`
- Actions
- `change-input-label`
- `transform-input-label`
- Border and label for the header section (#4159)
- Options
- `--header-border[=STYLE]`
- `--header-label=LABEL`
- `--header-label-pos=COL[:bottom]`
- Colors
- `header-fg` (`header`)
- `header-bg`
- `header-border`
- `header-label`
- Actions
- `change-header-label`
- `transform-header-label`
- Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
- Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
- fzf will now render a dashed line (`┈┈`) in each `--gap` for better visual separation.
```sh
# All bash/zsh functions, highlighted
declare -f |
perl -0 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --layout reverse --multi --highlight-line --gap
```
* You can customize the line using `--gap-line[=STR]`.
- You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
```sh
fzf --tmux border-native --bind 'enter:execute:less {}'
```
- Added `toggle-multi-line` action
- Added `toggle-hscroll` action
- Added `change-nth` action for dynamically changing the value of the `--nth` option
```sh
# Start with --nth 1, then 2, then 3, then back to the default, 1
echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo
```
- `--nth` parts of each line can now be rendered in a different text style
```sh
# nth in a different style
ls -al | fzf --nth -1 --color nth:italic
ls -al | fzf --nth -1 --color nth:reverse
ls -al | fzf --nth -1 --color nth:reverse:bold
# Dim the other parts
ls -al | fzf --nth -1 --color nth:regular,fg:dim
# With 'change-nth'. The current nth option is exported as $FZF_NTH.
ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
--color nth:regular,fg:dim \
--bind 'ctrl-n:change-nth(8..|1|2|3|4|5|6|7|)' \
--bind 'result:transform-prompt:echo "${FZF_NTH}> "'
```
- A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character.
- This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
- Bug fixes
- Bug fixes and improvements in fish scripts (thanks to @bitraid)
0.57.0
------
- You can now resize the preview window by dragging the border
- Built-in walker improvements
- `--walker-root` can take multiple directory arguments. e.g. `--walker-root include src lib`
- `--walker-skip` can handle multi-component patterns. e.g. `--walker-skip target/build`
- Removed long processing delay when displaying images in the preview window
- `FZF_PREVIEW_*` environment variables are exported to all child processes (#4098)
- Bug fixes in fish scripts
0.56.3
------
- Bug fixes in zsh scripts
- fix(zsh): handle backtick trigger edge case (#4090)
- revert(zsh): remove 'fc -RI' call in the history widget (#4093)
- Thanks to @LangLangBart for the contributions
0.56.2
------
- Bug fixes
- Fixed abnormal scrolling behavior when `--wrap` is set (#4083)
- [zsh] Fixed warning message when `ksh_arrays` is set (#4084)
0.56.1
------
- Bug fixes and improvements
- Fixed a race condition which would cause fzf to present stale results after `reload` (#4070)
- `page-up` and `page-down` actions now work correctly with multi-line items (#4069)
- `{n}` is allowed in `SCROLL` expression in `--preview-window` (#4079)
- [zsh] Fixed regression in history loading with shared option (#4071)
- [zsh] Better command extraction in zsh completion (#4082)
- Thanks to @LangLangBart, @jaydee-coder, @alex-huff, and @vejkse for the contributions
0.56.0
------
- Added `--gap[=N]` option to display empty lines between items.
- This can be useful to visually separate adjacent multi-line items.
```sh
# All bash functions, highlighted
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi --highlight-line --gap
```
- Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
```sh
fzf --info inline-right --gap --color gutter:-1
```
- Added `noinfo` option to `--preview-window` to hide the scroll indicator in the preview window
- Bug fixes
- Thanks to @LangLangBart, @akinomyoga, and @charlievieth for fixing the bugs
0.55.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
@@ -116,7 +486,7 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
```sh
# Now this will work as expected. Previously, this would print an invalid header line.
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
# `load` event would fire and the header would be prematurely updated.
fzf --header 'Loading ...' --header-lines 1 \
--bind 'start:reload:sleep 1; ps -ef' \

View File

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

8
Gemfile Normal file
View File

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

47
Gemfile.lock Normal file
View File

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

View File

@@ -1,4 +1,3 @@
SHELL := bash
GO ?= go
GOOS ?= $(shell $(GO) env GOOS)
@@ -14,7 +13,7 @@ endif
ifeq ($(VERSION),)
$(error Not on git repository; cannot determine $$FZF_VERSION)
endif
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION
@@ -83,12 +82,15 @@ test: $(SOURCES)
github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util
itest:
ruby test/runner.rb
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
lint: $(SOURCES) test/test_go.rb
lint: $(SOURCES) test/*.rb test/lib/*.rb
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
rubocop --require rubocop-minitest --require rubocop-performance
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
install: bin/fzf
@@ -187,4 +189,4 @@ update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all generate build release test bench lint install clean docker docker-test update
.PHONY: all generate build release test itest bench lint install clean docker docker-test update

View File

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

163
README.md

File diff suppressed because one or more lines are too long

View File

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

16
go.mod
View File

@@ -1,20 +1,20 @@
module github.com/junegunn/fzf
require (
github.com/charlievieth/fastwalk v1.0.8
github.com/gdamore/tcell/v2 v2.7.4
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
github.com/charlievieth/fastwalk v1.0.10
github.com/gdamore/tcell/v2 v2.8.1
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.25.0
golang.org/x/term v0.24.0
golang.org/x/sys v0.30.0
golang.org/x/term v0.29.0
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
golang.org/x/text v0.21.0 // indirect
)
go 1.20

60
go.sum
View File

@@ -1,17 +1,18 @@
github.com/charlievieth/fastwalk v1.0.8 h1:uaoH6cAKSk73aK7aKXqs0+bL+J3Txzd3NGH8tRXgHko=
github.com/charlievieth/fastwalk v1.0.8/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/charlievieth/fastwalk v1.0.10 h1:0qUbvA2O+K+X+IrTfZTC0UH2DK5MOA+KjVfStAHUnGg=
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/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -19,15 +20,29 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -35,23 +50,38 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.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/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
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.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

43
install
View File

@@ -2,7 +2,7 @@
set -u
version=0.55.0
version=0.62.0
auto_completion=
key_bindings=
update_config=2
@@ -83,7 +83,7 @@ ask() {
check_binary() {
echo -n " - Checking fzf executable ... "
local output
output=$("$fzf_base"/bin/fzf --version 2>&1)
output=$(FZF_DEFAULT_OPTS= "$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
@@ -295,35 +295,44 @@ EOF
fi
append_line() {
set -e
local update line file pat lno
local update line file pat lines
update="$1"
line="$2"
file="$3"
pat="${4:-}"
lno=""
lines=""
echo "Update $file:"
echo " - $line"
if [ -f "$file" ]; then
if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
lines=$(\grep -nF "$line" "$file")
else
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
lines=$(\grep -nF "$pat" "$file")
fi
fi
if [ -n "$lno" ]; then
echo " - Already exists: line #$lno"
if [ -n "$lines" ]; then
echo " - Already exists:"
sed 's/^/ Line /' <<< "$lines"
update=0
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
echo " - But they all seem to be commented"
ask " - Continue modifying $file?"
update=$?
fi
fi
set -e
if [ "$update" -eq 1 ]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
echo " + Added"
else
if [ $update -eq 1 ]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
echo " + Added"
else
echo " ~ Skipped"
fi
echo " ~ Skipped"
fi
echo
set +e
}

View File

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

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.55"
var version = "0.62"
var revision = "devel"
//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
THE SOFTWARE.
..
.TH fzf\-tmux 1 "Aug 2024" "fzf 0.55.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
fzf\-tmux - open fzf in tmux split pane

File diff suppressed because it is too large Load Diff

View File

@@ -358,7 +358,7 @@ endfunction
function! s:get_color(attr, ...)
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000
@@ -1081,7 +1081,7 @@ endfunction
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = { 'options': ['--multi'] }
let opts = { 'options': ['--multi', '--scheme', 'path'] }
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
if s:is_win && !&shellslash

View File

@@ -37,7 +37,7 @@ bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
@@ -311,12 +311,12 @@ __fzf_generic_path_completion() {
else
if [[ $1 =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
@@ -377,7 +377,7 @@ _fzf_complete() {
selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")
@@ -409,7 +409,33 @@ _fzf_complete_kill() {
}
_fzf_proc_completion() {
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( ${FZF_NTH//,/ } )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=${new_nths[*]}
new_nths=${new_nths// /,}
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
--bind "click-header:transform:$transformer" -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin

View File

@@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
echo -E "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() {
@@ -120,25 +120,18 @@ __fzf_comprun() {
fi
}
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
__fzf_extract_command() {
local token tokens
tokens=(${(z)1})
for token in $tokens; do
token=${(Q)token}
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
echo "$token"
return
fi
done
echo "${tokens[1]}"
# Control completion with the "compstate" parameter, insert and list nothing
compstate[insert]=
compstate[list]=
cmd_word="${(Q)words[1]}"
}
__fzf_generic_path_completion() {
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
local base lbuf compgen fzf_opts suffix tail dir leftover matches
base=$1
lbuf=$2
cmd=$(__fzf_extract_command "$lbuf")
compgen=$3
fzf_opts=$4
suffix=$5
@@ -161,7 +154,7 @@ __fzf_generic_path_completion() {
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
if [[ $compgen =~ dir ]]; then
walker=dir,follow
@@ -170,7 +163,7 @@ __fzf_generic_path_completion() {
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read -r item; do
item="${item%$suffix}$suffix"
echo -n -E "${(q)item} "
@@ -227,10 +220,9 @@ _fzf_complete() {
rest=("$@")
fi
local fifo lbuf cmd matches post
local fifo lbuf matches post
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
lbuf=${rest[0]}
cmd=$(__fzf_extract_command "$lbuf")
post="${funcstack[1]}_post"
type $post > /dev/null 2>&1 || post=cat
@@ -238,7 +230,7 @@ _fzf_complete() {
matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
@@ -298,7 +290,33 @@ _fzf_complete_unalias() {
}
_fzf_complete_kill() {
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( ${FZF_NTH//,/ } )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=${new_nths[*]}
new_nths=${new_nths// /,}
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
--bind "click-header:transform:$transformer" -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin
@@ -310,7 +328,7 @@ _fzf_complete_kill_post() {
}
fzf-completion() {
local tokens cmd prefix trigger tail matches lbuf d_cmds
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
@@ -321,11 +339,9 @@ fzf-completion() {
return
fi
cmd=$(__fzf_extract_command "$LBUFFER")
# Explicitly allow for empty trigger.
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
@@ -340,16 +356,37 @@ fzf-completion() {
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
{
cursor_pos=$CURSOR
# Move the cursor before the trigger to preserve word array elements when
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
CURSOR=$((cursor_pos - ${#trigger} - 1))
# Check if at least one completion system (old or new) is active.
# If at least one user-defined completion widget is detected, nothing will
# be completed if neither the old nor the new completion system is enabled.
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
zmodload -F zsh/compctl 2>/dev/null
fi
# Create a completion widget to access the 'words' array (man zshcompwid)
zle -C __fzf_extract_command .complete-word __fzf_extract_command
zle __fzf_extract_command
} always {
CURSOR=$cursor_pos
# Delete the completion widget
zle -D __fzf_extract_command 2>/dev/null
}
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
return
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
zle reset-prompt
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" "$lbuf"
else
_fzf_path_completion "$prefix" "$lbuf"
@@ -366,6 +403,7 @@ fzf-completion() {
unset binding
}
# Normal widget
zle -N fzf-completion
bindkey '^I' fzf-completion
fi

View File

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

View File

@@ -11,20 +11,123 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
status is-interactive; or exit 0
# Key bindings
# ------------
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
# command substitution syntax $(cmd) should never be used, even behind a version
# check, otherwise the source command will fail on fish versions older than 3.4.0.
function fzf_key_bindings
# 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
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_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
# Store current token in $dir as root for the 'find' command
@@ -34,59 +137,59 @@ function fzf_key_bindings
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
return
else
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- $prefix
commandline -it -- (string escape $i)
commandline -it -- ' '
end
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
"$FZF_CTRL_T_OPTS --multi --print0")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
commandline -f repaint
end
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
set -l -- command_line (commandline)
set -l -- current_line (commandline -L)
set -l -- total_lines (count $command_line)
set -l -- fzf_query (string escape -- $command_line[$current_line])
# merge history from other sessions before searching
if test -z "$fish_private_mode"
builtin history merge
end
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--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)
# history's -z flag is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
if type -P perl > /dev/null 2>&1
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
and commandline -- $result
else
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
end
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
set FZF_DEFAULT_COMMAND \
'set -l h (builtin history -z --reverse | string split0);' \
'for i in (seq (count $h) -1 1);' \
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end'
end
# Merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
if test "$total_lines" -eq 1
commandline -- (string replace -a -- \n\t \n $result)
else
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
set -l a (math $current_line - 1)
set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
commandline -a -- '' $command_line[$b..-1]
end
end
commandline -f repaint
end
@@ -96,102 +199,32 @@ function fzf_key_bindings
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=dir,follow,hidden --scheme=path" \
"$FZF_ALT_C_OPTS --no-multi --print0")
if [ -n "$result" ]
cd -- $result
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
# Remove last token from commandline.
commandline -t ""
commandline -it -- $prefix
end
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
cd -- $result
commandline -rt -- $prefix
end
commandline -f repaint
end
function __fzfcmd
test -n "$FZF_TMUX"; or set FZF_TMUX 0
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
if [ -n "$FZF_TMUX_OPTS" ]
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if [ $FZF_TMUX -eq 1 ]
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
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"
bind \ct fzf-file-widget
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
end
if bind -M insert > /dev/null 2>&1
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind -M insert \ec fzf-cd-widget
end
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# eval is used to do shell expansion on paths
eval set commandline $commandline
if [ -z $commandline ]
# Default to current directory with no --query
set dir '.'
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ]
# if $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline
else
# Also remove trailing slash after dir, to "split" input properly
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
end
end
echo $dir
echo $fzf_query
echo $prefix
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
set dir $argv
# Strip all trailing slashes. Ignore if $dir is root dir (/)
if [ (string length -- $dir) -gt 1 ]
set dir (string replace -r '/*$' -- '' $dir)
end
# Iteratively check if dir exists and strip tail end of path
while [ ! -d "$dir" ]
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
set dir (dirname -- "$dir")
end
echo $dir
bind -M insert \ec fzf-cd-widget
end
end

View File

@@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
echo -E "${FZF_DEFAULT_OPTS-} $2"
}
# CTRL-T - Paste the selected file path(s) into the command line
@@ -108,9 +108,10 @@ fi
fzf-history-widget() {
local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
# Ensure the associative history array, which maps event numbers to the full
# history lines, is loaded, and that Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
# Ensure the module is loaded if not already, and the required features, such
# as the associative 'history' array, which maps event numbers to full history
# lines, are set. Also, make sure Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \

View File

@@ -12,120 +12,145 @@ func _() {
_ = x[actStart-1]
_ = x[actClick-2]
_ = x[actInvalid-3]
_ = x[actChar-4]
_ = x[actMouse-5]
_ = x[actBeginningOfLine-6]
_ = x[actAbort-7]
_ = x[actAccept-8]
_ = x[actAcceptNonEmpty-9]
_ = x[actAcceptOrPrintQuery-10]
_ = x[actBackwardChar-11]
_ = x[actBackwardDeleteChar-12]
_ = x[actBackwardDeleteCharEof-13]
_ = x[actBackwardWord-14]
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangeMulti-18]
_ = x[actChangePreviewLabel-19]
_ = x[actChangePrompt-20]
_ = x[actChangeQuery-21]
_ = x[actClearScreen-22]
_ = x[actClearQuery-23]
_ = x[actClearSelection-24]
_ = x[actClose-25]
_ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28]
_ = x[actFatal-29]
_ = x[actForwardChar-30]
_ = x[actForwardWord-31]
_ = x[actKillLine-32]
_ = x[actKillWord-33]
_ = x[actUnixLineDiscard-34]
_ = x[actUnixWordRubout-35]
_ = x[actYank-36]
_ = x[actBackwardKillWord-37]
_ = x[actSelectAll-38]
_ = x[actDeselectAll-39]
_ = x[actToggle-40]
_ = x[actToggleSearch-41]
_ = x[actToggleAll-42]
_ = x[actToggleDown-43]
_ = x[actToggleUp-44]
_ = x[actToggleIn-45]
_ = x[actToggleOut-46]
_ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49]
_ = x[actToggleWrap-50]
_ = x[actTrackCurrent-51]
_ = x[actUntrackCurrent-52]
_ = x[actDown-53]
_ = x[actUp-54]
_ = x[actPageUp-55]
_ = x[actPageDown-56]
_ = x[actPosition-57]
_ = x[actHalfPageUp-58]
_ = x[actHalfPageDown-59]
_ = x[actOffsetUp-60]
_ = x[actOffsetDown-61]
_ = x[actOffsetMiddle-62]
_ = x[actJump-63]
_ = x[actJumpAccept-64]
_ = x[actPrintQuery-65]
_ = x[actRefreshPreview-66]
_ = x[actReplaceQuery-67]
_ = x[actToggleSort-68]
_ = x[actShowPreview-69]
_ = x[actHidePreview-70]
_ = x[actTogglePreview-71]
_ = x[actTogglePreviewWrap-72]
_ = x[actTransform-73]
_ = x[actTransformBorderLabel-74]
_ = x[actTransformHeader-75]
_ = x[actTransformPreviewLabel-76]
_ = x[actTransformPrompt-77]
_ = x[actTransformQuery-78]
_ = x[actPreview-79]
_ = x[actChangePreview-80]
_ = x[actChangePreviewWindow-81]
_ = x[actPreviewTop-82]
_ = x[actPreviewBottom-83]
_ = x[actPreviewUp-84]
_ = x[actPreviewDown-85]
_ = x[actPreviewPageUp-86]
_ = x[actPreviewPageDown-87]
_ = x[actPreviewHalfPageUp-88]
_ = x[actPreviewHalfPageDown-89]
_ = x[actPrevHistory-90]
_ = x[actPrevSelected-91]
_ = x[actPrint-92]
_ = x[actPut-93]
_ = x[actNextHistory-94]
_ = x[actNextSelected-95]
_ = x[actExecute-96]
_ = x[actExecuteSilent-97]
_ = x[actExecuteMulti-98]
_ = x[actSigStop-99]
_ = x[actFirst-100]
_ = x[actLast-101]
_ = x[actReload-102]
_ = x[actReloadSync-103]
_ = x[actDisableSearch-104]
_ = x[actEnableSearch-105]
_ = x[actSelect-106]
_ = x[actDeselect-107]
_ = x[actUnbind-108]
_ = x[actRebind-109]
_ = x[actBecome-110]
_ = x[actShowHeader-111]
_ = x[actHideHeader-112]
_ = x[actBracketedPasteBegin-4]
_ = x[actBracketedPasteEnd-5]
_ = x[actChar-6]
_ = x[actMouse-7]
_ = x[actBeginningOfLine-8]
_ = x[actAbort-9]
_ = x[actAccept-10]
_ = x[actAcceptNonEmpty-11]
_ = x[actAcceptOrPrintQuery-12]
_ = x[actBackwardChar-13]
_ = x[actBackwardDeleteChar-14]
_ = x[actBackwardDeleteCharEof-15]
_ = x[actBackwardWord-16]
_ = x[actCancel-17]
_ = x[actChangeBorderLabel-18]
_ = x[actChangeGhost-19]
_ = x[actChangeHeader-20]
_ = x[actChangeHeaderLabel-21]
_ = x[actChangeInputLabel-22]
_ = x[actChangeListLabel-23]
_ = x[actChangeMulti-24]
_ = x[actChangeNth-25]
_ = x[actChangePointer-26]
_ = x[actChangePreview-27]
_ = x[actChangePreviewLabel-28]
_ = x[actChangePreviewWindow-29]
_ = x[actChangePrompt-30]
_ = x[actChangeQuery-31]
_ = x[actClearScreen-32]
_ = x[actClearQuery-33]
_ = x[actClearSelection-34]
_ = x[actClose-35]
_ = x[actDeleteChar-36]
_ = x[actDeleteCharEof-37]
_ = x[actEndOfLine-38]
_ = x[actFatal-39]
_ = x[actForwardChar-40]
_ = x[actForwardWord-41]
_ = x[actKillLine-42]
_ = x[actKillWord-43]
_ = x[actUnixLineDiscard-44]
_ = x[actUnixWordRubout-45]
_ = x[actYank-46]
_ = x[actBackwardKillWord-47]
_ = x[actSelectAll-48]
_ = x[actDeselectAll-49]
_ = x[actToggle-50]
_ = x[actToggleSearch-51]
_ = x[actToggleAll-52]
_ = x[actToggleDown-53]
_ = x[actToggleUp-54]
_ = x[actToggleIn-55]
_ = x[actToggleOut-56]
_ = x[actToggleTrack-57]
_ = x[actToggleTrackCurrent-58]
_ = x[actToggleHeader-59]
_ = x[actToggleWrap-60]
_ = x[actToggleMultiLine-61]
_ = x[actToggleHscroll-62]
_ = x[actTrackCurrent-63]
_ = x[actToggleInput-64]
_ = x[actHideInput-65]
_ = x[actShowInput-66]
_ = x[actUntrackCurrent-67]
_ = x[actDown-68]
_ = x[actUp-69]
_ = x[actPageUp-70]
_ = x[actPageDown-71]
_ = x[actPosition-72]
_ = x[actHalfPageUp-73]
_ = x[actHalfPageDown-74]
_ = x[actOffsetUp-75]
_ = x[actOffsetDown-76]
_ = x[actOffsetMiddle-77]
_ = x[actJump-78]
_ = x[actJumpAccept-79]
_ = x[actPrintQuery-80]
_ = x[actRefreshPreview-81]
_ = x[actReplaceQuery-82]
_ = x[actToggleSort-83]
_ = x[actShowPreview-84]
_ = x[actHidePreview-85]
_ = x[actTogglePreview-86]
_ = x[actTogglePreviewWrap-87]
_ = x[actTransform-88]
_ = x[actTransformBorderLabel-89]
_ = x[actTransformGhost-90]
_ = x[actTransformHeader-91]
_ = x[actTransformHeaderLabel-92]
_ = x[actTransformInputLabel-93]
_ = x[actTransformListLabel-94]
_ = x[actTransformNth-95]
_ = x[actTransformPointer-96]
_ = x[actTransformPreviewLabel-97]
_ = x[actTransformPrompt-98]
_ = x[actTransformQuery-99]
_ = x[actTransformSearch-100]
_ = x[actSearch-101]
_ = x[actPreview-102]
_ = x[actPreviewTop-103]
_ = x[actPreviewBottom-104]
_ = x[actPreviewUp-105]
_ = x[actPreviewDown-106]
_ = x[actPreviewPageUp-107]
_ = x[actPreviewPageDown-108]
_ = x[actPreviewHalfPageUp-109]
_ = x[actPreviewHalfPageDown-110]
_ = x[actPrevHistory-111]
_ = x[actPrevSelected-112]
_ = x[actPrint-113]
_ = x[actPut-114]
_ = x[actNextHistory-115]
_ = x[actNextSelected-116]
_ = x[actExecute-117]
_ = x[actExecuteSilent-118]
_ = x[actExecuteMulti-119]
_ = x[actSigStop-120]
_ = x[actFirst-121]
_ = x[actLast-122]
_ = x[actReload-123]
_ = x[actReloadSync-124]
_ = x[actDisableSearch-125]
_ = x[actEnableSearch-126]
_ = x[actSelect-127]
_ = x[actDeselect-128]
_ = 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 = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
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, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 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 {
if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -401,7 +401,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
if i == 0 {
fmt.Print(" ")
for j := int(f); j <= lastIdx; j++ {
fmt.Printf(" " + string(T[j]) + " ")
fmt.Print(" " + string(T[j]) + " ")
}
fmt.Println()
}
@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
char = unicode.To(unicode.LowerCase, char)
}
}
if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]

View File

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

View File

@@ -44,7 +44,7 @@ func (s *ansiState) ToString() string {
}
ret := ""
if s.attr&tui.Bold > 0 {
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
ret += "1;"
}
if s.attr&tui.Dim > 0 {
@@ -98,11 +98,11 @@ func isPrint(c uint8) bool {
return '\x20' <= c && c <= '\x7e'
}
func matchOperatingSystemCommand(s string) int {
func matchOperatingSystemCommand(s string, start int) int {
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
// ^ match starting here
// ^ match starting here after the first printable character
//
i := 5 // prefix matched in nextAnsiEscapeSequence()
i := start // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s) && isPrint(s[i]); i++ {
}
if i < len(s) {
@@ -156,7 +156,7 @@ func isCtrlSeqStart(c uint8) bool {
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used):
//
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences
i := 0
@@ -191,12 +191,20 @@ Loop:
}
}
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' {
j := 2
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
// ------
for ; i+j < len(s) && isNumeric(s[i+j]); j++ {
}
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
// ---------------
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && isPrint(s[i+j+1]) {
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
return i, i + k
}
}
}
@@ -310,20 +318,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
return trimmed, nil, state
}
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
func parseAnsiCode(s string) (int, string) {
var remaining string
var i int
if delimiter == 0 {
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
}
} else {
i = strings.IndexByte(s, delimiter)
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
}
if i >= 0 {
delimiter = s[i]
remaining = s[i+1:]
s = s[:i]
}
@@ -335,14 +338,14 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
return -1, remaining
}
code = code*10 + int(ch)
}
return code, delimiter, remaining
return code, remaining
}
return -1, delimiter, remaining
return -1, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
@@ -355,12 +358,17 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
state.lbg = prevState.bg
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
state.url = nil
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
stLen := 2
if strings.HasSuffix(ansiCode, "\a") {
stLen = 1
}
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
state.url = nil
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
params := ansiCode[4 : 4+paramsEnd]
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
state.url = &url{uri: uri, params: params}
}
}
@@ -378,11 +386,10 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0
ptr := &state.fg
var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
count++
switch state256 {
case 0:

View File

@@ -335,6 +335,28 @@ func TestExtractColor(t *testing.T) {
assert((*offsets)[0], 0, 6, 2, -1, true)
assert((*offsets)[1], 6, 11, 200, 100, false)
})
state = nil
var color24 tui.Color = (1 << 24) + (180 << 16) + (190 << 8) + 254
src = "\x1b[1mhello \x1b[22;1;38:2:180:190:254mworld"
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 2 {
t.Fail()
}
if state.fg != color24 || state.attr != 1 {
t.Fail()
}
assert((*offsets)[0], 0, 6, -1, -1, true)
assert((*offsets)[1], 6, 11, color24, -1, true)
})
src = "\x1b]133;A\x1b\\hello \x1b]133;C\x1b\\world"
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert((*offsets)[0], 0, 11, color24, -1, true)
})
}
func TestAnsiCodeStringConversion(t *testing.T) {
@@ -381,7 +403,7 @@ func TestParseAnsiCode(t *testing.T) {
{"-2", "", -1},
}
for _, x := range tests {
n, _, s := parseAnsiCode(x.In, 0)
n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
}

View File

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

View File

@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 {
if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, byteString(data))
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
return true
})
} else {
nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
@@ -127,8 +128,7 @@ func Run(opts *Options) (int, error) {
}
}
}
trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans)
transformed := nthTransformer(tokens, itemIndex)
if len(header) < opts.HeaderLines {
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
@@ -172,7 +172,9 @@ func Run(opts *Options) (int, error) {
return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
readyChan := make(chan bool)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
<-readyChan
}
// Matcher
@@ -186,16 +188,37 @@ func Run(opts *Options) (int, error) {
forward = false
case byBegin:
forward = true
case byPathname:
withPos = true
forward = false
}
}
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
nth := opts.Nth
inputRevision := revision{}
snapshotRevision := revision{}
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)
// Filtering mode
@@ -224,7 +247,7 @@ func Run(opts *Options) (int, error) {
}
return false
}, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
@@ -272,6 +295,7 @@ func Run(opts *Options) (int, error) {
// Event coordination
reading := true
ticks := 0
startTick := 0
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
@@ -294,12 +318,18 @@ func Run(opts *Options) (int, error) {
var snapshot []*Chunk
var count int
restart := func(command commandSpec, environ []string) {
if !useSnapshot {
clearDenylist()
}
reading = true
startTick = ticks
chunkList.Clear()
itemIndex = 0
inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ)
readyChan := make(chan bool)
go reader.restart(command, environ, readyChan)
<-readyChan
}
exitCode := ExitOk
@@ -338,7 +368,8 @@ func Run(opts *Options) (int, error) {
} else {
reading = reading && evt == EvtReadNew
}
if useSnapshot && evt == EvtReadFin {
if useSnapshot && evt == EvtReadFin { // reload-sync
clearDenylist()
useSnapshot = false
}
if !useSnapshot {
@@ -369,6 +400,25 @@ func Run(opts *Options) (int, error) {
command = val.command
environ = val.environ
changed = val.changed
bump := false
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
denyMutex.Lock()
for _, itemIndex := range val.denylist {
denylist[itemIndex] = struct{}{}
}
denyMutex.Unlock()
bump = true
}
if val.nth != nil {
// Change nth and clear caches
nth = *val.nth
bump = true
}
if bump {
patternCache = make(map[string]*Pattern)
cache.Clear()
inputRevision.bumpMinor()
}
if command != nil {
useSnapshot = val.sync
}
@@ -429,8 +479,17 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 {
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++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
opts.Printer(transformer(val.Get(i).item))
}
if count == 0 {
exitCode = ExitNoMatch
@@ -452,7 +511,7 @@ func Run(opts *Options) (int, error) {
}
if delay && reading {
dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep,
time.Duration(ticks-startTick)*coordinatorDelayStep,
0, coordinatorDelayMax)
time.Sleep(dur)
}

View File

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

View File

@@ -102,7 +102,7 @@ func (m *Matcher) Loop() {
if !cacheCleared {
if count == prevCount {
// Look up mergerCache
if cached, found := m.mergerCache[patternString]; found {
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
merger = cached
}
} else {

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,13 @@ import (
)
func TestDelimiterRegex(t *testing.T) {
// Valid regex
// Valid regex, but a single character -> string
delim := delimiterRegexp(".")
if delim.regex == nil || delim.str != nil {
if delim.regex != nil || *delim.str != "." {
t.Error(delim)
}
delim = delimiterRegexp("|")
if delim.regex != nil || *delim.str != "|" {
t.Error(delim)
}
// Broken regex -> string
@@ -168,7 +172,7 @@ func TestParseKeys(t *testing.T) {
if len(pairs) != 9 {
t.Error(9)
}
check(tui.CtrlM, "Return")
check(tui.Enter, "Return")
checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab")
check(tui.ShiftTab, "btab")
@@ -191,7 +195,7 @@ func TestParseKeys(t *testing.T) {
check(tui.ShiftLeft, "shift-left")
check(tui.ShiftRight, "shift-right")
check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter")
check(tui.Enter, "Enter")
check(tui.Backspace, "bspace")
}
@@ -329,7 +333,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
}
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
customized, _ = parseTheme(theme, "fg:231,dark bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized")
}

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,8 @@ import (
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
@@ -25,16 +25,26 @@ type Reader struct {
event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
execOut io.ReadCloser
command *string
killed bool
termFunc func()
command *string
wait bool
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
return &Reader{
pusher,
executor,
eventBox,
delimNil,
int32(EvtReady),
make(chan bool, 1),
sync.Mutex{},
false,
func() { os.Stdin.Close() },
nil,
wait}
}
func (r *Reader) startEventPoller() {
@@ -80,19 +90,19 @@ func (r *Reader) fin(success bool) {
func (r *Reader) terminate() {
r.mutex.Lock()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
if r.termFunc != nil {
r.termFunc()
r.termFunc = nil
}
r.mutex.Unlock()
}
func (r *Reader) restart(command commandSpec, environ []string) {
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(command.command, environ)
success := r.readFromCommand(command.command, environ, func() {
readyChan <- true
})
r.fin(success)
removeFiles(command.tempFiles)
}
@@ -111,21 +121,29 @@ func (r *Reader) readChannel(inputChan chan string) bool {
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string) {
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
r.startEventPoller()
var success bool
signalReady := func() {
if readyChan != nil {
readyChan <- true
}
}
if inputChan != nil {
signalReady()
success = r.readChannel(inputChan)
} else if len(initCmd) > 0 {
success = r.readFromCommand(initCmd, initEnv)
success = r.readFromCommand(initCmd, initEnv, signalReady)
} else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores)
signalReady()
success = r.readFiles(roots, opts, ignores)
} else {
success = r.readFromCommand(cmd, initEnv)
success = r.readFromCommand(cmd, initEnv, signalReady)
}
} else {
signalReady()
success = r.readFromStdin()
}
r.fin(success)
@@ -248,14 +266,36 @@ func trimPath(path string) string {
return byteString(bytes)
}
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
r.killed = false
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
conf := fastwalk.Config{
Follow: opts.follow,
// Use forward slashes when running a Windows binary under WSL or MSYS
ToSlash: fastwalk.DefaultToSlash(),
Sort: fastwalk.SortFilesFirst,
}
ignoresBase := []string{}
ignoresFull := []string{}
ignoresSuffix := []string{}
sep := string(os.PathSeparator)
if _, ok := os.LookupEnv("MSYSTEM"); ok {
sep = "/"
}
for _, ignore := range ignores {
if strings.ContainsRune(ignore, os.PathSeparator) {
if strings.HasPrefix(ignore, sep) {
ignoresSuffix = append(ignoresSuffix, ignore)
} else {
// 'foo/bar' should match match
// * 'foo/bar'
// * 'baz/foo/bar'
// * but NOT 'bazfoo/bar'
ignoresFull = append(ignoresFull, ignore)
ignoresSuffix = append(ignoresSuffix, sep+ignore)
}
} else {
ignoresBase = append(ignoresBase, ignore)
}
}
fn := func(path string, de os.DirEntry, err error) error {
if err != nil {
return nil
@@ -265,14 +305,27 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
isDir := de.IsDir()
if isDir || opts.follow && isSymlinkToDir(path, de) {
base := filepath.Base(path)
if !opts.hidden && base[0] == '.' {
if !opts.hidden && base[0] == '.' && base != ".." {
return filepath.SkipDir
}
for _, ignore := range ignores {
for _, ignore := range ignoresBase {
if ignore == base {
return filepath.SkipDir
}
}
for _, ignore := range ignoresFull {
if ignore == path {
return filepath.SkipDir
}
}
for _, ignore := range ignoresSuffix {
if strings.HasSuffix(path, ignore) {
return filepath.SkipDir
}
}
if path != sep {
path += sep
}
}
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
@@ -285,34 +338,39 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
}
return nil
}
return fastwalk.Walk(&conf, root, fn) == nil
noerr := true
for _, root := range roots {
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
}
return noerr
}
func (r *Reader) readFromCommand(command string, environ []string) bool {
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
r.mutex.Lock()
r.killed = false
r.termFunc = nil
r.command = &command
r.exec = r.executor.ExecCommand(command, true)
exec := r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
exec.Env = environ
}
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil {
r.exec = nil
execOut, err := exec.StdoutPipe()
if err != nil || exec.Start() != nil {
signalReady()
r.mutex.Unlock()
return false
}
err = r.exec.Start()
if err != nil {
r.exec = nil
r.mutex.Unlock()
return false
// Function to call to terminate the running command
r.termFunc = func() {
execOut.Close()
util.KillCommand(exec)
}
signalReady()
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil
r.feed(execOut)
return exec.Wait() == nil
}

View File

@@ -23,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
}
// Normal command
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
counter := 0
ready := func() {
counter++
}
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
t.Errorf("%s", strs)
}
@@ -48,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
reader.fin(reader.readFromCommand(`no-such-command`, nil))
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
strs = []string{}
if len(strs) > 0 {
if len(strs) > 0 || counter != 2 {
t.Errorf("%s", strs)
}

View File

@@ -69,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
}
case byLength:
val = item.TrimLength()
case byPathname:
if validOffsetFound {
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
lastDelim := -1
s := item.text.ToString()
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' || s[i] == '\\' {
lastDelim = i
break
}
}
if lastDelim <= minBegin {
val = util.AsUint16(minBegin - lastDelim)
}
}
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
@@ -104,11 +119,11 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
itemColors := result.item.Colors()
// No ANSI codes
if len(itemColors) == 0 {
if len(itemColors) == 0 && len(nthOffsets) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
@@ -118,7 +133,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// Find max column
var maxCol int32
for _, off := range matchOffsets {
for _, off := range append(matchOffsets, nthOffsets...) {
if off[1] > maxCol {
maxCol = off[1]
}
@@ -129,20 +144,29 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
}
}
cols := make([]int, maxCol)
type cellInfo struct {
index int
color bool
match bool
nth bool
}
cols := make([]cellInfo, maxCol)
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // 1-based index of itemColors
cols[i] = cellInfo{colorIndex, true, false, false}
}
}
for _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ {
// Negative of 1-based index of itemColors
// - The extra -1 means highlighted
if cols[i] >= 0 {
cols[i] = cols[i]*-1 - 1
}
cols[i].match = true
}
}
for _, off := range nthOffsets {
for i := off[0]; i < off[1]; i++ {
cols[i].nth = true
}
}
@@ -152,35 +176,32 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
var curr cellInfo = cellInfo{0, false, false, false}
start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
fg := ansi.color.fg
bg := ansi.color.bg
if fg == -1 {
if current {
fg = theme.Current.Color
} else {
fg = theme.Fg.Color
}
fg = colBase.Fg()
}
if bg == -1 {
if current {
bg = theme.DarkBg.Color
} else {
bg = theme.Bg.Color
}
bg = colBase.Bg()
}
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
}
var colors []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr < 0 {
color := colMatch
if (curr.color || curr.nth || curr.match) && idx > start {
if curr.match {
var color tui.ColorPair
if curr.nth {
color = colBase.WithAttr(attrNth).Merge(colMatch)
} else {
color = colBase.Merge(colMatch)
}
var url *url
if curr < -1 && theme.Colored {
ansi := itemColors[-curr-2]
if curr.color && theme.Colored {
ansi := itemColors[curr.index]
url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch)
// hl or hl+ only sets the foreground color, so colMatch is the
@@ -193,19 +214,32 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
if color.Fg().IsDefault() && origColor.HasBg() {
color = origColor
if curr.nth {
color = color.WithAttr(attrNth)
}
} else {
color = origColor.MergeNonDefault(color)
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else {
ansi := itemColors[curr-1]
} else if curr.color {
ansi := itemColors[curr.index]
color := ansiToColorPair(ansi, colBase)
if curr.nth {
color = color.WithAttr(attrNth)
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase),
color: color,
match: false,
url: ansi.color.url})
} else {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: colBase.WithAttr(attrNth),
match: false,
url: nil})
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {r}, strip ansi
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {r..}, strip ansi
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {}, with multiple items
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
@@ -484,7 +492,12 @@ func TestParsePlaceholder(t *testing.T) {
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
`{q}`: `{qq}`,
`{q}`: `{qq}`,
`{q:1}`: `{qq:1}`,
`{q:2..}`: `{qq:2..}`,
`{q:..}`: `{qq:..}`,
`{q:2..-1}`: `{qq:2..-1}`,
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
// IV. escaping placeholder
`\{}`: `{}`,
@@ -507,6 +520,34 @@ func TestParsePlaceholder(t *testing.T) {
}
}
func TestExtractPassthroughs(t *testing.T) {
for _, middle := range []string{
"\x1bPtmux;\x1b\x1bbar\x1b\\",
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
"\x1b]1337;bar\x1b\\",
"\x1b]1337;bar\x1bbar\x1b\\",
"\x1b]1337;bar\a",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
} {
line := "foo" + middle + "baz"
loc := findPassThrough(line)
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
t.Error("failed to find passthrough")
}
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
passthroughs, result := extractPassThroughs(line)
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
t.Error("failed to extract passthroughs")
}
}
}
/* utilities section */
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
@@ -532,7 +573,7 @@ func (item *Item) String() string {
}
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string {
func templateToString(format string, data any) string {
bb := &bytes.Buffer{}
err := template.Must(template.New("").Parse(format)).Execute(bb, data)

View File

@@ -9,13 +9,18 @@ import (
func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
fzf := args[0]
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
if opts.BorderShape == tui.BorderUndefined {
args = append(args, "--border")
fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"}
if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined {
// We append --border option at the end, because `--style=full:STYLE`
// may have changed the default border style.
rest = append(rest, "--border")
}
if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
for _, arg := range append(args, rest...) {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
@@ -33,7 +38,10 @@ func runTmux(args []string, opts *Options) (int, error) {
// M Both The mouse position
// W Both The window position on the status line
// S -y The line above or below the status line
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
tmuxArgs := []string{"display-popup", "-E", "-d", dir}
if !opts.Tmux.border {
tmuxArgs = append(tmuxArgs, "-B")
}
switch opts.Tmux.position {
case posUp:
tmuxArgs = append(tmuxArgs, "-xC", "-y0")

View File

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

View File

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

View File

@@ -11,6 +11,11 @@ func HasFullscreenRenderer() bool {
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
return a | b
}
@@ -18,6 +23,7 @@ const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
Bold = Attr(1)
Dim = Attr(1 << 1)
@@ -30,6 +36,7 @@ const (
)
func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}
@@ -37,6 +44,9 @@ func (r *FullscreenRenderer) PassThrough(string) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Bell() {}
func (r *FullscreenRenderer) HideCursor() {}
func (r *FullscreenRenderer) ShowCursor() {}
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
@@ -48,6 +58,6 @@ func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
return nil
}

View File

@@ -21,7 +21,7 @@ func _() {
_ = x[CtrlJ-10]
_ = x[CtrlK-11]
_ = x[CtrlL-12]
_ = x[CtrlM-13]
_ = x[Enter-13]
_ = x[CtrlN-14]
_ = x[CtrlO-15]
_ = x[CtrlP-16]
@@ -84,35 +84,37 @@ func _() {
_ = x[CtrlAlt-73]
_ = x[Invalid-74]
_ = x[Fatal-75]
_ = x[Mouse-76]
_ = x[DoubleClick-77]
_ = x[LeftClick-78]
_ = x[RightClick-79]
_ = x[SLeftClick-80]
_ = x[SRightClick-81]
_ = x[ScrollUp-82]
_ = x[ScrollDown-83]
_ = x[SScrollUp-84]
_ = x[SScrollDown-85]
_ = x[PreviewScrollUp-86]
_ = x[PreviewScrollDown-87]
_ = x[Resize-88]
_ = x[Change-89]
_ = x[BackwardEOF-90]
_ = x[Start-91]
_ = x[Load-92]
_ = x[Focus-93]
_ = x[One-94]
_ = x[Zero-95]
_ = x[Result-96]
_ = x[Jump-97]
_ = x[JumpCancel-98]
_ = x[ClickHeader-99]
_ = x[BracketedPasteBegin-76]
_ = x[BracketedPasteEnd-77]
_ = x[Mouse-78]
_ = x[DoubleClick-79]
_ = x[LeftClick-80]
_ = x[RightClick-81]
_ = x[SLeftClick-82]
_ = x[SRightClick-83]
_ = x[ScrollUp-84]
_ = x[ScrollDown-85]
_ = x[SScrollUp-86]
_ = x[SScrollDown-87]
_ = x[PreviewScrollUp-88]
_ = x[PreviewScrollDown-89]
_ = x[Resize-90]
_ = x[Change-91]
_ = x[BackwardEOF-92]
_ = x[Start-93]
_ = x[Load-94]
_ = x[Focus-95]
_ = x[One-96]
_ = x[Zero-97]
_ = x[Result-98]
_ = x[Jump-99]
_ = x[JumpCancel-100]
_ = x[ClickHeader-101]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
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 {
if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
@@ -27,11 +28,15 @@ const (
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")
func (r *LightRenderer) Bell() {
r.flushRaw("\a")
}
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
}
@@ -40,8 +45,9 @@ func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true, "")
}
const CR string = "\x1b[2m"
const LF string = "\x1b[2m␊"
const DIM string = "\x1b[2m"
const CR string = DIM + "␍"
const LF string = DIM + "␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str)
@@ -73,14 +79,23 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() {
if r.queued.Len() > 0 {
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
if r.showCursor {
raw += "\x1b[?25h\x1b[?7h"
} else {
raw += "\x1b[?7h"
}
r.flushRaw(raw)
r.queued.Reset()
}
}
func (r *LightRenderer) flushRaw(sequence string) {
fmt.Fprint(r.ttyout, sequence)
}
// Light renderer
type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme
mouse bool
forceBlack bool
@@ -102,8 +117,10 @@ type LightRenderer struct {
y int
x int
maxHeightFunc func(int) int
showCursor bool
// Windows only
mutex sync.Mutex
ttyinChannel chan byte
inHandle uintptr
outHandle uintptr
@@ -112,28 +129,29 @@ type LightRenderer struct {
}
type LightWindow struct {
renderer *LightRenderer
colored bool
preview bool
border BorderStyle
top int
left int
width int
height int
posx int
posy int
tabstop int
fg Color
bg Color
renderer *LightRenderer
colored bool
windowType WindowType
border BorderStyle
top int
left int
width int
height int
posx int
posy int
tabstop int
fg 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) {
out, err := openTtyOut()
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(ttyDefault)
if err != nil {
out = os.Stderr
}
r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
@@ -144,7 +162,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
maxHeightFunc: maxHeightFunc,
showCursor: true}
return &r, nil
}
@@ -170,7 +189,6 @@ func (r *LightRenderer) Init() error {
return err
}
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
if r.fullscreen {
r.smcup()
@@ -195,7 +213,7 @@ func (r *LightRenderer) Init() error {
}
}
r.enableMouse()
r.enableModes()
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
@@ -253,7 +271,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
return nil, errors.New("failed to read " + consoleDevice)
return nil, errors.New("failed to read " + DefaultTtyDevice)
}
retries := 0
@@ -444,10 +462,11 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
// 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] == '~' {
// Immediately discard the sequence from the buffer and reread input
r.buffer = r.buffer[6:]
*sz = 0
return r.GetChar()
*sz = 6
if r.buffer[4] == '0' {
return Event{BracketedPasteBegin, 0, nil}
}
return Event{BracketedPasteEnd, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case '3':
@@ -619,15 +638,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// middle := t & 0b1
left := t&0b11 == 0
// shift := t & 0b100
// ctrl := t & 0b1000
mod := t&0b1100 > 0
drag := t&0b100000 > 0
ctrl := t&0b10000 > 0
alt := t&0b01000 > 0
shift := t&0b00100 > 0
drag := t&0b100000 > 0 // 32
if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, ctrl, alt, shift}}
}
double := false
@@ -651,19 +668,21 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
}
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
}
func (r *LightRenderer) smcup() {
r.csi("?1049h")
r.flush()
r.flushRaw("\x1b[?1049h")
}
func (r *LightRenderer) rmcup() {
r.csi("?1049l")
r.flush()
r.flushRaw("\x1b[?1049l")
}
func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.disableModes()
r.restoreTerminal()
if clear {
if r.fullscreen {
@@ -676,12 +695,13 @@ func (r *LightRenderer) Pause(clear bool) {
}
}
func (r *LightRenderer) enableMouse() {
func (r *LightRenderer) enableModes() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
r.csi("?2004h") // Enable bracketed paste mode
}
func (r *LightRenderer) disableMouse() {
@@ -692,6 +712,11 @@ func (r *LightRenderer) disableMouse() {
}
}
func (r *LightRenderer) disableModes() {
r.disableMouse()
r.csi("?2004l")
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal()
if clear {
@@ -700,7 +725,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else {
r.rmcup()
}
r.enableMouse()
r.enableModes()
r.flush()
} else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z):
@@ -752,11 +777,13 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen {
r.csi("u")
}
r.disableMouse()
if !r.showCursor {
r.csi("?25h")
}
r.disableModes()
r.flush()
r.closePlatform()
r.restoreTerminal()
r.closed.Set(true)
r.closePlatform()
}
func (r *LightRenderer) Top() int {
@@ -774,27 +801,40 @@ func (r *LightRenderer) MaxY() int {
return r.height
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = util.Max(0, width)
height = util.Max(0, height)
w := &LightWindow{
renderer: r,
colored: r.theme.Colored,
preview: preview,
border: borderStyle,
top: top,
left: left,
width: width,
height: height,
tabstop: r.tabstop,
fg: colDefault,
bg: colDefault}
if preview {
w.fg = r.theme.PreviewFg.Color
w.bg = r.theme.PreviewBg.Color
} else {
renderer: r,
colored: r.theme.Colored,
windowType: windowType,
border: borderStyle,
top: top,
left: left,
width: width,
height: height,
tabstop: r.tabstop,
fg: colDefault,
bg: colDefault}
switch windowType {
case WindowBase:
w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color
case WindowList:
w.fg = r.theme.ListFg.Color
w.bg = r.theme.ListBg.Color
case WindowInput:
w.fg = r.theme.Input.Color
w.bg = r.theme.InputBg.Color
case WindowHeader:
w.fg = r.theme.Header.Color
w.bg = r.theme.HeaderBg.Color
case WindowPreview:
w.fg = r.theme.PreviewFg.Color
w.bg = r.theme.PreviewBg.Color
}
if !w.bg.IsDefault() && w.border.shape != BorderNone {
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone {
// fzf --color bg:blue --border --padding 1,2
w.Erase()
}
w.drawBorder(false)
@@ -810,6 +850,9 @@ func (w *LightWindow) DrawHBorder() {
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 {
return
}
switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
w.drawBorderAround(onlyHorizontal)
@@ -839,7 +882,14 @@ func (w *LightWindow) drawBorder(onlyHorizontal bool) {
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color := ColBorder
if w.preview {
switch w.windowType {
case WindowList:
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowPreview:
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
@@ -857,7 +907,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
func (w *LightWindow) drawBorderVertical(left, right bool) {
vw := runeWidth(w.border.left)
color := ColBorder
if w.preview {
switch w.windowType {
case WindowList:
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowPreview:
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
@@ -877,7 +934,14 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0)
color := ColBorder
if w.preview {
switch w.windowType {
case WindowList:
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowPreview:
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
@@ -929,9 +993,6 @@ func (w *LightWindow) Height() int {
func (w *LightWindow) Refresh() {
}
func (w *LightWindow) Close() {
}
func (w *LightWindow) X() int {
return w.posx
}
@@ -940,9 +1001,16 @@ func (w *LightWindow) Y() int {
return w.posy
}
func (w *LightWindow) EncloseX(x int) bool {
return x >= w.left && x < (w.left+w.width)
}
func (w *LightWindow) EncloseY(y int) bool {
return y >= w.top && y < (w.top+w.height)
}
func (w *LightWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
return w.EncloseX(x) && w.EncloseY(y)
}
func (w *LightWindow) Move(y int, x int) {
@@ -965,7 +1033,7 @@ func attrCodes(attr Attr) []string {
if (attr & AttrClear) > 0 {
return codes
}
if (attr & Bold) > 0 {
if (attr&Bold) > 0 || (attr&BoldForce) > 0 {
codes = append(codes, "1")
}
if (attr & Dim) > 0 {
@@ -1046,11 +1114,12 @@ type wrappedLine struct {
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{}
width := 0
line := ""
gr := uniseg.NewGraphemes(input)
max := initialMax
for gr.Next() {
rs := gr.Runes()
str := string(rs)
@@ -1072,6 +1141,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
line = str
prefixLength = 0
width = w
max = initialMax - wrapSignWidth
}
}
lines = append(lines, wrappedLine{string(line), width})
@@ -1081,7 +1151,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n")
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 {
w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth
@@ -1094,10 +1164,22 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0)
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)
}
}
}
}
if w.posx+1 >= w.Width() {
if w.posx >= w.Width() {
if w.posy+1 >= w.height {
return FillSuspend
}
@@ -1166,3 +1248,18 @@ func (w *LightWindow) Erase() {
func (w *LightWindow) EraseMaybe() bool {
return false
}
func (w *LightWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (r *LightRenderer) HideCursor() {
r.showCursor = false
r.csi("?25l")
}
func (r *LightRenderer) ShowCursor() {
r.showCursor = true
r.csi("?25h")
}

View File

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

View File

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

View File

@@ -39,19 +39,22 @@ func (p ColorPair) style() tcell.Style {
type Attr int32
type TcellWindow struct {
color bool
preview bool
top int
left int
width int
height int
normal ColorPair
lastX int
lastY int
moveCursor bool
borderStyle BorderStyle
uri *string
params *string
color bool
windowType WindowType
top int
left int
width int
height int
normal ColorPair
lastX int
lastY int
moveCursor bool
borderStyle BorderStyle
uri *string
params *string
showCursor bool
wrapSign string
wrapSignWidth int
}
func (w *TcellWindow) Top() int {
@@ -72,7 +75,9 @@ func (w *TcellWindow) Height() int {
func (w *TcellWindow) Refresh() {
if w.moveCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
if w.showCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
}
w.moveCursor = false
}
w.lastX = 0
@@ -97,8 +102,21 @@ const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 7)
AttrClear = Attr(1 << 8)
BoldForce = Attr(1 << 10)
)
func (r *FullscreenRenderer) Bell() {
_screen.Beep()
}
func (r *FullscreenRenderer) HideCursor() {
r.showCursor = false
}
func (r *FullscreenRenderer) ShowCursor() {
r.showCursor = true
}
func (r *FullscreenRenderer) PassThrough(str string) {
// No-op
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
@@ -106,8 +124,12 @@ func (r *FullscreenRenderer) PassThrough(str string) {
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme {
s, e := r.getScreen()
if e != nil {
return Default16
}
if s.Colors() >= 256 {
return Dark256
}
return Default16
@@ -137,6 +159,11 @@ func (c Color) Style() tcell.Color {
}
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
return a | b
}
@@ -148,20 +175,34 @@ var (
_initialResize bool = true
)
func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
if _screen == nil {
s, e := tcell.NewScreen()
if e != nil {
return nil, e
}
if !r.showCursor {
s.HideCursor()
}
_screen = s
}
return _screen, nil
}
func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
s, e := r.getScreen()
if e != nil {
return e
}
if e = s.Init(); e != nil {
return e
}
s.EnablePaste()
if r.mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
return nil
}
@@ -174,7 +215,6 @@ func (r *FullscreenRenderer) Init() error {
if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
@@ -227,6 +267,11 @@ func (r *FullscreenRenderer) Size() TermSize {
func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventPaste:
if ev.Start() {
return Event{BracketedPasteBegin, 0, nil}
}
return Event{BracketedPasteEnd, 0, nil}
case *tcell.EventResize:
// Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
@@ -243,7 +288,11 @@ func (r *FullscreenRenderer) GetChar() Event {
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
// dragging has same structure, it only repeats the middle (main) event appropriately
x, y := ev.Position()
mod := ev.Modifiers() != 0
mod := ev.Modifiers()
ctrl := (mod & tcell.ModCtrl) > 0
alt := (mod & tcell.ModAlt) > 0
shift := (mod & tcell.ModShift) > 0
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
prevButton, button := _prevMouseButton, ev.Buttons()
@@ -252,9 +301,9 @@ func (r *FullscreenRenderer) GetChar() Event {
switch {
case button&tcell.WheelDown != 0:
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
case button&tcell.Button1 != 0:
double := false
if !drag {
@@ -277,9 +326,9 @@ func (r *FullscreenRenderer) GetChar() Event {
}
}
// fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
case button&tcell.Button2 != 0:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
default:
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
@@ -288,7 +337,11 @@ func (r *FullscreenRenderer) GetChar() Event {
down := left || button&tcell.Button3 != 0
double := false
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
// No need to report mouse movement events when no button is pressed
if drag {
return Event{Invalid, 0, nil}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
}
// process keyboard:
@@ -515,7 +568,7 @@ func (r *FullscreenRenderer) GetChar() Event {
func (r *FullscreenRenderer) Pause(clear bool) {
if clear {
_screen.Fini()
r.Close()
}
}
@@ -527,6 +580,7 @@ func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
func (r *FullscreenRenderer) Close() {
_screen.Fini()
_screen = nil
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
@@ -537,28 +591,34 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
_screen.Show()
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
normal := ColNormal
if preview {
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = util.Max(0, width)
height = util.Max(0, height)
normal := ColBorder
switch windowType {
case WindowList:
normal = ColNormal
case WindowHeader:
normal = ColHeader
case WindowInput:
normal = ColInput
case WindowPreview:
normal = ColPreview
}
w := &TcellWindow{
color: r.theme.Colored,
preview: preview,
windowType: windowType,
top: top,
left: left,
width: width,
height: height,
normal: normal,
borderStyle: borderStyle}
borderStyle: borderStyle,
showCursor: r.showCursor}
w.Erase()
return w
}
func (w *TcellWindow) Close() {
// TODO
}
func fill(x, y, w, h int, n ColorPair, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
@@ -568,11 +628,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
w.drawBorder(false)
}
@@ -581,9 +637,21 @@ func (w *TcellWindow) EraseMaybe() bool {
return true
}
func (w *TcellWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (w *TcellWindow) EncloseX(x int) bool {
return x >= w.left && x < (w.left+w.width)
}
func (w *TcellWindow) EncloseY(y int) bool {
return y >= w.top && y < (w.top+w.height)
}
func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
return w.EncloseX(x) && w.EncloseY(y)
}
func (w *TcellWindow) Move(y int, x int) {
@@ -673,7 +741,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
@@ -702,11 +770,26 @@ Loop:
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) {
if xPos >= w.left+w.width {
w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0
lx = 0
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
@@ -760,6 +843,9 @@ func (w *TcellWindow) DrawHBorder() {
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 {
return
}
shape := w.borderStyle.shape
if shape == BorderNone {
return
@@ -772,10 +858,17 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
var style tcell.Style
if w.color {
if w.preview {
style = ColPreviewBorder.style()
} else {
switch w.windowType {
case WindowBase:
style = ColBorder.style()
case WindowList:
style = ColListBorder.style()
case WindowHeader:
style = ColHeaderBorder.style()
case WindowInput:
style = ColInputBorder.style()
case WindowPreview:
style = ColPreviewBorder.style()
}
} else {
style = w.normal.style()

View File

@@ -10,7 +10,7 @@ import (
"github.com/junegunn/fzf/src/util"
)
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
func assert(t *testing.T, context string, got any, want any) bool {
if got == want {
return true
} else {
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
// KeyEnter is alias for KeyCR
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
// Ctrl+Alt keys
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
@@ -233,7 +233,7 @@ Quick reference
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
11 1 KeyCtrlK KeyVT = ^K CtrlK
12 1 KeyCtrlL KeyFF = ^L CtrlL
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
14 1 KeyCtrlN KeySO = ^N CtrlN
15 1 KeyCtrlO KeySI = ^O CtrlO
16 1 KeyCtrlP KeyDLE = ^P CtrlP

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ const (
CtrlJ
CtrlK
CtrlL
CtrlM
Enter
CtrlN
CtrlO
CtrlP
@@ -103,6 +103,8 @@ const (
Invalid
Fatal
BracketedPasteBegin
BracketedPasteEnd
Mouse
DoubleClick
@@ -150,12 +152,19 @@ func (e Event) Comparable() Event {
}
func (e Event) KeyName() string {
if me := e.MouseEvent; me != nil {
return me.Name()
}
if e.Type >= Invalid {
return ""
}
switch e.Type {
case Rune:
if e.Char == ' ' {
return "space"
}
return string(e.Char)
case Alt:
return "alt-" + string(e.Char)
@@ -205,10 +214,24 @@ type ColorAttr struct {
Attr Attr
}
func (a ColorAttr) IsColorDefined() bool {
return a.Color != colUndefined
}
func NewColorAttr() ColorAttr {
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
}
func (a ColorAttr) Merge(other ColorAttr) ColorAttr {
if other.Color != colUndefined {
a.Color = other.Color
}
if other.Attr != AttrUndefined {
a.Attr = a.Attr.Merge(other.Attr)
}
return a
}
const (
colUndefined Color = -2
colDefault Color = -1
@@ -285,6 +308,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
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 {
return p.WithAttr(other.attr)
}
@@ -303,6 +332,10 @@ type ColorTheme struct {
Disabled ColorAttr
Fg ColorAttr
Bg ColorAttr
ListFg ColorAttr
ListBg ColorAttr
AltBg ColorAttr
Nth ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
@@ -311,6 +344,9 @@ type ColorTheme struct {
DarkBg ColorAttr
Gutter ColorAttr
Prompt ColorAttr
InputBg ColorAttr
InputBorder ColorAttr
InputLabel ColorAttr
Match ColorAttr
Current ColorAttr
CurrentMatch ColorAttr
@@ -319,13 +355,19 @@ type ColorTheme struct {
Cursor ColorAttr
Marker ColorAttr
Header ColorAttr
HeaderBg ColorAttr
HeaderBorder ColorAttr
HeaderLabel ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
Border ColorAttr
PreviewBorder ColorAttr
PreviewLabel ColorAttr
PreviewScrollbar ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
ListLabel ColorAttr
ListBorder ColorAttr
GapLine ColorAttr
}
type Event struct {
@@ -341,14 +383,46 @@ type MouseEvent struct {
Left bool
Down bool
Double bool
Mod bool
Ctrl bool
Alt bool
Shift bool
}
func (e MouseEvent) Mod() bool {
return e.Ctrl || e.Alt || e.Shift
}
func (e MouseEvent) Name() string {
name := ""
if e.Down {
return name
}
if e.Ctrl {
name += "ctrl-"
}
if e.Alt {
name += "alt-"
}
if e.Shift {
name += "shift-"
}
if e.Double {
name += "double-"
}
if !e.Left {
name += "right-"
}
return name + "click"
}
type BorderShape int
const (
BorderUndefined BorderShape = iota
BorderLine
BorderNone
BorderPhantom
BorderRounded
BorderSharp
BorderBold
@@ -365,7 +439,7 @@ const (
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
@@ -373,7 +447,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false
}
return true
@@ -381,12 +455,24 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool {
switch s {
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false
}
return true
}
func (s BorderShape) HasBottom() bool {
switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
return false
}
return true
}
func (s BorderShape) Visible() bool {
return s != BorderNone
}
type BorderStyle struct {
shape BorderShape
top rune
@@ -402,6 +488,18 @@ type BorderStyle struct {
type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if shape == BorderNone || shape == BorderPhantom {
return BorderStyle{
shape: shape,
top: ' ',
bottom: ' ',
left: ' ',
right: ' ',
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
}
if !unicode {
return BorderStyle{
shape: shape,
@@ -498,19 +596,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
}
}
func MakeTransparentBorder() BorderStyle {
return BorderStyle{
shape: BorderRounded,
top: ' ',
bottom: ' ',
left: ' ',
right: ' ',
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
}
type TermSize struct {
Lines int
Columns int
@@ -518,7 +603,18 @@ type TermSize struct {
PxHeight int
}
type WindowType int
const (
WindowBase WindowType = iota
WindowList
WindowPreview
WindowInput
WindowHeader
)
type Renderer interface {
DefaultTheme() *ColorTheme
Init() error
Resize(maxHeightFunc func(int) int)
Pause(clear bool)
@@ -530,6 +626,9 @@ type Renderer interface {
PassThrough(string)
NeedScrollbarRedraw() bool
ShouldEmitResizeEvent() bool
Bell()
HideCursor()
ShowCursor()
GetChar() Event
@@ -539,7 +638,7 @@ type Renderer interface {
Size() TermSize
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window
}
type Window interface {
@@ -552,10 +651,11 @@ type Window interface {
DrawHBorder()
Refresh()
FinishFill()
Close()
X() int
Y() int
EncloseX(x int) bool
EncloseY(y int) bool
Enclose(y int, x int) bool
Move(y int, x int)
@@ -568,6 +668,8 @@ type Window interface {
LinkEnd()
Erase()
EraseMaybe() bool
SetWrapSign(string, int)
}
type FullscreenRenderer struct {
@@ -576,6 +678,7 @@ type FullscreenRenderer struct {
forceBlack bool
prevDownTime time.Time
clicks [][2]int
showCursor bool
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@@ -584,7 +687,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse,
forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0),
clicks: [][2]int{}}
clicks: [][2]int{},
showCursor: true}
return r
}
@@ -612,8 +716,11 @@ var (
ColSpinner ColorPair
ColInfo ColorPair
ColHeader ColorPair
ColHeaderBorder ColorPair
ColHeaderLabel ColorPair
ColSeparator ColorPair
ColScrollbar ColorPair
ColGapLine ColorPair
ColBorder ColorPair
ColPreview ColorPair
ColPreviewBorder ColorPair
@@ -621,6 +728,10 @@ var (
ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
ColPreviewSpinner ColorPair
ColListBorder ColorPair
ColListLabel ColorPair
ColInputBorder ColorPair
ColInputLabel ColorPair
)
func EmptyTheme() *ColorTheme {
@@ -629,6 +740,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@@ -644,6 +758,8 @@ func EmptyTheme() *ColorTheme {
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
@@ -653,6 +769,14 @@ func EmptyTheme() *ColorTheme {
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
@@ -662,6 +786,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colDefault, AttrUndefined},
ListBg: ColorAttr{colDefault, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
@@ -684,8 +811,18 @@ func NoColorTheme() *ColorTheme {
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
ListLabel: ColorAttr{colDefault, AttrUndefined},
ListBorder: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
InputBg: ColorAttr{colDefault, AttrUndefined},
InputBorder: ColorAttr{colDefault, AttrUndefined},
InputLabel: ColorAttr{colDefault, AttrUndefined},
HeaderBg: ColorAttr{colDefault, AttrUndefined},
HeaderBorder: ColorAttr{colDefault, AttrUndefined},
HeaderLabel: ColorAttr{colDefault, AttrUndefined},
GapLine: ColorAttr{colDefault, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
@@ -695,6 +832,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@@ -717,14 +857,24 @@ func init() {
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
Dark256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@@ -747,14 +897,24 @@ func init() {
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
Light256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
AltBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
@@ -777,12 +937,22 @@ func init() {
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
if forceBlack {
theme.Bg = ColorAttr{colBlack, AttrUndefined}
}
@@ -803,7 +973,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
theme.Current = theme.Fg.Merge(o(baseTheme.Current, theme.Current))
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
@@ -813,9 +985,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
undefined := NewColorAttr()
scrollbarDefined := theme.Scrollbar != undefined
previewBorderDefined := theme.PreviewBorder != undefined
// These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.ListFg = o(theme.Fg, theme.ListFg)
theme.ListBg = o(theme.Bg, theme.ListBg)
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
@@ -823,9 +1001,38 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
theme.ListLabel = o(theme.BorderLabel, theme.ListLabel)
theme.ListBorder = o(theme.Border, theme.ListBorder)
theme.Separator = o(theme.ListBorder, theme.Separator)
theme.Scrollbar = o(theme.ListBorder, theme.Scrollbar)
theme.GapLine = o(theme.ListBorder, theme.GapLine)
/*
--color list-border:green
--color scrollbar:red
--color scrollbar:red,list-border:green
--color scrollbar:red,preview-border:green
*/
if scrollbarDefined && !previewBorderDefined {
theme.PreviewScrollbar = o(theme.Scrollbar, theme.PreviewScrollbar)
} else {
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
}
if hasInputWindow {
theme.InputBg = o(theme.Bg, theme.InputBg)
} else {
// We shouldn't use input-bg if there's no separate input window
// e.g. fzf --color 'list-bg:green,input-bg:red' --no-input-border
theme.InputBg = o(theme.Bg, theme.ListBg)
}
theme.InputBorder = o(theme.Border, theme.InputBorder)
theme.InputLabel = o(theme.BorderLabel, theme.InputLabel)
if hasHeaderWindow {
theme.HeaderBg = o(theme.Bg, theme.HeaderBg)
} else {
theme.HeaderBg = o(theme.Bg, theme.ListBg)
}
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
initPalette(theme)
}
@@ -837,19 +1044,19 @@ func initPalette(theme *ColorTheme) {
}
return ColorPair{fg.Color, bg.Color, fg.Attr}
}
blank := theme.Fg
blank := theme.ListFg
blank.Attr = AttrRegular
ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg)
ColPrompt = pair(theme.Prompt, theme.InputBg)
ColNormal = pair(theme.ListFg, theme.ListBg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColInput = pair(theme.Input, theme.InputBg)
ColDisabled = pair(theme.Disabled, theme.ListBg)
ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
if theme.SelectedBg.Color != theme.Bg.Color {
if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
@@ -860,11 +1067,11 @@ func initPalette(theme *ColorTheme) {
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg)
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColSpinner = pair(theme.Spinner, theme.InputBg)
ColInfo = pair(theme.Info, theme.InputBg)
ColSeparator = pair(theme.Separator, theme.InputBg)
ColScrollbar = pair(theme.Scrollbar, theme.ListBg)
ColGapLine = pair(theme.GapLine, theme.ListBg)
ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
@@ -872,6 +1079,13 @@ func initPalette(theme *ColorTheme) {
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
ColListLabel = pair(theme.ListLabel, theme.ListBg)
ColListBorder = pair(theme.ListBorder, theme.ListBg)
ColInputBorder = pair(theme.InputBorder, theme.InputBg)
ColInputLabel = pair(theme.InputLabel, theme.InputBg)
ColHeader = pair(theme.Header, theme.HeaderBg)
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg)
}
func runeWidth(r rune) int {

View File

@@ -189,6 +189,27 @@ func (chars *Chars) TrimTrailingWhitespaces() {
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 {
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
@@ -273,9 +294,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
line = line[:len(line)-1]
}
hasWrapSign := false
for {
cols := wrapCols
if len(wrapped) > 0 {
if hasWrapSign {
cols -= wrapSignWidth
}
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
@@ -288,9 +310,11 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
return wrapped, true
}
wrapped = append(wrapped, line[:overflowIdx])
hasWrapSign = true
line = line[overflowIdx:]
continue
}
hasWrapSign = false
// Restore trailing '\n'
if newline {
@@ -306,5 +330,5 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
}
}
return wrapped, false
return wrapped, overflow
}

View File

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

View File

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

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

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

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

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

5
test/runner.rb Normal file
View File

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

1942
test/test_core.rb Normal file

File diff suppressed because it is too large Load Diff

417
test/test_exec.rb Normal file
View File

@@ -0,0 +1,417 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Process execution: execute, become, reload
class TestExec < TestInteractive
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
writelines(%w[foo'bar foo"bar foo$bar])
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Up
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Up
tmux.send_keys :C
ready.call('C')
tmux.send_keys 'barfoo'
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
wait do
assert_path_exists output
assert_equal %w[
/foo'bar/ /foo'barfoo'bar/
/foo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/
], File.readlines(output, chomp: true)
end
ensure
FileUtils.rm_f(output)
end
def test_execute_multi
output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
writelines(%w[foo'bar foo"bar foo$bar foobar])
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
tmux.send_keys :Escape, :b
ready.call('alt-b')
tmux.send_keys :Tab, :Tab
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
tmux.send_keys :Escape, :a
ready.call('alt-a')
wait do
assert_path_exists output
assert_equal [
%(foo'bar/foo'bar),
%(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar),
%(foo'bar foo"bar foobar/foo'bar foo"bar foobar)
], File.readlines(output, chomp: true)
end
ensure
FileUtils.rm_f(output)
end
def test_execute_plus_flag
output = tempname + '.tmp'
FileUtils.rm_f(output)
writelines(['foo bar', '123 456'])
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
tmux.send_keys 'xy'
tmux.until { |lines| assert_equal ' 0/2 (0)', lines[-2] }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
tmux.send_keys :Up
tmux.send_keys :Tab
tmux.send_keys 'xy'
tmux.until { |lines| assert_equal ' 0/2 (1)', lines[-2] }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal ' 2/2 (1)', lines[-2] }
tmux.send_keys :Tab
tmux.send_keys 'xy'
tmux.until { |lines| assert_equal ' 0/2 (2)', lines[-2] }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] }
wait do
assert_path_exists output
assert_equal [
%(foo bar/foo bar/bar/bar),
%(123 456/foo bar/456/bar),
%(123 456 foo bar/foo bar/456 bar/bar)
], File.readlines(output, chomp: true)
end
rescue StandardError
FileUtils.rm_f(output)
end
def test_execute_shell
# Custom script to use as $SHELL
output = tempname + '.out'
FileUtils.rm_f(output)
writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
system("chmod +x #{tempname}")
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
wait do
assert_path_exists output
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
end
ensure
FileUtils.rm_f(output)
end
def test_interrupt_execute
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-l'
tmux.until { |lines| assert lines.any_include?('executing 1') }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 99
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_kill_default_command_on_abort
writelines(['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{tempname}")
tmux.send_keys FZF.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{tempname}") }
ensure
system("pkill -9 -f #{tempname}")
end
def test_kill_default_command_on_accept
writelines(['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{tempname}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
assert_equal 'Started', fzf_output
wait { refute system("pgrep -f #{tempname}") }
ensure
system("pkill -9 -f #{tempname}")
end
def test_kill_reload_command_on_abort
writelines(['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{tempname}")
tmux.send_keys "seq 1 3 | #{FZF} --bind 'ctrl-r:reload(#{tempname})'", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{tempname}") }
ensure
system("pkill -9 -f #{tempname}")
end
def test_kill_reload_command_on_accept
writelines(['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{tempname}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{tempname})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
assert_equal 'Started', fzf_output
wait { refute system("pgrep -f #{tempname}") }
ensure
system("pkill -9 -f #{tempname}")
end
def test_reload
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
tmux.until { |lines| assert_equal 998, lines.match_count }
tmux.send_keys 'a'
tmux.until do |lines|
assert_equal 98, lines.item_count
assert_equal 98, lines.match_count
end
tmux.send_keys 'b'
tmux.until do |lines|
assert_equal 198, lines.item_count
assert_equal 198, lines.match_count
end
tmux.send_keys :Tab
tmux.until { |lines| assert_equal ' 198/198 (1/2)', lines[-2] }
tmux.send_keys '555'
tmux.until { |lines| assert_equal ' 1/553 (0/2)', lines[-2] }
end
def test_reload_even_when_theres_no_match
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
tmux.until { |lines| assert_equal 0, lines.item_count }
tmux.send_keys :Space
tmux.until { |lines| assert_equal 10, lines.item_count }
end
def test_reload_should_terminate_standard_input_stream
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
end
def test_clear_list_when_header_lines_changed_due_to_reload
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| assert_includes lines, ' 9' }
tmux.send_keys :Space
tmux.until { |lines| refute_includes lines, ' 9' }
end
def test_item_index_reset_on_reload
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_includes lines[1], '[[0]]'
end
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
end
def test_reload_should_update_preview
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
tmux.until { |lines| assert_includes lines[1], '1' }
tmux.send_keys 'C-t'
tmux.until { |lines| assert_includes lines[1], '4' }
end
def test_reload_and_change_preview_should_update_preview
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '1' }
tmux.send_keys 'C-t'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_includes lines[1], '4' }
end
def test_reload_sync
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys '00'
tmux.until { |lines| assert_equal 1, lines.match_count }
# After 1 second
tmux.until { |lines| assert_equal 10, lines.match_count }
end
def test_reload_disabled_case1
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case2
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case3
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case4
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_equal 1000, lines.match_count }
end
def test_reload_disabled_case5
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.item_count
assert_equal 1, lines.match_count
end
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1001, lines.match_count }
end
def test_reload_disabled_case6
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys '9'
tmux.until { |lines| assert_equal 9, lines.match_count }
tmux.send_keys '9'
tmux.until { |lines| assert_equal 99, lines.match_count }
# TODO: How do we verify if an intermediate empty list is not shown?
end
def test_reload_and_change
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_become_tty
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
tmux.until { |lines| assert_includes lines, '/dev/tty' }
end
def test_disabled_preview_update
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
tmux.send_keys :x
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
end
def test_start_on_reload
tmux.send_keys %(echo foo | #{FZF} --header Loading --header-lines 1 --bind 'start:reload:sleep 2; echo bar' --bind 'load:change-header:Loaded' --bind space:change-header:), :Enter
tmux.until(timeout: 1) { |lines| assert_includes lines[-3], 'Loading' }
tmux.until(timeout: 1) { |lines| refute_includes lines[-4], 'foo' }
tmux.until { |lines| assert_includes lines[-3], 'Loaded' }
tmux.until { |lines| assert_includes lines[-4], 'bar' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[-3], 'bar' }
end
def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 99, lines.item_count }
end
end

315
test/test_filter.rb Normal file
View File

@@ -0,0 +1,315 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Non-interactive tests
class TestFilter < TestBase
def test_default_extended
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
end
def test_exact
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
end
def test_or_operator
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines(chomp: true)
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
`seq 10 | #{FZF} -f '1 | !1'`.lines(chomp: true)
end
def test_smart_case_for_each_term
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
end
def test_filter_exitstatus
# filter / streaming filter
['', '--no-sort'].each do |opts|
assert_includes `echo foo | #{FZF} -f foo #{opts}`, 'foo'
assert_equal 0, $CHILD_STATUS.exitstatus
assert_empty `echo foo | #{FZF} -f bar #{opts}`
assert_equal 1, $CHILD_STATUS.exitstatus
end
end
def test_long_line
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `#{FZF} -f . < #{tempname}`.chomp
end
def test_read0
lines = `find .`.lines(chomp: true)
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
assert_equal \
lines.last,
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
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
writelines(['hello world ', 'byebye'])
assert_equal \
'hello world ',
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
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
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
assert_equal \
'hello world ',
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
end
def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines([src, 'byebye'])
assert_equal \
src,
`#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
end
def test_escaped_meta_characters
input = [
'foo^bar',
'foo$bar',
'foo!bar',
"foo'bar",
'foo bar',
'bar foo'
]
writelines(input)
assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines(chomp: true)
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines(chomp: true)
assert_equal input.length - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.length
end
def test_normalized_match
echoes = '(echo a; echo á; echo A; echo Á;)'
assert_equal %w[a á A Á], `#{echoes} | #{FZF} -f a`.lines.map(&:chomp)
assert_equal %w[á Á], `#{echoes} | #{FZF} -f á`.lines.map(&:chomp)
assert_equal %w[A Á], `#{echoes} | #{FZF} -f A`.lines.map(&:chomp)
assert_equal %w[Á], `#{echoes} | #{FZF} -f Á`.lines.map(&:chomp)
end
def test_unicode_case
writelines(%w[строКА1 СТРОКА2 строка3 Строка4])
assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
end
def test_tiebreak
input = %w[
--foobar--------
-----foobar---
----foobar--
-------foobar-
]
writelines(input)
assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
by_length = %w[
----foobar--
-----foobar---
-------foobar-
--foobar--------
]
assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.lines(chomp: true)
assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.lines(chomp: true)
by_begin = %w[
--foobar--------
----foobar--
-----foobar---
-------foobar-
]
assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.lines(chomp: true)
assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.lines(chomp: true)
assert_equal %w[
-------foobar-
----foobar--
-----foobar---
--foobar--------
], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.lines(chomp: true)
assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_index_begin
writelines([
'xoxxxxxoxx',
'xoxxxxxox',
'xxoxxxoxx',
'xxxoxoxxx',
'xxxxoxox',
' xxoxoxxx'
])
assert_equal [
'xxxxoxox',
' xxoxoxxx',
'xxxoxoxxx',
'xxoxxxoxx',
'xoxxxxxox',
'xoxxxxxoxx'
], `#{FZF} -foo < #{tempname}`.lines(chomp: true)
assert_equal [
'xxxoxoxxx',
'xxxxoxox',
' xxoxoxxx',
'xxoxxxoxx',
'xoxxxxxoxx',
'xoxxxxxox'
], `#{FZF} -foo --tiebreak=index < #{tempname}`.lines(chomp: true)
# Note that --tiebreak=begin is now based on the first occurrence of the
# first character on the pattern
assert_equal [
' xxoxoxxx',
'xxxoxoxxx',
'xxxxoxox',
'xxoxxxoxx',
'xoxxxxxoxx',
'xoxxxxxox'
], `#{FZF} -foo --tiebreak=begin < #{tempname}`.lines(chomp: true)
assert_equal [
' xxoxoxxx',
'xxxoxoxxx',
'xxxxoxox',
'xxoxxxoxx',
'xoxxxxxox',
'xoxxxxxoxx'
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_begin_algo_v2
writelines(['baz foo bar',
'foo bar baz'])
assert_equal [
'foo bar baz',
'baz foo bar'
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_end
writelines(['xoxxxxxxxx',
'xxoxxxxxxx',
'xxxoxxxxxx',
'xxxxoxxxx',
'xxxxxoxxx',
' xxxxoxxx'])
assert_equal [
' xxxxoxxx',
'xxxxoxxxx',
'xxxxxoxxx',
'xoxxxxxxxx',
'xxoxxxxxxx',
'xxxoxxxxxx'
], `#{FZF} -fo < #{tempname}`.lines(chomp: true)
assert_equal [
'xxxxxoxxx',
' xxxxoxxx',
'xxxxoxxxx',
'xxxoxxxxxx',
'xxoxxxxxxx',
'xoxxxxxxxx'
], `#{FZF} -fo --tiebreak=end < #{tempname}`.lines(chomp: true)
assert_equal [
'xxxxxoxxx',
' xxxxoxxx',
'xxxxoxxxx',
'xxxoxxxxxx',
'xxoxxxxxxx',
'xoxxxxxxxx'
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
writelines(['/bar/baz', '/foo/bar/baz'])
assert_equal [
'/foo/bar/baz',
'/bar/baz'
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_length_with_nth
input = %w[
1:hell
123:hello
12345:he
1234567:h
]
writelines(input)
output = %w[
1:hell
12345:he
123:hello
1234567:h
]
assert_equal output, `#{FZF} -fh < #{tempname}`.lines(chomp: true)
# Since 0.16.8, --nth doesn't affect --tiebreak
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
end
def test_tiebreak_chunk
writelines(['1 foobarbaz ba',
'2 foobar baz',
'3 foo barbaz'])
assert_equal [
'3 foo barbaz',
'2 foobar baz',
'1 foobarbaz ba'
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
assert_equal [
'1 foobarbaz ba',
'2 foobar baz',
'3 foo barbaz'
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
assert_equal [
'3 foo barbaz'
], `#{FZF} -f'!foobar' --tiebreak=chunk < #{tempname}`.lines(chomp: true)
end
def test_boundary_match
# Underscore boundaries should be ranked lower
{
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
}.each do |scheme, expected|
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
assert_equal expected, result
end
end
end

File diff suppressed because it is too large Load Diff

1006
test/test_layout.rb Normal file

File diff suppressed because it is too large Load Diff

561
test/test_preview.rb Normal file
View File

@@ -0,0 +1,561 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Test cases for preview
class TestPreview < TestInteractive
def test_preview
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], ' {-} ' }
tmux.send_keys '555'
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[1], ' {555-555} ' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
tmux.send_keys :BSpace
tmux.until { |lines| assert lines[-2]&.start_with?(' 28/1000 ') }
tmux.send_keys 'foobar'
tmux.until { |lines| refute_includes lines[1], ' {55-55} ' }
tmux.send_keys 'C-u'
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {-1} ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {3-1 } ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {4-1 3} ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
end
def test_toggle_preview_without_default_preview_command
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '║ [1]'
end
# toggle-preview should do nothing
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[1], '║ [1]'
refute_includes lines[1], '║ [2]'
end
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '║ [3]'
end
# One-off preview action
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
# toggle-preview to hide it
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
# toggle-preview again does nothing
tmux.send_keys :Enter, :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '║ [4]'
end
end
def test_show_and_hide_preview
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
# Hidden by default
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '┃ [1]'
end
# Show
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
# Already shown
tmux.send_keys :a
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
# Hide
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '┃ [3]'
end
# Already hidden
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '┃ [4]'
end
# Show it again
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
end
def test_preview_hidden
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys '?'
tmux.until { |lines| assert_match(/ {1-1-1-[0-9]+}/, lines[-2]) }
tmux.send_keys '555'
tmux.until { |lines| assert_match(/ {555-555-1-[0-9]+}/, lines[-2]) }
tmux.send_keys '?'
tmux.until { |lines| assert_equal '> 555', lines[-1] }
end
def test_preview_size_0
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
assert_equal ' 100/100', lines[1]
assert_equal '> 1', lines[2]
end
wait do
assert_path_exists tempname
assert_equal %w[1], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down, :Down
tmux.until { |lines| assert_equal '> 3', lines[4] }
wait do
assert_path_exists tempname
assert_equal %w[1], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_equal '> 4', lines[5] }
wait do
assert_path_exists tempname
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
end
end
def test_preview_size_0_hidden
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_includes lines, '> 3' }
wait { refute_path_exists tempname }
tmux.send_keys :Space
wait do
assert_path_exists tempname
assert_equal %w[3], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Down
wait do
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_includes lines, '> 5' }
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines, '> 6' }
tmux.send_keys :Space
wait do
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
end
end
def test_preview_flags
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 //0/0} ' }
tmux.send_keys '123'
tmux.until { |lines| assert_includes lines[1], ' {////123//} ' }
tmux.send_keys 'C-u', '1'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 /1/0/0} ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1/1 /1/9/0} ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1 10/1 10 /1/9/0 9} ' }
tmux.send_keys '2'
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /12//0 9} ' }
tmux.send_keys '3'
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
end
def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' foo barbar foobarfoo0101 ' }
end
def test_preview_q_no_match
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
tmux.send_keys 'bar'
tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
tmux.send_keys 'C-u'
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
end
def test_preview_q_no_match_with_initial_query
tmux.send_keys %(: | #{FZF} --preview 'echo 1. /{q}/{q:1}/; echo 2. /{q:..}/{q:2}/{q:-1}/; echo 3. /{q:s-2}/{q:-2}/{q:x}/' --query 'foo bar'), :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| assert_includes lines[1], '1. /foo bar/foo/' }
tmux.until { |lines| assert_includes lines[2], '2. /foo bar/bar/bar/' }
tmux.until { |lines| assert_includes lines[3], '3. /foo /foo/{q:x}/' }
end
def test_preview_update_on_select
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.send_keys 'a'
tmux.until { |lines| assert(lines.any? { |line| line.include?(' 1 2 3 4 5 ') }) }
tmux.send_keys 'a'
tmux.until { |lines| lines.each { |line| refute_includes line, ' 1 2 3 4 5 ' } }
end
def test_preview_correct_tab_width_after_ansi_reset_code
writelines(["\x1b[31m+\x1b[m\t\x1b[32mgreen"])
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| assert_includes lines[1], ' + green ' }
end
def test_preview_bindings_with_default_preview
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
tmux.until { |lines| lines.match_count == 10 }
tmux.until { |lines| assert_includes lines[1], '[1]' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'c'
tmux.until { |lines| assert_includes lines[1], '[1]' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines[1], '[111]' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[2]' }
end
def test_preview_bindings_without_default_preview
tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
tmux.until { |lines| lines.match_count == 10 }
tmux.until { |lines| refute_includes lines[1], '1' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'c' # does nothing
tmux.until { |lines| assert_includes lines[1], '[11]' }
tmux.send_keys 'b'
tmux.until { |lines| assert_includes lines[1], '[111]' }
tmux.send_keys 9
tmux.until { |lines| lines.match_count == 1 }
tmux.until { |lines| refute_includes lines[1], '2' }
tmux.until { |lines| assert_includes lines[1], '[111]' }
end
def test_preview_scroll_begin_constant
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] }
end
def test_preview_scroll_begin_expr
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] }
end
def test_preview_scroll_begin_and_offset
['echo foo 123 321', 'echo foo :123: 321'].each do |input|
tmux.send_keys "#{input} | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] }
tmux.send_keys 'C-c'
end
end
def test_preview_clear_screen
tmux.send_keys %{seq 100 | #{FZF} --preview 'for i in $(seq 300); do (( i % 200 == 0 )) && printf "\\033[2J"; echo "[$i]"; sleep 0.001; done'}, :Enter
tmux.until { |lines| lines.match_count == 100 }
tmux.until { |lines| lines[1]&.include?('[200]') }
end
def test_preview_window_follow
file = Tempfile.new('fzf-follow')
file.sync = true
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
tmux.until { |lines| lines.match_count == 100 }
# Write to the temporary file, and check if the preview window is showing
# the last line of the file
tmux.until { |lines| assert_includes lines[1], 'start' }
3.times { file.puts _1 } # header lines
1000.times { file.puts _1 }
tmux.until { |lines| assert_includes lines[1], '/1004' }
tmux.until { |lines| assert_includes lines[-2], '999' }
# Scroll the preview window and fzf should stop following the file content
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[-2], '998' }
file.puts 'foo', 'bar'
tmux.until do |lines|
assert_includes lines[1], '/1006'
assert_includes lines[-2], '998'
end
# Scroll back to the bottom and fzf should start following the file again
%w[999 foo bar].each do |item|
wait do
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines[-2], item }
end
end
file.puts 'baz'
tmux.until do |lines|
assert_includes lines[1], '/1007'
assert_includes lines[-2], 'baz'
end
# Scroll upwards to stop following
tmux.send_keys :Up
wait { assert_includes lines[-2], 'bar' }
file.puts 'aaa'
tmux.until do |lines|
assert_includes lines[1], '/1008'
assert_includes lines[-2], 'bar'
end
# Manually enable following
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
file.puts 'bbb'
tmux.until do |lines|
assert_includes lines[1], '/1009'
assert_includes lines[-2], 'bbb'
end
# Disable following
tmux.send_keys :Space
file.puts 'ccc', 'ddd'
tmux.until do |lines|
assert_includes lines[1], '/1011'
assert_includes lines[-2], 'bbb'
end
rescue StandardError
file.close
file.unlink
end
def test_toggle_preview_wrap
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
2.times do
tmux.until { |lines| assert_includes lines[2], '2nd line' }
tmux.send_keys 'C-w'
tmux.until do |lines|
assert_includes lines[2], 'wrapped'
assert_includes lines[3], '2nd line'
end
tmux.send_keys 'C-w'
end
end
def test_close
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| assert_includes lines[1], 'foo' }
tmux.send_keys 'C-c'
tmux.until { |lines| refute_includes lines[1], 'foo' }
tmux.send_keys '10'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
end
def test_preview_header
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
tmux.until do |lines|
assert_includes lines[1], '4/1000'
assert_equal(%w[1 2 3 4 5], top5[lines])
end
tmux.send_keys '55'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(%w[1 2 3 55 56], top5[lines])
end
tmux.send_keys 'C-J'
tmux.until do |lines|
assert_equal(%w[1 2 3 58 59], top5[lines])
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 19, lines.match_count
assert_equal(%w[1 2 3 5 6], top5[lines])
end
tmux.send_keys 'C-K'
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
end
def test_change_preview_window
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --no-preview-border --bind '" \
'a:change-preview(echo __{}__),' \
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
'c:change-preview(),d:change-preview-window(hidden),' \
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
# change-preview action permanently changes the preview command set by --preview
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[0], '__1__' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[0], '__2__' }
# When multiple change-preview-window actions are bound to a single key,
# the last one wins and the updated options are immediately applied to the new preview
tmux.send_keys 'b'
tmux.until { |lines| assert_equal '==2==', lines[0] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal '==3==', lines[0] }
# change-preview with an empty preview command closes the preview window
tmux.send_keys 'c'
tmux.until { |lines| refute_includes lines[0], '==' }
# change-preview again to re-open the preview window
tmux.send_keys 'a'
tmux.until { |lines| assert_equal '__3__', lines[0] }
# Hide the preview window with hidden flag
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines[0], '__3__' }
# One-off preview
tmux.send_keys 'e'
tmux.until do |lines|
assert_equal '::', lines[0]
refute_includes lines[1], '3'
end
# Wrapped
tmux.send_keys 'f'
tmux.until do |lines|
assert_equal '::', lines[0]
assert_equal '↳ 3', lines[1]
end
end
def test_change_preview_window_should_not_reset_change_preview
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
tmux.until { |lines| assert_includes lines, 'hello' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '│ hello' }
end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
3.times do
tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[0].end_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[-1].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| assert_equal 'hello', lines[0] }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[0], 'hello' }
tmux.send_keys 'a'
end
end
def test_change_preview_window_rotate_hidden
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
"a:change-preview-window(nohidden||down,1|)'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_change_preview_window_rotate_hidden_down
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_toggle_alternative_preview_window
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.until { |lines| refute_includes lines, '/1/1/' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '/1/1/' }
end
def test_alternative_preview_window_opts
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 do |lines|
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
end
end
def test_preview_window_width_exception
tmux.send_keys "seq 10 | #{FZF} --scrollbar --preview-window border-left --border --preview 'seq 1000'", :Enter
tmux.until do |lines|
assert lines[1]&.end_with?(' 1/1000││')
end
end
def test_preview_window_hidden_on_focus
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' }
end
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

52
test/test_server.rb Normal file
View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Test cases for API server
class TestServer < TestInteractive
def test_listen
{ '--listen 6266' => -> { URI('http://localhost:6266') },
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal 10, state[:totalCount]
assert_equal 10, state[:matchCount]
assert_empty state[:query]
assert_equal({ index: 0, text: '1' }, state[:current])
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal 100, state[:totalCount]
assert_equal 0, state[:matchCount]
assert_equal 'yo', state[:query]
assert_nil state[:current]
teardown
setup
end
end
def test_listen_with_api_key
post_uri = URI('http://localhost:6266')
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
# Incorrect API Key
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
end
# Valid API Key
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '200', res.code
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
end
end
end

View File

@@ -0,0 +1,517 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Testing shell integration
module TestShell
attr_reader :tmux
def setup
@tmux = Tmux.new(shell)
tmux.prepare
end
def teardown
@tmux.kill
end
def set_var(name, val)
tmux.prepare
tmux.send_keys "export #{name}='#{val}'", :Enter
tmux.prepare
end
def unset_var(name)
tmux.prepare
tmux.send_keys "unset #{name}", :Enter
tmux.prepare
end
def test_ctrl_t
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
tmux.prepare
tmux.send_keys 'C-t'
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert lines.any_include?(' (3)') }
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('1 2 3') }
tmux.send_keys 'C-c'
end
def test_ctrl_t_unicode
writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
tmux.prepare
tmux.send_keys 'echo ', 'C-t'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys 'fzf-unicode'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '2'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
end
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
end
def test_alt_c_command
set_var('FZF_ALT_C_COMMAND', 'echo /tmp')
tmux.prepare
tmux.send_keys 'cd /', :Enter
tmux.prepare
tmux.send_keys :Escape, :c
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter
tmux.prepare
tmux.send_keys 'echo 2nd', :Enter
tmux.prepare
tmux.send_keys 'echo 3d', :Enter
tmux.prepare
3.times do
tmux.send_keys 'echo 3rd', :Enter
tmux.prepare
end
tmux.send_keys 'echo 4th', :Enter
tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'e3d'
# Duplicates removed: 3d (1) + 3rd (1) => 2 matches
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
tmux.send_keys 'C-r'
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '3rd', lines[-1] }
end
def test_ctrl_r_multiline
# NOTE: Current bash implementation shows an extra new line if there's
# only entry in the history
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar'
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end
def test_ctrl_r_abort
skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
%w[foo ' "].each do |query|
tmux.prepare
tmux.send_keys :Enter, query
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
tmux.send_keys 'C-g'
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
end
end
end
module CompletionTest
def test_file_completion
FileUtils.mkdir_p('/tmp/fzf-test')
FileUtils.mkdir_p('/tmp/fzf test')
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
['no~such~user', '/tmp/fzf test/foobar'].each do |f|
FileUtils.touch(File.expand_path(f))
end
tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys ' !d'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) do |lines|
assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
end
# ~USERNAME**<TAB>
user = `whoami`.chomp
tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{user}**", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys "/#{user}"
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
tmux.send_keys :Enter
tmux.until(true) do |lines|
assert_match %r{cat .*/#{user}}, lines[-1]
end
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat ~such**', :Tab
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'foobar$'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> /tmp/fzf test/foobar')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
# Should include hidden files
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until(true) do |lines|
assert_equal 100, lines.match_count
assert lines.any_include?('/tmp/fzf-test/.hidden-')
end
tmux.send_keys :Enter
ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf(File.expand_path(f))
end
end
def test_file_completion_root
tmux.send_keys 'ls /**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Enter
end
def test_dir_completion
(1..100).each do |idx|
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
end
FileUtils.touch('/tmp/fzf-test/d55/xxx')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55/'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.send_keys :xx
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
# Should not match regular files (bash-only)
if instance_of?(TestBash)
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
end
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
end
def test_process_completion
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys 'kill **', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
end
end
end
def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab
tmux.until { |lines| assert_equal 11, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert_equal 3, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
end
def test_unset_completion
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
# Using tmux
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
tmux.send_keys 'C-c'
# FZF_TMUX=1
new_shell
tmux.focus
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
end
def test_completion_in_command_sequence
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
end
end
def test_file_completion_unicode
FileUtils.mkdir_p('/tmp/fzf-test')
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '2'
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
end
def test_custom_completion_api
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys "#{command} b**", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
end
ensure
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
end
def test_ssh_completion
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
tmux.send_keys 'ssh jg@localhost**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 5
assert_equal 0, lines.select_count
end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until do |lines|
assert_equal 3, lines.select_count
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
tmux.send_keys 'localhost**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
end
end
class TestBash < TestBase
include TestShell
include CompletionTest
def shell
:bash
end
def new_shell
tmux.prepare
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
tmux.prepare
end
def test_dynamic_completion_loader
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
tmux.paste '_completion_loader() { complete -o default fake; }'
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'C-c'
tmux.prepare
tmux.send_keys 'fake /tmp/foo'
tmux.send_keys :Tab, 'C-u'
tmux.prepare
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
end
end
class TestZsh < TestBase
include TestShell
include CompletionTest
def shell
:zsh
end
def new_shell
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
tmux.prepare
end
def test_complete_quoted_command
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
['unset', '\unset', "'unset'"].each do |command|
tmux.prepare
tmux.send_keys "#{command} FZFFOOBR**", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
tmux.send_keys 'C-c'
end
end
end
class TestFish < TestBase
include TestShell
def shell
:fish
end
def new_shell
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines }
end
def set_var(name, val)
tmux.prepare
tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare
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

View File

@@ -13,7 +13,7 @@ Execute (fzf#run with dir option):
execute 'lcd' fnameescape(cwd)
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual ['fzf.vader'], result
AssertEqual 1, haslocaldir()
AssertEqual getcwd(), cwd
@@ -23,8 +23,8 @@ Execute (fzf#run with Funcref command):
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
AssertEqual ['fzf.vader'], result
AssertEqual ['fzf.vader'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
@@ -78,18 +78,18 @@ Execute (fzf#wrap):
let opts = fzf#wrap('foobar')
Log opts
AssertEqual '~40%', opts.down
AssertEqual 0.9, opts.window.width
Assert opts.options =~ '--expect='
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {}, 0)
Log opts
AssertEqual '~40%', opts.down
AssertEqual 0.9, opts.window.width
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'down')
Assert !has_key(opts, 'window')
let opts = fzf#wrap('foobar', {'down': '50%'})
Log opts
@@ -148,7 +148,7 @@ Execute (fzf#wrap):
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
let opts = fzf#wrap({})
Assert opts.options =~ '^--color=fg:'
Assert opts.options =~ '--color=fg:'
Execute (fzf#shellescape with sh):
AssertEqual '''''', fzf#shellescape('', 'sh')