3766 Commits
Author SHA1 Message Date
Junegunn Choi 26ff936df0 Check walker ancestor symlinks against the current root only
- A symlink in one root was skipped when its target was an ancestor of
  another root, hiding files reachable only through it
- Fix and tests by @mabunemeh

Close #4944
2026-10-25 22:33:38 +09:00
Junegunn Choi b412a45053 Avoid O(n^2) string concatenation in maskActionContents
- `masked +=` per token copied the whole prefix on every append
- ~1 MB POST to --listen blocked the server for ~30 s
- switch to strings.Builder

Reported by Zachariah Mithani (@zachariah-mithani, AploSec).
2026-10-22 08:24:40 +09:00
Robby Russell ac28b8c3ba docs: add commit goods badge and utm tags to merch links 2026-10-21 20:00:06 +09:00
Junegunn ChoiandNavpreetSSidhu 2adb741c33 Respect --no-sort in non-streaming filter mode
- --sync, --tac, --bench disable streaming filter, and the result was sorted
- Matcher.scan already checks pattern.sortable

Problem analyzed and patch submitted by @NavpreetSSidhu

Fix #4940

Co-authored-by: NavpreetSSidhu <116211004+NavpreetSSidhu@users.noreply.github.com>
2026-10-21 15:05:38 +09:00
Junegunn Choi a38ab117c5 Add CHANGELOG entry for #4941 2026-10-21 15:05:17 +09:00
Junegunn Choi 903a4da7ab Avoid control characters in Neovim terminal buffer name
- With 'title', run command from temp script if it has control chars
- Raw ESC in buffer name leaked through title and drew garbage

Fix #4941
2026-10-21 14:47:16 +09:00
Junegunn Choi d3d6e40962 change-nth: empty value restores default --nth
- Same rule as change-with-nth and transform-with-nth
    - Empty: default
    - '..': all fields
    - Non-empty invalid: ignored
- Previously, change-nth() and empty transform-nth output searched
  all fields, and transform-nth could not restore default
- Partly reverts af0014ab. Use change-nth(..) to unset default
2026-10-21 08:29:43 +09:00
Junegunn Choi c943963281 change-(with-)nth: apply value after current one
- Find current value in bound list and apply next valid,
  non-current one
- Keep rotating bound list as a position hint, so repeated
  values in the list still reach every position
- Treat empty --nth and '..' as the same value
- Ignore invalid nth values, also in transform-nth, as with-nth
  does. Previously, they restored default --nth
- Previously, first value of rotated list was applied blindly
    - Key press spent on value already applied, e.g. first value
      equal to initial --nth
    - Stuck on change-nth printed by transform (new action on
      each run), as it always applied first value
- Add nextValue helper for reuse by change-layout
2026-10-20 16:19:06 +09:00
Junegunn Choi a0f6bac731 transform-(with-)nth: use whole output, no cycling
- Rotation of '|'-separated values applied to transform and
  bg-transform variants, where a.a is the command template
- Output '1|2' turned the binding into '2|1', run as a shell
  command on the next key press
- Output was also cut at '|', breaking --with-nth templates
  like '{2}|{1}'
- Split and rotate only for change-nth and change-with-nth
2026-10-20 14:09:55 +09:00
Simon Désaulniers 55c122f1a0 fix(nushell): adapt the completer to the 0.116 contract (#4934)
* fix(nushell): adapt the completer to the 0.116 contract

Nushell 0.116 runs an external completer on a background worker
without the terminal unless the registered closure reaches a
command carrying the @interactive attribute. Interactivity is
seen through the closure to the command it calls, so wrap the
completion logic in an @interactive entry point and register a
closure that ends with the call to it.

Read the named `place` input instead of the deprecated
positional `spans`; $place.command carries the same token list.

An @interactive completer cannot decline with null (null reads
as a dismissed picker), so request the fallback explicitly when
the `**` trigger is absent, keeping plain file completion
working. The previous external completer is still called when
the trigger is absent.

The script now requires Nushell 0.116+, since @interactive is a
parse error on older versions.

Fixes #4932

* fix(nushell): retry the previous completer with the legacy spans input

The fallback to the previously configured external completer always
handed it the new `place` input. A completer that still declares the
legacy `spans` parameter errors on a record, so the fallback silently
produced no completions for pre-0.116 completers (e.g. older Carapace
installs).

Try `place` first, then retry with the token list, which is what the
old positional bridge handed such completers.

* docs(nushell): note the 0.116 requirement and load order

Requires Nushell 0.116.1+, where a completion can take the terminal
through the @interactive attribute, and the wrapper captures the
external completer present at load time.
2026-10-17 11:10:52 +09:00
Daehyun KimandJunegunn Choi c8c2d0828a Fix Windows shell detection with explicit --with-shell
Close #4936

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2026-10-15 10:31:52 +09:00
Junegunn Choi c0a193e031 0.74.5 v0.74.5 2026-10-11 11:29:16 +09:00
Junegunn Choi cbc11e4d95 completion(zsh): use private mktemp dir for completion FIFO
FIFO path was predictable (${TMPDIR:-/tmp}/fzf-complete-fifo-$$) and
_fzf_feed_fifo ignored rm/mkfifo failures, so on a shared /tmp another
user could pre-create the path (symlink or FIFO) and have the victim's
redirect follow it.

Allocate the FIFO inside a per-invocation mktemp -d directory, check
mkfifo status, and clean up the directory instead of the bare path.

Reported-by: sb24CK (https://github.com/sb24CK)
2026-10-11 11:27:06 +09:00
Junegunn Choi f126ff2954 fzf-tmux: validate attached layout size value
Attached option form (e.g. -l50) captured size via ${arg:2} with no
validation, unlike the separate-argument form. For -l/-u the value
reached $((...)) arithmetic, where a[$(cmd)] runs cmd during option
parsing, before any tmux check. Same value was also expanded unquoted
into `tmux popup ... $opt`, allowing tmux flag injection.

Apply the separate-form regex to the attached form too. Rejects the
array-subscript payload and space-bearing geometry values that enabled
flag injection.

Reported-by: sb24CK (https://github.com/sb24CK)
2026-10-11 11:27:05 +09:00
Junegunn ChoiandJere Viikari d8aeec3058 Preserve tmux window zoom when opening a floating pane
- Use new-pane -A on tmux 3.8+ to keep floating pane above zoomed pane
- Detect -A from new-pane usage; keep unzoom fallback for 3.7

Fix #4927
Fix junegunn/fzf.vim#1625

Co-authored-by: Jere Viikari <jere.viikari@gmail.com>
2026-10-11 11:22:03 +09:00
Junegunn Choi 981f1539b0 Run itest in a separate tmux server
- No user config, so options like extended-keys do not affect tests
- Default 80x24 window, same as CI
2026-10-11 11:05:23 +09:00
Junegunn Choi 25adb67453 Trigger focus event after reload (#4931)
- Indexes restart after reload, so same-index item was not seen as new
- With --id-nth, compare by key. No event if focused item keeps its key.
- Skip focus check while track is blocked
- Document that binding reload to focus can loop

Close #4930
2026-10-10 20:59:13 +09:00
Junegunn Choi 33a3456921 Fix panic on a read error during the second-chance read
The read error left a nil buffer, and the deferred slice in GetChar
panicked instead of returning Fatal.
2026-10-08 21:42:32 +09:00
Junegunn Choi b563e208d6 Check escape sequence bytes before counting them (#4929)
The parser decided how many bytes a key used before checking them, so a
key that arrived in the same read was lost or misread.

- Recognize F1-F8 and modified Delete, PageUp and PageDown when more input
  follows them, as with a paste or tmux send-keys. They required the
  sequence to be the whole buffer.
- After \e[ and a digit, do not consume a byte that cannot continue the
  sequence. ALT-[, 2 and Enter lost the Enter.
- Report Home and End for \e[7, \e[4 and \e[8 only before ~, $, ^ or @.
  ALT-[, 4 and x fired End and lost the x.
- Look for the end of a mouse report within its frame. A later M swallowed
  the keys before it, and repeated \e[< took quadratic time.
2026-10-08 21:34:46 +09:00
Junegunn Choi c009c106b9 Drop unrecognized escape sequences instead of typing them (#4926)
fzf consumed only the part of a sequence it recognized, and the rest was
typed into the query. CTRL-A sent as \e[97;5u became "97;5u", and CTRL-F
sent as \e[70;5u fired Home before typing ";5u". A BEL-terminated OSC
reply also aborted fzf, because the BEL that followed the typed payload
was read as CTRL-G.

Frame CSI and SS3 by their parameter and final byte ranges, OSC, DCS and
APC by their terminator. Drop the whole frame when the parser does not
recognize it, or recognizes only a prefix of it, and never consume past
it: the parser could count a key typed after it. rxvt ends keys with $,
as in \e[7$ and \e[23$, so $ after plain digits is a final byte. Elsewhere
it stays intermediate, as in DECRPM replies.

Take the second-chance read only when the sequence at the start of the
buffer is the only one and is unfinished. Otherwise it blocked until the
next key, holding back what followed, CTRL-G included. That read has no
time limit, so what it returns may be the next key, typed any time later.
It is never dropped with the frame.

Telling a terminal's sequence from an ALT key is the hard part, so these
keep what the parser made of them:

- An unterminated sequence, since that is how ALT-[, ALT-O, ALT-], ALT-P
  and ALT-_ arrive
- A CSI or SS3 of four bytes or fewer. It could be an ALT key and typed
  text, and rxvt sends keys of that size fzf does not know, such as \e[3^
- A string sequence whose payload does not start like a reply: digits and
  ';' for OSC, '>|', '!|', '[01]$r' or '[01]+r' for DCS, 'G' and a key
  for APC
- SOS and PM, which nothing sends

Within a string, an ESC ends it and introduces a sequence of its own, and
only OSC ends with BEL. The read loop does not wait for a string
terminator: that held ALT-], ALT-P and ALT-_ for ESCDELAY and let typed
bytes pile up until they looked like a sequence.
2026-10-08 20:17:03 +09:00
Junegunn Choi b1be3a8be1 Update issue template 2026-09-14 12:16:10 +09:00
Junegunn Choi 961793cf39 Fix --gap-line cutting a grapheme cluster
RepeatToFill filled the remaining width rune by rune, so a cluster
could be split at the right edge. Iterate grapheme clusters instead.

Fix #4920
2026-09-13 19:31:36 +09:00
Junegunn Choi a140afeb4d 0.74.4 v0.74.4 2026-09-12 09:21:00 +09:00
Junegunn Choi 758b1ac73d Document adaptive height waiting for the input (#4915)
fzf cannot pick the height until the input ends or exceeds it, so a
stream that stays below the height delays the first render.
2026-09-11 22:16:00 +09:00
Junegunn Choi 013532d5cd Update CHANGELOG 2026-09-11 21:54:58 +09:00
Junegunn Choi fe04475d24 [nushell] Quote paths inserted by CTRL-T (#4919)
- Selections were appended to the command line as-is, so a path with
  spaces became several arguments and syntax in a file name was left
  unquoted for the parser
- Read the selection with --print0 and serialize each path with `to nuon`,
  so each one stays a single string literal
- Skip the insertion entirely when nothing was selected
2026-09-11 21:46:03 +09:00
Junegunn Choi b224480a98 Do not erase the line the prompt was on (#4918)
- When the cursor is not at column 0, Init() makes space so fzf draws on
  the next line and the prompt line is left intact
- Close() undid that: it moved up onto the prompt line before ESC[J, so
  the erase wiped a line fzf never wrote on
- Shells repaint afterwards, so it showed as a flicker of the last prompt
  line. Reported on fish, also seen on bash and nushell. zsh erases the
  same line but repaints fast enough to hide it
- Erase our own area first, then step back and restore the column, so
  nothing on that line is touched, RPROMPT included

Fix #4913
2026-09-10 23:19:38 +09:00
Junegunn Choi 1578b7c3c3 Fix spurious exit while waiting for a key with --listen
- getch() charged EINTR against a 100 iteration limit meant for read
  attempts, and the count only reset once a byte was read
- So signals arriving during an idle prompt drained it, and running out
  returned getCharError, which became Event{Fatal} and exit status 2
- Only the cancellable path is affected, hence only --listen
- select() is not restarted by SA_RESTART, unlike the blocking read on
  the non-cancellable path, so it is the only one that sees EINTR
- waitReadable() already retries EINTR without a limit

Fix #4917
2026-09-10 17:56:15 +09:00
Simon Désaulniers 63e82a9e3d nushell: keybinds support for helix mode (#4914)
Thanks!
2026-09-09 09:20:02 +09:00
bitraid a1e983baf1 fish: Optimize description alignment of completions (#4912)
Close #4910
2026-09-09 00:14:48 +09:00
bitraid ad151d8415 fish: Improve custom CTRL-T command execution (#4909)
The custom CTRL-T command is now executed by the script in the current
shell session, instead of being called by fzf through a subshell. This
ensures that the value of $dir can't be altered by user initialization
scripts, and is also a little faster.

Fix #4498
2026-09-07 08:36:30 +09:00
Junegunn Choi 7e6339979f Fix stale progress in --listen status payload (#4907)
- progress was set to 100 by every UpdateList, partial results included,
  and never reset when a search started
- So a GET could report a new query, the previous result set, and
  progress 100, and a client had no way to tell it was stale
- Now cleared where the search is requested and set to 100 only on the
  final result. Query and the reset happen in the same locked section
  that dumpStatus locks, so query == X with progress 100 means the
  matches belong to X

Fix #4903
2026-09-06 19:36:40 +09:00
Junegunn Choi 1372d04f79 Fix adaptive height with an inline header or footer border
- extraLines() used borderLines(), which reports zero for BorderInline
- Divider drawn for an inline section still takes a line, and
  resizeWindows() reserves it via addInline()
- So adaptive height came up one line short per inline section and the
  list scrolled
- Affects --header-lines-border, --header-border and --footer-border
  alike. All three set came up three lines short
- Without a list border the shape normalizes to BorderTop or BorderBottom,
  which report a line, hence --no-list-border looked like a fix

    seq 10 | fzf --height=~100% --list-border \
                 --header-lines=1 --header-lines-border=inline

Fix #4904
2026-09-06 11:27:17 +09:00
Junegunn Choi a52ac843d7 [vim] Open fzf in a tmux or Zellij floating pane by default (#4905)
The window fzf was started from stays visible, and can be used while fzf
runs, which a popup inside Vim cannot offer.

Only where the pane is not modal, so tmux 3.7 or above, or Zellij 0.44 or
above with fzf 0.71.0 or above. A tmux popup from 3.3 to 3.6 cannot be left,
and below 3.3 fzf goes through the fzf-tmux script, whose options are
spelled differently. Both keep the window inside Vim. An explicit popup
layout is unaffected and still works from 3.3.

fzf checks tmux before Zellij, so tmux wins when both are set.

g:fzf_layout still wins, so anyone who set it sees no change.
2026-09-06 00:36:08 +09:00
Junegunn Choi 52f4319a72 Update CHANGELOG 2026-08-31 19:09:35 +09:00
Junegunn Choi b395cfbd91 Fix --tiebreak=pathname with non-ASCII text
Fix #4902
2026-08-31 18:57:12 +09:00
Cyrus f7ae439ff5 Pin $SHELL to cmd in TestQuoteEntry and TestReplacePlaceholder 2026-08-26 09:06:03 +09:00
Junegunn Choi d46bfd005a Keep a minimum wait for a sequence in flight
ESCDELAY=0 makes the retry count zero, so the wait added in d0377ed had
no effect and the reply still leaked. Past the introducer these bytes
cannot be the ESC key, so waiting for them adds no Escape latency.

- Wait at least defaultEscDelay, honour a larger ESCDELAY
- Lone ESC still honours ESCDELAY=0, measured 3ms to exit either way

Fix #4899
2026-08-24 09:54:09 +09:00
Junegunn Choi d0377ed4dc Wait for rest of escape sequence before parsing (#4901)
Read loop dropped its escDelay retry budget after every successful byte,
so a sequence split across reads reached the parser as a fragment, parsed
as ALT-[ with the remainder left behind as query text.

- fzf queries DECRQM at startup since dab626b, so a terminal answering
  late leaked "?2004;2$y" into the query
- Same split leaked modified keys and mouse sequences: CTRL-UP left "5A",
  SGR mouse left "0;1;1M"
- Bound unchanged, a stall longer than escDelay still falls back to ALT

Fix #4899
2026-08-24 00:40:06 +09:00
Junegunn Choi 5cb7bab702 Run fzf asynchronously in the Vim plugin (#4897)
* Run fzf asynchronously in the Vim plugin

Popup mode held the fzf process with system(), which froze Vim until fzf
exited. fzf in a popup draws in a pane of its own, so that process only
waits for it and does not need a window. Hold it with a job instead and
Vim keeps processing its event loop, which is what a live preview needs.
Nothing is displayed for the job. Falls back to the blocking path when
the job cannot start, so the sink still runs and temp files are removed.

- job_start() sets $TERM=dumb and the popup inherits the environment, so
  fzf dropped to its 16-color scheme. Restore it via 'env', or in the
  command itself before 8.0.902, when 'env' was added
- Fullscreen now uses a terminal buffer in a new tab on Vim too. use_term
  lacked parentheses, so && bound tighter than || and the layout test was
  dead on Neovim, which already behaved this way
- fzf#run returns an empty list in these modes. Callers use sink,
  sinklist or exit, and the vader specs now wait for completion
- Append --no-tmux only when the spec asks for a Vim window, so --popup
  in $FZF_DEFAULT_OPTS survives a spec with no layout option

Accept popup as a synonym of the tmux layout key, matching --popup being
the name of --tmux. popup wins when both are given.

s:tmux_enabled():

- Accept $ZELLIJ, which --popup covers as well
- Parse tmux -V with matchstr and compare with s:compare_versions. The
  old string comparison against 'tmux 1.7' misreads 10.0
- Drop the fzf-tmux requirement on tmux 3.3 or above, where --tmux needs
  no script. Removing the script silently disabled popups entirely
- Resolve the script where it is used, and anchor the legacy test to ^-
  so a --tmux value containing a dash, as in 90%,60%,border-native, is
  not mistaken for a legacy flag

* Turn g:loaded_fzf into a version marker

Callers cannot detect plugin-side behavior. fzf#exec() reports the version
of the binary, not of this plugin, and everything else here is script-local.
fzf.vim needs to know whether fzf#run is asynchronous in popup mode before
offering a key binding that depends on it.

The value was only ever read through the exists() guard, so raising it from
1 breaks nothing.
2026-08-23 23:23:51 +09:00
Junegunn Choi 956562daaa Fix CI: Pin nushell version to 0.114 (#4898)
https://github.com/junegunn/fzf/discussions/4896
https://github.com/nushell/nushell/issues/18771
2026-08-21 13:28:14 +09:00
Junegunn Choi 15f64c492a 0.74.3 v0.74.3 2026-08-17 12:05:05 +09:00
Cyrus bd4efa277b Use the platform quote placeholder for {fzf:prompt}
Every other entry in this table goes through {{.O}}; this one had the
POSIX single quotes written in, so it could only pass off Windows.
2026-08-14 19:16:31 +09:00
Cyrus 63f6cfec5b Split the doubled and plain backslash cases in the quoting test
escapeArg only doubles a backslash that precedes a quote or ends the
argument, the same rule as syscall.EscapeArg, and @ is not a cmd
metacharacter so it is not caret-escaped. The Windows expectations here
still asked for both. Checked by round-tripping each entry through
cmd.exe into a program's argv: all ten come back byte for byte.
2026-08-14 19:16:31 +09:00
Cyrus d4b6ba781c Match the current temp file name in the powershell {f} test
The template writes to os.CreateTemp("", "fzf-temp-*"); the fzf-preview-
prefix went away with the --tmux work. The digit count was also pinned at
nine, but CreateTemp's suffix has no fixed width.
2026-08-14 19:16:31 +09:00
Cyrus 85a2a33612 Close the temp ttyout file in the SGR deduplication test
The file is opened under t.TempDir() and never closed, so the cleanup
that TempDir registers cannot remove it on Windows, where an open file
cannot be unlinked. The test itself passes; it is the cleanup that fails
the run. history_test.go and options_test.go already close theirs.
2026-08-14 19:16:30 +09:00
Junegunn Choi ca4c1b80e3 Erase 'p' on Terminal.app
Terminal.app fails to properly handle DECRQM and leaves 'p' on the
screen.

Fix #4893
2026-08-11 23:42:09 +09:00
Junegunn Choi dab626bd9e Restore bracketed paste mode instead of forcing it off
Ask the terminal whether the mode is already on (DECRQM) and put it back
that way on exit. Forcing it off broke pasting in shells that run fzf
from a line editor widget, which enable the mode only when the editor
starts. Terminals that do not answer fall back to disabling it.

- Startup queries go out in one write, cursor position last. Every
  terminal answers DSR, so its reply bounds the wait: a paste reply
  still missing by then means the terminal does not know the query.
- Bound the first read with select(2). Terminals that never answer
  escape sequences, such as FreeBSD virtual terminals, blocked startup
  until a key was pressed, and that keystroke was then discarded.

Fix #4887
Fix #2860
Fix #976
2026-08-10 22:36:25 +09:00
Junegunn Choi 2885df8395 Clean up comments and tests 2026-08-10 22:36:25 +09:00
Junegunn Choi 715d26fa39 Parenthesize an assert_equal argument in the zsh chpwd test
Lint/AmbiguousBlockAssociation. rubocop -a rewrote this on every
'make lint' run, leaving the tree dirty.
2026-08-08 19:48:04 +09:00