Compare commits

..
7 Commits
Author SHA1 Message Date
Junegunn Choi 99bfd5aa0f Add change-info-command and transform variants
- change-info-command, transform-info-command, bg-transform-info-command
- change-info-command disallowed from remote --listen like change-preview

Close #4951
2026-11-02 22:14:08 +09:00
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
12 changed files with 340 additions and 191 deletions

No files matched your search

+11 -1
View File
@@ -1,8 +1,14 @@
CHANGELOG
=========
0.74.6
0.75.0
------
- Added `{change,transform,bg-transform}-info-command` actions (#4951) (@casperdcl)
```sh
seq 100 | fzf --info-command 'echo "mode: A | $FZF_INFO"' \
--bind 'ctrl-a:change-info-command:echo "mode: A | $FZF_INFO"' \
--bind 'ctrl-b:change-info-command:echo "mode: B | $FZF_INFO"'
```
- Fixed `--with-shell` on Windows not detecting cmd, PowerShell, and pwsh, which made fzf quote commands and entries for a POSIX shell (#4936) (@vimkim)
- e.g. With `--with-shell "cmd.exe /d /c"`, `--preview '"C:\Program Files\nu\bin\nu.exe" --version'` failed
- `transform-nth` and `transform-with-nth` (and their `bg-` variants) no longer split the output at `|`
@@ -26,6 +32,10 @@ CHANGELOG
```
- nushell
- The completion integration now requires nushell 0.116.1 or above, where an interactive completer can take the terminal through the `@interactive` attribute (#4934) (@sim590)
- Fixed `--no-sort` being ignored in filter mode with `--sync`, `--tac`, or `--bench` (#4940) (@NavpreetSSidhu)
- Vim plugin: Fixed garbage text on screen in Neovim with `set title` when the fzf command contains ANSI escape sequences (#4941)
- Fixed `--walker=follow` skipping a symlink in one `--walker-root` when its target is an ancestor of another root (#4944) (@mabunemeh)
- Fixed O(n^2) action parsing in `--listen`. A single ~1 MB request could block the server for ~30 s (@zachariah-mithani, AploSec)
0.74.5
------
+3 -3
View File
@@ -12,13 +12,13 @@
<kbd align="center">
<br/>
<a href="https://commitgoods.com/collections/fzf"><img src="https://junegunn.github.io/fzf/images/fzf-mugs.jpg" width="80%" alt="fzf merch"></a>
<a href="https://commitgoods.com/collections/fzf?utm_source=github&amp;utm_medium=referral"><img src="https://junegunn.github.io/fzf/images/fzf-mugs.jpg" width="80%" alt="fzf merch"></a>
<br/>
<br/>
Show your love for fzf -- T-shirts, mugs, and stickers now available!
<br/>
<br/>
<a href="https://commitgoods.com/collections/fzf">commitgoods.com/collections/fzf</a>
<a href="https://commitgoods.com/collections/fzf?utm_source=github&amp;utm_medium=referral"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy-dark.svg"><img src="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy.svg" height="28" alt="Official merch on commitgoods.com"></picture></a>
<br/>
<br/>
</kbd>
@@ -1150,7 +1150,7 @@ Copyright (c) 2013-2026 Junegunn Choi
Goods
-----
Grab fzf T-shirts, mugs, and stickers here: https://commitgoods.com/collections/fzf
Grab fzf T-shirts, mugs, and stickers here: [commitgoods.com/collections/fzf](https://commitgoods.com/collections/fzf?utm_source=github&utm_medium=referral)
Sponsors :heart:
----------------
+2
View File
@@ -2056,6 +2056,7 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
\fBchange\-header\-lines(N)\fR (change the number of \fB\-\-header\-lines\fR)
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
\fBchange\-info\-command(...)\fR (change \fB\-\-info\-command\fR option)
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
\fBchange\-multi\fR (enable multi-select mode with no limit)
@@ -2164,6 +2165,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-header(...)\fR (transform header using an external command)
\fBtransform\-header\-lines(...)\fR (transform the number of \fB\-\-header\-lines\fR using an external command)
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
\fBtransform\-info\-command(...)\fR (transform \fB\-\-info\-command\fR using an external command)
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
\fBtransform\-nth(...)\fR (transform nth using an external command; see CHANGE NTH AND WITH\-NTH)
+6
View File
@@ -1104,6 +1104,12 @@ function! s:execute_term(dict, command, temps) abort
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile
elseif has('nvim') && &title && a:command =~ '[[:cntrl:]]'
" Neovim puts the command in the buffer name, which can end up raw in
" the terminal title (#4941)
let fzf.temps.shellscript = s:fzf_tempname()
call s:writefile([a:command], fzf.temps.shellscript)
let command = 'sh '.fzf#shellescape(fzf.temps.shellscript)
else
let command = a:command
endif
+161 -157
View File
@@ -34,168 +34,172 @@ func _() {
_ = x[actChangeFooter-23]
_ = x[actChangeHeaderLabel-24]
_ = x[actChangeFooterLabel-25]
_ = x[actChangeInputLabel-26]
_ = x[actChangeListLabel-27]
_ = x[actChangeMulti-28]
_ = x[actChangeNth-29]
_ = x[actChangeWithNth-30]
_ = x[actChangePointer-31]
_ = x[actChangePreview-32]
_ = x[actChangePreviewLabel-33]
_ = x[actChangePreviewWindow-34]
_ = x[actChangePrompt-35]
_ = x[actChangeQuery-36]
_ = x[actClearScreen-37]
_ = x[actClearQuery-38]
_ = x[actClearSelection-39]
_ = x[actClose-40]
_ = x[actDeleteChar-41]
_ = x[actDeleteCharEof-42]
_ = x[actEndOfLine-43]
_ = x[actFatal-44]
_ = x[actForwardChar-45]
_ = x[actForwardWord-46]
_ = x[actForwardSubWord-47]
_ = x[actKillLine-48]
_ = x[actKillWord-49]
_ = x[actKillSubWord-50]
_ = x[actUnixLineDiscard-51]
_ = x[actUnixWordRubout-52]
_ = x[actYank-53]
_ = x[actBackwardKillWord-54]
_ = x[actBackwardKillSubWord-55]
_ = x[actSelectAll-56]
_ = x[actDeselectAll-57]
_ = x[actToggle-58]
_ = x[actToggleSearch-59]
_ = x[actToggleAll-60]
_ = x[actToggleDown-61]
_ = x[actToggleUp-62]
_ = x[actToggleIn-63]
_ = x[actToggleOut-64]
_ = x[actToggleTrack-65]
_ = x[actToggleTrackCurrent-66]
_ = x[actToggleHeader-67]
_ = x[actToggleWrap-68]
_ = x[actToggleWrapWord-69]
_ = x[actToggleMultiLine-70]
_ = x[actToggleHscroll-71]
_ = x[actToggleRaw-72]
_ = x[actEnableRaw-73]
_ = x[actDisableRaw-74]
_ = x[actTrackCurrent-75]
_ = x[actToggleInput-76]
_ = x[actHideInput-77]
_ = x[actShowInput-78]
_ = x[actUntrackCurrent-79]
_ = x[actDown-80]
_ = x[actDownMatch-81]
_ = x[actUp-82]
_ = x[actUpMatch-83]
_ = x[actPageUp-84]
_ = x[actPageDown-85]
_ = x[actPosition-86]
_ = x[actHalfPageUp-87]
_ = x[actHalfPageDown-88]
_ = x[actOffsetUp-89]
_ = x[actOffsetDown-90]
_ = x[actOffsetMiddle-91]
_ = x[actJump-92]
_ = x[actJumpAccept-93]
_ = x[actPrintQuery-94]
_ = x[actRefreshPreview-95]
_ = x[actReplaceQuery-96]
_ = x[actToggleSort-97]
_ = x[actShowPreview-98]
_ = x[actHidePreview-99]
_ = x[actTogglePreview-100]
_ = x[actTogglePreviewWrap-101]
_ = x[actTogglePreviewWrapWord-102]
_ = x[actTransform-103]
_ = x[actTransformBorderLabel-104]
_ = x[actTransformGhost-105]
_ = x[actTransformHeader-106]
_ = x[actTransformHeaderLines-107]
_ = x[actTransformFooter-108]
_ = x[actTransformHeaderLabel-109]
_ = x[actTransformFooterLabel-110]
_ = x[actTransformInputLabel-111]
_ = x[actTransformListLabel-112]
_ = x[actTransformNth-113]
_ = x[actTransformWithNth-114]
_ = x[actTransformPointer-115]
_ = x[actTransformPreviewLabel-116]
_ = x[actTransformPrompt-117]
_ = x[actTransformQuery-118]
_ = x[actTransformSearch-119]
_ = x[actTrigger-120]
_ = x[actBgTransform-121]
_ = x[actBgTransformBorderLabel-122]
_ = x[actBgTransformGhost-123]
_ = x[actBgTransformHeader-124]
_ = x[actBgTransformHeaderLines-125]
_ = x[actBgTransformFooter-126]
_ = x[actBgTransformHeaderLabel-127]
_ = x[actBgTransformFooterLabel-128]
_ = x[actBgTransformInputLabel-129]
_ = x[actBgTransformListLabel-130]
_ = x[actBgTransformNth-131]
_ = x[actBgTransformWithNth-132]
_ = x[actBgTransformPointer-133]
_ = x[actBgTransformPreviewLabel-134]
_ = x[actBgTransformPrompt-135]
_ = x[actBgTransformQuery-136]
_ = x[actBgTransformSearch-137]
_ = x[actBgCancel-138]
_ = x[actSearch-139]
_ = x[actPreview-140]
_ = x[actPreviewTop-141]
_ = x[actPreviewBottom-142]
_ = x[actPreviewUp-143]
_ = x[actPreviewDown-144]
_ = x[actPreviewPageUp-145]
_ = x[actPreviewPageDown-146]
_ = x[actPreviewHalfPageUp-147]
_ = x[actPreviewHalfPageDown-148]
_ = x[actPrevHistory-149]
_ = x[actPrevSelected-150]
_ = x[actPrint-151]
_ = x[actPut-152]
_ = x[actNextHistory-153]
_ = x[actNextSelected-154]
_ = x[actExecute-155]
_ = x[actExecuteSilent-156]
_ = x[actExecuteMulti-157]
_ = x[actSigStop-158]
_ = x[actBest-159]
_ = x[actFirst-160]
_ = x[actLast-161]
_ = x[actReload-162]
_ = x[actReloadSync-163]
_ = x[actDisableSearch-164]
_ = x[actEnableSearch-165]
_ = x[actSelect-166]
_ = x[actDeselect-167]
_ = x[actUnbind-168]
_ = x[actRebind-169]
_ = x[actToggleBind-170]
_ = x[actBecome-171]
_ = x[actShowHeader-172]
_ = x[actHideHeader-173]
_ = x[actBell-174]
_ = x[actExclude-175]
_ = x[actExcludeMulti-176]
_ = x[actAsync-177]
_ = x[actWait-178]
_ = x[actChangeInfoCommand-26]
_ = x[actChangeInputLabel-27]
_ = x[actChangeListLabel-28]
_ = x[actChangeMulti-29]
_ = x[actChangeNth-30]
_ = x[actChangeWithNth-31]
_ = x[actChangePointer-32]
_ = x[actChangePreview-33]
_ = x[actChangePreviewLabel-34]
_ = x[actChangePreviewWindow-35]
_ = x[actChangePrompt-36]
_ = x[actChangeQuery-37]
_ = x[actClearScreen-38]
_ = x[actClearQuery-39]
_ = x[actClearSelection-40]
_ = x[actClose-41]
_ = x[actDeleteChar-42]
_ = x[actDeleteCharEof-43]
_ = x[actEndOfLine-44]
_ = x[actFatal-45]
_ = x[actForwardChar-46]
_ = x[actForwardWord-47]
_ = x[actForwardSubWord-48]
_ = x[actKillLine-49]
_ = x[actKillWord-50]
_ = x[actKillSubWord-51]
_ = x[actUnixLineDiscard-52]
_ = x[actUnixWordRubout-53]
_ = x[actYank-54]
_ = x[actBackwardKillWord-55]
_ = x[actBackwardKillSubWord-56]
_ = x[actSelectAll-57]
_ = x[actDeselectAll-58]
_ = x[actToggle-59]
_ = x[actToggleSearch-60]
_ = x[actToggleAll-61]
_ = x[actToggleDown-62]
_ = x[actToggleUp-63]
_ = x[actToggleIn-64]
_ = x[actToggleOut-65]
_ = x[actToggleTrack-66]
_ = x[actToggleTrackCurrent-67]
_ = x[actToggleHeader-68]
_ = x[actToggleWrap-69]
_ = x[actToggleWrapWord-70]
_ = x[actToggleMultiLine-71]
_ = x[actToggleHscroll-72]
_ = x[actToggleRaw-73]
_ = x[actEnableRaw-74]
_ = x[actDisableRaw-75]
_ = x[actTrackCurrent-76]
_ = x[actToggleInput-77]
_ = x[actHideInput-78]
_ = x[actShowInput-79]
_ = x[actUntrackCurrent-80]
_ = x[actDown-81]
_ = x[actDownMatch-82]
_ = x[actUp-83]
_ = x[actUpMatch-84]
_ = x[actPageUp-85]
_ = x[actPageDown-86]
_ = x[actPosition-87]
_ = x[actHalfPageUp-88]
_ = x[actHalfPageDown-89]
_ = x[actOffsetUp-90]
_ = x[actOffsetDown-91]
_ = x[actOffsetMiddle-92]
_ = x[actJump-93]
_ = x[actJumpAccept-94]
_ = x[actPrintQuery-95]
_ = x[actRefreshPreview-96]
_ = x[actReplaceQuery-97]
_ = x[actToggleSort-98]
_ = x[actShowPreview-99]
_ = x[actHidePreview-100]
_ = x[actTogglePreview-101]
_ = x[actTogglePreviewWrap-102]
_ = x[actTogglePreviewWrapWord-103]
_ = x[actTransform-104]
_ = x[actTransformBorderLabel-105]
_ = x[actTransformGhost-106]
_ = x[actTransformHeader-107]
_ = x[actTransformHeaderLines-108]
_ = x[actTransformFooter-109]
_ = x[actTransformHeaderLabel-110]
_ = x[actTransformFooterLabel-111]
_ = x[actTransformInfoCommand-112]
_ = x[actTransformInputLabel-113]
_ = x[actTransformListLabel-114]
_ = x[actTransformNth-115]
_ = x[actTransformWithNth-116]
_ = x[actTransformPointer-117]
_ = x[actTransformPreviewLabel-118]
_ = x[actTransformPrompt-119]
_ = x[actTransformQuery-120]
_ = x[actTransformSearch-121]
_ = x[actTrigger-122]
_ = x[actBgTransform-123]
_ = x[actBgTransformBorderLabel-124]
_ = x[actBgTransformGhost-125]
_ = x[actBgTransformHeader-126]
_ = x[actBgTransformHeaderLines-127]
_ = x[actBgTransformFooter-128]
_ = x[actBgTransformHeaderLabel-129]
_ = x[actBgTransformFooterLabel-130]
_ = x[actBgTransformInfoCommand-131]
_ = x[actBgTransformInputLabel-132]
_ = x[actBgTransformListLabel-133]
_ = x[actBgTransformNth-134]
_ = x[actBgTransformWithNth-135]
_ = x[actBgTransformPointer-136]
_ = x[actBgTransformPreviewLabel-137]
_ = x[actBgTransformPrompt-138]
_ = x[actBgTransformQuery-139]
_ = x[actBgTransformSearch-140]
_ = x[actBgCancel-141]
_ = x[actSearch-142]
_ = x[actPreview-143]
_ = x[actPreviewTop-144]
_ = x[actPreviewBottom-145]
_ = x[actPreviewUp-146]
_ = x[actPreviewDown-147]
_ = x[actPreviewPageUp-148]
_ = x[actPreviewPageDown-149]
_ = x[actPreviewHalfPageUp-150]
_ = x[actPreviewHalfPageDown-151]
_ = x[actPrevHistory-152]
_ = x[actPrevSelected-153]
_ = x[actPrint-154]
_ = x[actPut-155]
_ = x[actNextHistory-156]
_ = x[actNextSelected-157]
_ = x[actExecute-158]
_ = x[actExecuteSilent-159]
_ = x[actExecuteMulti-160]
_ = x[actSigStop-161]
_ = x[actBest-162]
_ = x[actFirst-163]
_ = x[actLast-164]
_ = x[actReload-165]
_ = x[actReloadSync-166]
_ = x[actDisableSearch-167]
_ = x[actEnableSearch-168]
_ = x[actSelect-169]
_ = x[actDeselect-170]
_ = x[actUnbind-171]
_ = x[actRebind-172]
_ = x[actToggleBind-173]
_ = x[actBecome-174]
_ = x[actShowHeader-175]
_ = x[actHideHeader-176]
_ = x[actBell-177]
_ = x[actExclude-178]
_ = x[actExcludeMulti-179]
_ = x[actAsync-180]
_ = x[actWait-181]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsyncactWait"
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInfoCommandactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInfoCommandactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInfoCommandactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsyncactWait"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 502, 523, 545, 560, 574, 588, 601, 618, 626, 639, 655, 667, 675, 689, 703, 720, 731, 742, 756, 774, 791, 798, 817, 839, 851, 865, 874, 889, 901, 914, 925, 936, 948, 962, 983, 998, 1011, 1028, 1046, 1062, 1074, 1086, 1099, 1114, 1128, 1140, 1152, 1169, 1176, 1188, 1193, 1203, 1212, 1223, 1234, 1247, 1262, 1273, 1286, 1301, 1308, 1321, 1334, 1351, 1366, 1379, 1393, 1407, 1423, 1443, 1467, 1479, 1502, 1519, 1537, 1560, 1578, 1601, 1624, 1646, 1667, 1682, 1701, 1720, 1744, 1762, 1779, 1797, 1807, 1821, 1846, 1865, 1885, 1910, 1930, 1955, 1980, 2004, 2027, 2044, 2065, 2086, 2112, 2132, 2151, 2171, 2182, 2191, 2201, 2214, 2230, 2242, 2256, 2272, 2290, 2310, 2332, 2346, 2361, 2369, 2375, 2389, 2404, 2414, 2430, 2445, 2455, 2462, 2470, 2477, 2486, 2499, 2515, 2530, 2539, 2550, 2559, 2568, 2581, 2590, 2603, 2616, 2623, 2633, 2648, 2656, 2663}
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 411, 430, 448, 462, 474, 490, 506, 522, 543, 565, 580, 594, 608, 621, 638, 646, 659, 675, 687, 695, 709, 723, 740, 751, 762, 776, 794, 811, 818, 837, 859, 871, 885, 894, 909, 921, 934, 945, 956, 968, 982, 1003, 1018, 1031, 1048, 1066, 1082, 1094, 1106, 1119, 1134, 1148, 1160, 1172, 1189, 1196, 1208, 1213, 1223, 1232, 1243, 1254, 1267, 1282, 1293, 1306, 1321, 1328, 1341, 1354, 1371, 1386, 1399, 1413, 1427, 1443, 1463, 1487, 1499, 1522, 1539, 1557, 1580, 1598, 1621, 1644, 1667, 1689, 1710, 1725, 1744, 1763, 1787, 1805, 1822, 1840, 1850, 1864, 1889, 1908, 1928, 1953, 1973, 1998, 2023, 2048, 2072, 2095, 2112, 2133, 2154, 2180, 2200, 2219, 2239, 2250, 2259, 2269, 2282, 2298, 2310, 2324, 2340, 2358, 2378, 2400, 2414, 2429, 2437, 2443, 2457, 2472, 2482, 2498, 2513, 2523, 2530, 2538, 2545, 2554, 2567, 2583, 2598, 2607, 2618, 2627, 2636, 2649, 2658, 2671, 2684, 2691, 2701, 2716, 2724, 2731}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {
idx := int(i) - 0
if i < 0 || idx >= len(_actionType_index)-1 {
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
return _actionType_name[_actionType_index[idx]:_actionType_index[idx+1]]
}
-1
View File
@@ -264,7 +264,6 @@ func Run(opts *Options) (int, error) {
}
pattern := patternBuilder([]rune(*opts.Filter))
matcher.sort = pattern.sortable
transformer := buildItemTransformer(opts)
+14 -7
View File
@@ -1672,21 +1672,21 @@ const (
func init() {
argActionRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|with-nth|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|with-nth|nth|pointer|ghost|info-command)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
func maskActionContents(action string) string {
masked := ""
var sb strings.Builder
Loop:
for len(action) > 0 {
loc := argActionRegexp.FindStringIndex(action)
if loc == nil {
masked += action
sb.WriteString(action)
break
}
masked += action[:loc[1]]
sb.WriteString(action[:loc[1]])
action = action[loc[1]:]
if len(action) == 0 {
break
@@ -1695,7 +1695,7 @@ Loop:
var ce string
switch action[0] {
case ':':
masked += strings.Repeat(" ", len(action))
sb.WriteString(strings.Repeat(" ", len(action)))
break Loop
case '(':
ce = ")"
@@ -1716,7 +1716,7 @@ Loop:
// @$ or @+
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
if loc == nil {
masked += action
sb.WriteString(action)
break
}
// Keep + or , at the end
@@ -1724,9 +1724,10 @@ Loop:
if lastChar == '+' || lastChar == ',' {
loc[1]--
}
masked += strings.Repeat(" ", loc[1])
sb.WriteString(strings.Repeat(" ", loc[1]))
action = action[loc[1]:]
}
masked := sb.String()
masked = strings.ReplaceAll(masked, ",,,", string([]rune{',', escapedComma, ','}))
masked = strings.ReplaceAll(masked, ",:,", string([]rune{',', escapedColon, ','}))
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
@@ -2100,6 +2101,8 @@ func isExecuteAction(str string) actionType {
return actChangeHeaderLabel
case "change-footer-label":
return actChangeFooterLabel
case "change-info-command":
return actChangeInfoCommand
case "change-ghost":
return actChangeGhost
case "change-pointer":
@@ -2144,6 +2147,8 @@ func isExecuteAction(str string) actionType {
return actTransformHeaderLabel
case "transform-footer-label":
return actTransformFooterLabel
case "transform-info-command":
return actTransformInfoCommand
case "transform-footer":
return actTransformFooter
case "transform-header":
@@ -2178,6 +2183,8 @@ func isExecuteAction(str string) actionType {
return actBgTransformHeaderLabel
case "bg-transform-footer-label":
return actBgTransformFooterLabel
case "bg-transform-info-command":
return actBgTransformInfoCommand
case "bg-transform-footer":
return actBgTransformFooter
case "bg-transform-header":
+20 -22
View File
@@ -275,22 +275,12 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
Sort: fastwalk.SortFilesFirst,
}
// When following symlinks, precompute the absolute real paths of walker
// roots so we can skip symlinks that point to an ancestor. fastwalk's
// built-in loop detection (shouldTraverse) catches loops on the second
// pass, but a single pass through a symlink like z: -> / already
// traverses the entire root filesystem, causing severe resource
// exhaustion. Skipping ancestor symlinks prevents this entirely.
var absRoots []string
if opts.follow {
for _, root := range roots {
if real, err := filepath.EvalSymlinks(root); err == nil {
if abs, err := filepath.Abs(real); err == nil {
absRoots = append(absRoots, filepath.Clean(abs))
}
}
}
}
// When following symlinks, skip targets that point to an ancestor of the
// current walker root. fastwalk's built-in loop detection (shouldTraverse)
// catches loops on the second pass, but a single pass through a symlink
// like z: -> / already traverses the entire root filesystem, causing
// severe resource exhaustion. Skipping ancestor symlinks prevents this.
var absRoot string
ignoresBase := []string{}
ignoresFull := []string{}
@@ -326,19 +316,17 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
return filepath.SkipDir
}
// Skip symlinks whose target is an ancestor of (or equal to)
// any walker root. Following such symlinks would traverse a
// the current walker root. Following such symlinks would traverse a
// superset of the tree we're already walking.
if isDirSymlink && len(absRoots) > 0 {
if isDirSymlink && absRoot != "" {
if target, err := filepath.EvalSymlinks(path); err == nil {
if abs, err := filepath.Abs(target); err == nil {
abs = filepath.Clean(abs)
if abs == string(os.PathSeparator) {
return filepath.SkipDir
}
for _, absRoot := range absRoots {
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
return filepath.SkipDir
}
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
return filepath.SkipDir
}
}
}
@@ -377,6 +365,16 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
}
noerr := true
for _, root := range roots {
// Compare against this root only. Safe to update here because
// fastwalk.Walk returns after all its workers finish.
absRoot = ""
if opts.follow {
if real, err := filepath.EvalSymlinks(root); err == nil {
if abs, err := filepath.Abs(real); err == nil {
absRoot = filepath.Clean(abs)
}
}
}
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
}
return noerr
+71
View File
@@ -1,6 +1,10 @@
package fzf
import (
"os"
"path/filepath"
"slices"
"sync"
"testing"
"time"
@@ -66,3 +70,70 @@ func TestReadFromCommand(t *testing.T) {
t.Error("EvtReadFin should be set")
}
}
func walkFiles(t *testing.T, roots ...string) []string {
t.Helper()
var mu sync.Mutex
files := []string{}
reader := NewReader(func(data []byte) bool {
mu.Lock()
defer mu.Unlock()
files = append(files, filepath.FromSlash(string(data)))
return true
}, util.NewEventBox(), nil, false, false)
if !reader.readFiles(roots, walkerOpts{file: true, follow: true}, nil) {
t.Fatal("file walk failed")
}
slices.Sort(files)
return files
}
func TestReadFilesSymlinkAcrossRoots(t *testing.T) {
base := t.TempDir()
app := filepath.Join(base, "app")
shared := filepath.Join(base, "shared")
frontend := filepath.Join(shared, "frontend")
os.MkdirAll(app, 0755)
os.MkdirAll(frontend, 0755)
os.WriteFile(filepath.Join(shared, "docs.txt"), nil, 0644)
os.WriteFile(filepath.Join(frontend, "index.js"), nil, 0644)
if err := os.Symlink(filepath.Join("..", "shared"), filepath.Join(app, "shared")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
viaApp := []string{
filepath.Join(app, "shared", "docs.txt"),
filepath.Join(app, "shared", "frontend", "index.js"),
}
both := append(slices.Clone(viaApp), filepath.Join(frontend, "index.js"))
slices.Sort(both)
for _, tc := range []struct {
roots []string
want []string
}{
{[]string{app}, viaApp},
{[]string{app, frontend}, both},
{[]string{frontend, app}, both},
} {
if files := walkFiles(t, tc.roots...); !slices.Equal(files, tc.want) {
t.Errorf("roots = %v, files = %v, want %v", tc.roots, files, tc.want)
}
}
}
func TestReadFilesSkipsAncestorSymlinks(t *testing.T) {
base := t.TempDir()
project := filepath.Join(base, "project")
os.Mkdir(project, 0755)
file := filepath.Join(project, "file.txt")
os.WriteFile(file, nil, 0644)
os.WriteFile(filepath.Join(base, "outside.txt"), nil, 0644)
for name, target := range map[string]string{"parent": "..", "self": "."} {
if err := os.Symlink(target, filepath.Join(project, name)); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
}
if files := walkFiles(t, project); !slices.Equal(files, []string{file}) {
t.Errorf("files = %v, want [%s]", files, file)
}
}
+13
View File
@@ -589,6 +589,7 @@ const (
actChangeFooter
actChangeHeaderLabel
actChangeFooterLabel
actChangeInfoCommand
actChangeInputLabel
actChangeListLabel
actChangeMulti
@@ -676,6 +677,7 @@ const (
actTransformFooter
actTransformHeaderLabel
actTransformFooterLabel
actTransformInfoCommand
actTransformInputLabel
actTransformListLabel
actTransformNth
@@ -696,6 +698,7 @@ const (
actBgTransformFooter
actBgTransformHeaderLabel
actBgTransformFooterLabel
actBgTransformInfoCommand
actBgTransformInputLabel
actBgTransformListLabel
actBgTransformNth
@@ -764,6 +767,7 @@ func processExecution(action actionType) bool {
actTransformFooter,
actTransformHeaderLabel,
actTransformFooterLabel,
actTransformInfoCommand,
actTransformInputLabel,
actTransformListLabel,
actTransformNth,
@@ -781,6 +785,7 @@ func processExecution(action actionType) bool {
actBgTransformFooter,
actBgTransformHeaderLabel,
actBgTransformFooterLabel,
actBgTransformInfoCommand,
actBgTransformInputLabel,
actBgTransformListLabel,
actBgTransformNth,
@@ -792,6 +797,7 @@ func processExecution(action actionType) bool {
actBgTransformSearch,
actPreview,
actChangePreview,
actChangeInfoCommand,
actRefreshPreview,
actExecute,
actExecuteSilent,
@@ -7447,6 +7453,13 @@ func (t *Terminal) Loop() error {
t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false)
req(reqRedrawFooterLabel)
})
case actChangeInfoCommand, actTransformInfoCommand, actBgTransformInfoCommand:
capture(true, func(command string) {
if t.infoCommand != command {
t.infoCommand = command
req(reqInfo)
}
})
case actChangeInputLabel, actTransformInputLabel, actBgTransformInputLabel:
capture(true, func(label string) {
t.inputLabelOpts.label = label
+30
View File
@@ -1826,6 +1826,36 @@ class TestCore < TestInteractive
tmux.until { assert_match(/^ 2 xx/, it[-2]) }
end
def test_change_info_command
tmux.send_keys(%(seq 100 | #{FZF} --separator x --bind 'a:change-info-command(echo {}: $FZF_INFO)' --bind 'b:change-info-command()'), :Enter)
tmux.until { assert_match(%r{^ 100/100 xx}, it[-2]) }
tmux.send_keys :a
tmux.until { assert_match(%r{^ 1: 100/100 xx}, it[-2]) }
# Placeholders are evaluated on each update, not when the action runs
tmux.send_keys :Up
tmux.until { assert_match(%r{^ 2: 100/100 xx}, it[-2]) }
tmux.send_keys :b
tmux.until { assert_match(%r{^ 100/100 xx}, it[-2]) }
end
def test_transform_info_command
tmux.send_keys(%(seq 100 | #{FZF} --separator x --bind 'a:transform-info-command(echo "echo {}: \\$FZF_POS")'), :Enter)
tmux.until { assert_match(%r{^ 100/100 xx}, it[-2]) }
tmux.send_keys :a
tmux.until { assert_match(%r{^ 1: 1 xx}, it[-2]) }
tmux.send_keys :Up
tmux.until { assert_match(%r{^ 1: 2 xx}, it[-2]) }
end
def test_bg_transform_info_command
tmux.send_keys(%(seq 100 | #{FZF} --separator x --bind 'a:bg-transform-info-command(sleep 0.5; echo "echo {}: \\$FZF_POS")'), :Enter)
tmux.until { assert_match(%r{^ 100/100 xx}, it[-2]) }
tmux.send_keys :a
tmux.until { assert_match(%r{^ 1: 1 xx}, it[-2]) }
tmux.send_keys :Up
tmux.until { assert_match(%r{^ 1: 2 xx}, it[-2]) }
end
def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
+9
View File
@@ -25,6 +25,15 @@ class TestFilter < TestBase
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
end
def test_filter_no_sort
input = "printf 'abz\\nab\\nzab\\n'"
['', '--sync'].each do |opts|
assert_equal %w[ab abz zab], `#{input} | #{FZF} -f ab #{opts}`.lines(chomp: true)
assert_equal %w[abz ab zab], `#{input} | #{FZF} -f ab --no-sort #{opts}`.lines(chomp: true)
assert_equal %w[zab ab abz], `#{input} | #{FZF} -f ab --no-sort --tac #{opts}`.lines(chomp: true)
end
end
def test_filter_exitstatus
# filter / streaming filter
['', '--no-sort'].each do |opts|