Add change-with-nth action to dynamically change --with-nth

This commit is contained in:
Junegunn Choi
2026-02-24 20:29:07 +09:00
parent ff6a3bbee0
commit ed27d36bb2
8 changed files with 453 additions and 179 deletions

View File

@@ -1,6 +1,14 @@
CHANGELOG CHANGELOG
========= =========
0.69.0
------
- Added `change-with-nth` action for dynamically changing the `--with-nth` option
```sh
echo -e "a b c\nd e f\ng h i" | fzf --with-nth .. \
--bind 'space:change-with-nth(1|2|3|1,3|2,3|)'
```
0.68.0 0.68.0
------ ------
- Implemented word wrapping in the list section - Implemented word wrapping in the list section

View File

@@ -134,6 +134,14 @@ e.g.
# Use template to rearrange fields # Use template to rearrange fields
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}' echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
.RE .RE
.RS
\fBchange\-with\-nth\fR action is only available when \fB\-\-with\-nth\fR is set.
When \fB\-\-with\-nth\fR is used, fzf retains the original input lines in memory
so they can be re\-transformed on the fly (e.g. \fB\-\-with\-nth ..\fR to keep
the original presentation). This increases memory usage, so only use
\fB\-\-with\-nth\fR when you actually need field transformation.
.RE
.TP .TP
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE" .BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
Define which fields to print on accept. The last delimiter is stripped from the Define which fields to print on accept. The last delimiter is stripped from the
@@ -1398,6 +1406,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_NTH " Current \-\-nth option" .BR FZF_NTH " Current \-\-nth option"
.br .br
.BR FZF_WITH_NTH " Current \-\-with\-nth option"
.br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
.br .br
.BR FZF_GHOST " Ghost string" .BR FZF_GHOST " Ghost string"
@@ -1888,6 +1898,7 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-multi\fR (enable multi-select mode with no limit) \fBchange\-multi\fR (enable multi-select mode with no limit)
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0) \fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|') \fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
\fBchange\-with\-nth(...)\fR (change \fB\-\-with\-nth\fR option; rotate through the multiple options separated by '|')
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option) \fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option) \fBchange\-preview(...)\fR (change \fB\-\-preview\fR option)
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string) \fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
@@ -1993,6 +2004,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-input\-label(...)\fR (transform input label using an external command) \fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command) \fBtransform\-list\-label(...)\fR (transform list label using an external command)
\fBtransform\-nth(...)\fR (transform nth using an external command) \fBtransform\-nth(...)\fR (transform nth using an external command)
\fBtransform\-with\-nth(...)\fR (transform with-nth using an external command)
\fBtransform\-pointer(...)\fR (transform pointer using an external command) \fBtransform\-pointer(...)\fR (transform pointer using an external command)
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command) \fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
\fBtransform\-prompt(...)\fR (transform prompt string using an external command) \fBtransform\-prompt(...)\fR (transform prompt string using an external command)

View File

@@ -38,156 +38,159 @@ func _() {
_ = x[actChangeListLabel-27] _ = x[actChangeListLabel-27]
_ = x[actChangeMulti-28] _ = x[actChangeMulti-28]
_ = x[actChangeNth-29] _ = x[actChangeNth-29]
_ = x[actChangePointer-30] _ = x[actChangeWithNth-30]
_ = x[actChangePreview-31] _ = x[actChangePointer-31]
_ = x[actChangePreviewLabel-32] _ = x[actChangePreview-32]
_ = x[actChangePreviewWindow-33] _ = x[actChangePreviewLabel-33]
_ = x[actChangePrompt-34] _ = x[actChangePreviewWindow-34]
_ = x[actChangeQuery-35] _ = x[actChangePrompt-35]
_ = x[actClearScreen-36] _ = x[actChangeQuery-36]
_ = x[actClearQuery-37] _ = x[actClearScreen-37]
_ = x[actClearSelection-38] _ = x[actClearQuery-38]
_ = x[actClose-39] _ = x[actClearSelection-39]
_ = x[actDeleteChar-40] _ = x[actClose-40]
_ = x[actDeleteCharEof-41] _ = x[actDeleteChar-41]
_ = x[actEndOfLine-42] _ = x[actDeleteCharEof-42]
_ = x[actFatal-43] _ = x[actEndOfLine-43]
_ = x[actForwardChar-44] _ = x[actFatal-44]
_ = x[actForwardWord-45] _ = x[actForwardChar-45]
_ = x[actForwardSubWord-46] _ = x[actForwardWord-46]
_ = x[actKillLine-47] _ = x[actForwardSubWord-47]
_ = x[actKillWord-48] _ = x[actKillLine-48]
_ = x[actKillSubWord-49] _ = x[actKillWord-49]
_ = x[actUnixLineDiscard-50] _ = x[actKillSubWord-50]
_ = x[actUnixWordRubout-51] _ = x[actUnixLineDiscard-51]
_ = x[actYank-52] _ = x[actUnixWordRubout-52]
_ = x[actBackwardKillWord-53] _ = x[actYank-53]
_ = x[actBackwardKillSubWord-54] _ = x[actBackwardKillWord-54]
_ = x[actSelectAll-55] _ = x[actBackwardKillSubWord-55]
_ = x[actDeselectAll-56] _ = x[actSelectAll-56]
_ = x[actToggle-57] _ = x[actDeselectAll-57]
_ = x[actToggleSearch-58] _ = x[actToggle-58]
_ = x[actToggleAll-59] _ = x[actToggleSearch-59]
_ = x[actToggleDown-60] _ = x[actToggleAll-60]
_ = x[actToggleUp-61] _ = x[actToggleDown-61]
_ = x[actToggleIn-62] _ = x[actToggleUp-62]
_ = x[actToggleOut-63] _ = x[actToggleIn-63]
_ = x[actToggleTrack-64] _ = x[actToggleOut-64]
_ = x[actToggleTrackCurrent-65] _ = x[actToggleTrack-65]
_ = x[actToggleHeader-66] _ = x[actToggleTrackCurrent-66]
_ = x[actToggleWrap-67] _ = x[actToggleHeader-67]
_ = x[actToggleWrapWord-68] _ = x[actToggleWrap-68]
_ = x[actToggleMultiLine-69] _ = x[actToggleWrapWord-69]
_ = x[actToggleHscroll-70] _ = x[actToggleMultiLine-70]
_ = x[actToggleRaw-71] _ = x[actToggleHscroll-71]
_ = x[actEnableRaw-72] _ = x[actToggleRaw-72]
_ = x[actDisableRaw-73] _ = x[actEnableRaw-73]
_ = x[actTrackCurrent-74] _ = x[actDisableRaw-74]
_ = x[actToggleInput-75] _ = x[actTrackCurrent-75]
_ = x[actHideInput-76] _ = x[actToggleInput-76]
_ = x[actShowInput-77] _ = x[actHideInput-77]
_ = x[actUntrackCurrent-78] _ = x[actShowInput-78]
_ = x[actDown-79] _ = x[actUntrackCurrent-79]
_ = x[actDownMatch-80] _ = x[actDown-80]
_ = x[actUp-81] _ = x[actDownMatch-81]
_ = x[actUpMatch-82] _ = x[actUp-82]
_ = x[actPageUp-83] _ = x[actUpMatch-83]
_ = x[actPageDown-84] _ = x[actPageUp-84]
_ = x[actPosition-85] _ = x[actPageDown-85]
_ = x[actHalfPageUp-86] _ = x[actPosition-86]
_ = x[actHalfPageDown-87] _ = x[actHalfPageUp-87]
_ = x[actOffsetUp-88] _ = x[actHalfPageDown-88]
_ = x[actOffsetDown-89] _ = x[actOffsetUp-89]
_ = x[actOffsetMiddle-90] _ = x[actOffsetDown-90]
_ = x[actJump-91] _ = x[actOffsetMiddle-91]
_ = x[actJumpAccept-92] _ = x[actJump-92]
_ = x[actPrintQuery-93] _ = x[actJumpAccept-93]
_ = x[actRefreshPreview-94] _ = x[actPrintQuery-94]
_ = x[actReplaceQuery-95] _ = x[actRefreshPreview-95]
_ = x[actToggleSort-96] _ = x[actReplaceQuery-96]
_ = x[actShowPreview-97] _ = x[actToggleSort-97]
_ = x[actHidePreview-98] _ = x[actShowPreview-98]
_ = x[actTogglePreview-99] _ = x[actHidePreview-99]
_ = x[actTogglePreviewWrap-100] _ = x[actTogglePreview-100]
_ = x[actTogglePreviewWrapWord-101] _ = x[actTogglePreviewWrap-101]
_ = x[actTransform-102] _ = x[actTogglePreviewWrapWord-102]
_ = x[actTransformBorderLabel-103] _ = x[actTransform-103]
_ = x[actTransformGhost-104] _ = x[actTransformBorderLabel-104]
_ = x[actTransformHeader-105] _ = x[actTransformGhost-105]
_ = x[actTransformHeaderLines-106] _ = x[actTransformHeader-106]
_ = x[actTransformFooter-107] _ = x[actTransformHeaderLines-107]
_ = x[actTransformHeaderLabel-108] _ = x[actTransformFooter-108]
_ = x[actTransformFooterLabel-109] _ = x[actTransformHeaderLabel-109]
_ = x[actTransformInputLabel-110] _ = x[actTransformFooterLabel-110]
_ = x[actTransformListLabel-111] _ = x[actTransformInputLabel-111]
_ = x[actTransformNth-112] _ = x[actTransformListLabel-112]
_ = x[actTransformPointer-113] _ = x[actTransformNth-113]
_ = x[actTransformPreviewLabel-114] _ = x[actTransformWithNth-114]
_ = x[actTransformPrompt-115] _ = x[actTransformPointer-115]
_ = x[actTransformQuery-116] _ = x[actTransformPreviewLabel-116]
_ = x[actTransformSearch-117] _ = x[actTransformPrompt-117]
_ = x[actTrigger-118] _ = x[actTransformQuery-118]
_ = x[actBgTransform-119] _ = x[actTransformSearch-119]
_ = x[actBgTransformBorderLabel-120] _ = x[actTrigger-120]
_ = x[actBgTransformGhost-121] _ = x[actBgTransform-121]
_ = x[actBgTransformHeader-122] _ = x[actBgTransformBorderLabel-122]
_ = x[actBgTransformHeaderLines-123] _ = x[actBgTransformGhost-123]
_ = x[actBgTransformFooter-124] _ = x[actBgTransformHeader-124]
_ = x[actBgTransformHeaderLabel-125] _ = x[actBgTransformHeaderLines-125]
_ = x[actBgTransformFooterLabel-126] _ = x[actBgTransformFooter-126]
_ = x[actBgTransformInputLabel-127] _ = x[actBgTransformHeaderLabel-127]
_ = x[actBgTransformListLabel-128] _ = x[actBgTransformFooterLabel-128]
_ = x[actBgTransformNth-129] _ = x[actBgTransformInputLabel-129]
_ = x[actBgTransformPointer-130] _ = x[actBgTransformListLabel-130]
_ = x[actBgTransformPreviewLabel-131] _ = x[actBgTransformNth-131]
_ = x[actBgTransformPrompt-132] _ = x[actBgTransformWithNth-132]
_ = x[actBgTransformQuery-133] _ = x[actBgTransformPointer-133]
_ = x[actBgTransformSearch-134] _ = x[actBgTransformPreviewLabel-134]
_ = x[actBgCancel-135] _ = x[actBgTransformPrompt-135]
_ = x[actSearch-136] _ = x[actBgTransformQuery-136]
_ = x[actPreview-137] _ = x[actBgTransformSearch-137]
_ = x[actPreviewTop-138] _ = x[actBgCancel-138]
_ = x[actPreviewBottom-139] _ = x[actSearch-139]
_ = x[actPreviewUp-140] _ = x[actPreview-140]
_ = x[actPreviewDown-141] _ = x[actPreviewTop-141]
_ = x[actPreviewPageUp-142] _ = x[actPreviewBottom-142]
_ = x[actPreviewPageDown-143] _ = x[actPreviewUp-143]
_ = x[actPreviewHalfPageUp-144] _ = x[actPreviewDown-144]
_ = x[actPreviewHalfPageDown-145] _ = x[actPreviewPageUp-145]
_ = x[actPrevHistory-146] _ = x[actPreviewPageDown-146]
_ = x[actPrevSelected-147] _ = x[actPreviewHalfPageUp-147]
_ = x[actPrint-148] _ = x[actPreviewHalfPageDown-148]
_ = x[actPut-149] _ = x[actPrevHistory-149]
_ = x[actNextHistory-150] _ = x[actPrevSelected-150]
_ = x[actNextSelected-151] _ = x[actPrint-151]
_ = x[actExecute-152] _ = x[actPut-152]
_ = x[actExecuteSilent-153] _ = x[actNextHistory-153]
_ = x[actExecuteMulti-154] _ = x[actNextSelected-154]
_ = x[actSigStop-155] _ = x[actExecute-155]
_ = x[actBest-156] _ = x[actExecuteSilent-156]
_ = x[actFirst-157] _ = x[actExecuteMulti-157]
_ = x[actLast-158] _ = x[actSigStop-158]
_ = x[actReload-159] _ = x[actBest-159]
_ = x[actReloadSync-160] _ = x[actFirst-160]
_ = x[actDisableSearch-161] _ = x[actLast-161]
_ = x[actEnableSearch-162] _ = x[actReload-162]
_ = x[actSelect-163] _ = x[actReloadSync-163]
_ = x[actDeselect-164] _ = x[actDisableSearch-164]
_ = x[actUnbind-165] _ = x[actEnableSearch-165]
_ = x[actRebind-166] _ = x[actSelect-166]
_ = x[actToggleBind-167] _ = x[actDeselect-167]
_ = x[actBecome-168] _ = x[actUnbind-168]
_ = x[actShowHeader-169] _ = x[actRebind-169]
_ = x[actHideHeader-170] _ = x[actToggleBind-170]
_ = x[actBell-171] _ = x[actBecome-171]
_ = x[actExclude-172] _ = x[actShowHeader-172]
_ = x[actExcludeMulti-173] _ = x[actHideHeader-173]
_ = x[actAsync-174] _ = x[actBell-174]
_ = x[actExclude-175]
_ = x[actExcludeMulti-176]
_ = x[actAsync-177]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
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, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600} 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}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -99,6 +99,21 @@ func (cl *ChunkList) Clear() {
cl.mutex.Unlock() cl.mutex.Unlock()
} }
// Retransform iterates all items and applies fn to each one.
// The done callback runs under the lock to safely update shared state.
func (cl *ChunkList) Retransform(fn func(*Item), done func()) {
cl.mutex.Lock()
for _, chunk := range cl.chunks {
for i := 0; i < chunk.count; i++ {
fn(&chunk.items[i])
}
}
if done != nil {
done()
}
cl.mutex.Unlock()
}
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) { func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
cl.mutex.Lock() cl.mutex.Lock()

View File

@@ -112,6 +112,42 @@ func Run(opts *Options) (int, error) {
cache := NewChunkCache() cache := NewChunkCache()
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32 var itemIndex int32
// transformItem applies with-nth transformation to an item's raw data.
// It handles ANSI token propagation using prevLineAnsiState for cross-line continuity.
transformItem := func(item *Item, data []byte, transformer func([]Token, int32) string, index int32) {
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
ansiStateDup := *prevLineAnsiState
ansiState = &ansiStateDup
}
for _, token := range tokens {
prevAnsiState := ansiState
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
if prevAnsiState != nil {
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
} else {
token.text.Prepend("\x1b[m")
}
}
}
transformed := transformer(tokens, index)
item.text, item.colors = ansiProcessor(stringBytes(transformed))
// We should not trim trailing whitespaces with background colors
var maxColorOffset int32
if item.colors != nil {
for _, ansi := range *item.colors {
if ansi.color.bg >= 0 {
maxColorOffset = max(maxColorOffset, ansi.offset[1])
}
}
}
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
}
var nthTransformer func([]Token, int32) string
if opts.WithNth == nil { if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
item.text, item.colors = ansiProcessor(data) item.text, item.colors = ansiProcessor(data)
@@ -120,38 +156,13 @@ func Run(opts *Options) (int, error) {
return true return true
}) })
} else { } else {
nthTransformer := opts.WithNth(opts.Delimiter) nthTransformer = opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter) if nthTransformer == nil {
if opts.Ansi && len(tokens) > 1 { item.text, item.colors = ansiProcessor(data)
var ansiState *ansiState } else {
if prevLineAnsiState != nil { transformItem(item, data, nthTransformer, itemIndex)
ansiStateDup := *prevLineAnsiState
ansiState = &ansiStateDup
}
for _, token := range tokens {
prevAnsiState := ansiState
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
if prevAnsiState != nil {
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
} else {
token.text.Prepend("\x1b[m")
}
}
} }
transformed := nthTransformer(tokens, itemIndex)
item.text, item.colors = ansiProcessor(stringBytes(transformed))
// We should not trim trailing whitespaces with background colors
var maxColorOffset int32
if item.colors != nil {
for _, ansi := range *item.colors {
if ansi.color.bg >= 0 {
maxColorOffset = max(maxColorOffset, ansi.offset[1])
}
}
}
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@@ -420,6 +431,7 @@ func Run(opts *Options) (int, error) {
var environ []string var environ []string
var changed bool var changed bool
headerLinesChanged := false headerLinesChanged := false
withNthChanged := false
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
@@ -446,6 +458,27 @@ func Run(opts *Options) (int, error) {
headerLinesChanged = true headerLinesChanged = true
bump = true bump = true
} }
if val.withNth != nil {
newTransformer := val.withNth.fn
// Reset cross-line ANSI state before re-processing all items
lineAnsiState = nil
prevLineAnsiState = nil
chunkList.Retransform(func(item *Item) {
origBytes := *item.origText
savedIndex := item.Index()
if newTransformer != nil {
transformItem(item, origBytes, newTransformer, savedIndex)
} else {
item.text, item.colors = ansiProcessor(origBytes)
}
item.text.Index = savedIndex
item.transformed = nil
}, func() {
nthTransformer = newTransformer
})
withNthChanged = true
bump = true
}
if bump { if bump {
patternCache = make(map[string]*Pattern) patternCache = make(map[string]*Pattern)
cache.Clear() cache.Clear()
@@ -489,6 +522,8 @@ func Run(opts *Options) (int, error) {
} else { } else {
terminal.UpdateHeader(nil) terminal.UpdateHeader(nil)
} }
} else if withNthChanged && headerLines > 0 {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
} }
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision) matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false

View File

@@ -587,6 +587,7 @@ type Options struct {
FreezeLeft int FreezeLeft int
FreezeRight int FreezeRight int
WithNth func(Delimiter) func([]Token, int32) string WithNth func(Delimiter) func([]Token, int32) string
WithNthExpr string
AcceptNth func(Delimiter) func([]Token, int32) string AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
@@ -1626,7 +1627,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = 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|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)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -2069,6 +2070,8 @@ func isExecuteAction(str string) actionType {
return actChangeMulti return actChangeMulti
case "change-nth": case "change-nth":
return actChangeNth return actChangeNth
case "change-with-nth":
return actChangeWithNth
case "pos": case "pos":
return actPosition return actPosition
case "execute": case "execute":
@@ -2105,6 +2108,8 @@ func isExecuteAction(str string) actionType {
return actTransformGhost return actTransformGhost
case "transform-nth": case "transform-nth":
return actTransformNth return actTransformNth
case "transform-with-nth":
return actTransformWithNth
case "transform-pointer": case "transform-pointer":
return actTransformPointer return actTransformPointer
case "transform-prompt": case "transform-prompt":
@@ -2137,6 +2142,8 @@ func isExecuteAction(str string) actionType {
return actBgTransformGhost return actBgTransformGhost
case "bg-transform-nth": case "bg-transform-nth":
return actBgTransformNth return actBgTransformNth
case "bg-transform-with-nth":
return actBgTransformWithNth
case "bg-transform-pointer": case "bg-transform-pointer":
return actBgTransformPointer return actBgTransformPointer
case "bg-transform-prompt": case "bg-transform-prompt":
@@ -2778,6 +2785,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.WithNth, err = nthTransformer(str); err != nil { if opts.WithNth, err = nthTransformer(str); err != nil {
return err return err
} }
opts.WithNthExpr = str
case "--accept-nth": case "--accept-nth":
str, err := nextString("nth expression required") str, err := nextString("nth expression required")
if err != nil { if err != nil {

View File

@@ -340,6 +340,8 @@ type Terminal struct {
nthAttr tui.Attr nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range nthCurrent []Range
withNthExpr string
withNthEnabled bool
acceptNth func([]Token, int32) string acceptNth func([]Token, int32) string
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
@@ -384,6 +386,7 @@ type Terminal struct {
hasLoadActions bool hasLoadActions bool
hasResizeActions bool hasResizeActions bool
triggerLoad bool triggerLoad bool
filterSelection bool
reading bool reading bool
running *util.AtomicBool running *util.AtomicBool
failed *string failed *string
@@ -551,6 +554,7 @@ const (
actChangeListLabel actChangeListLabel
actChangeMulti actChangeMulti
actChangeNth actChangeNth
actChangeWithNth
actChangePointer actChangePointer
actChangePreview actChangePreview
actChangePreviewLabel actChangePreviewLabel
@@ -636,6 +640,7 @@ const (
actTransformInputLabel actTransformInputLabel
actTransformListLabel actTransformListLabel
actTransformNth actTransformNth
actTransformWithNth
actTransformPointer actTransformPointer
actTransformPreviewLabel actTransformPreviewLabel
actTransformPrompt actTransformPrompt
@@ -655,6 +660,7 @@ const (
actBgTransformInputLabel actBgTransformInputLabel
actBgTransformListLabel actBgTransformListLabel
actBgTransformNth actBgTransformNth
actBgTransformWithNth
actBgTransformPointer actBgTransformPointer
actBgTransformPreviewLabel actBgTransformPreviewLabel
actBgTransformPrompt actBgTransformPrompt
@@ -721,6 +727,7 @@ func processExecution(action actionType) bool {
actTransformInputLabel, actTransformInputLabel,
actTransformListLabel, actTransformListLabel,
actTransformNth, actTransformNth,
actTransformWithNth,
actTransformPointer, actTransformPointer,
actTransformPreviewLabel, actTransformPreviewLabel,
actTransformPrompt, actTransformPrompt,
@@ -737,6 +744,7 @@ func processExecution(action actionType) bool {
actBgTransformInputLabel, actBgTransformInputLabel,
actBgTransformListLabel, actBgTransformListLabel,
actBgTransformNth, actBgTransformNth,
actBgTransformWithNth,
actBgTransformPointer, actBgTransformPointer,
actBgTransformPreviewLabel, actBgTransformPreviewLabel,
actBgTransformPrompt, actBgTransformPrompt,
@@ -766,10 +774,15 @@ type placeholderFlags struct {
raw bool raw bool
} }
type withNthSpec struct {
fn func([]Token, int32) string // nil = clear (restore original)
}
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool sync bool
nth *[]Range nth *[]Range
withNth *withNthSpec
headerLines *int headerLines *int
command *commandSpec command *commandSpec
environ []string environ []string
@@ -1080,6 +1093,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
nthAttr: opts.Theme.Nth.Attr, nthAttr: opts.Theme.Nth.Attr,
nth: opts.Nth, nth: opts.Nth,
nthCurrent: opts.Nth, nthCurrent: opts.Nth,
withNthExpr: opts.WithNthExpr,
withNthEnabled: opts.WithNth != nil,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
raw: opts.Raw, raw: opts.Raw,
hasStartActions: false, hasStartActions: false,
@@ -1351,6 +1366,9 @@ func (t *Terminal) environImpl(forPreview bool) []string {
if len(t.nthCurrent) > 0 { if len(t.nthCurrent) > 0 {
env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent)) env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
} }
if len(t.withNthExpr) > 0 {
env = append(env, "FZF_WITH_NTH="+t.withNthExpr)
}
if t.raw { if t.raw {
val := "0" val := "0"
if t.isCurrentItemMatch() { if t.isCurrentItemMatch() {
@@ -1843,6 +1861,18 @@ func (t *Terminal) UpdateList(result MatchResult) {
t.revision = newRevision t.revision = newRevision
t.version++ t.version++
} }
if t.filterSelection && t.multi > 0 && len(t.selected) > 0 {
t.filterSelection = false
matchMap := t.resultMerger.ToMap()
filtered := make(map[int32]selectedItem)
for k, v := range t.selected {
if _, matched := matchMap[k]; matched {
filtered[k] = v
}
}
t.selected = filtered
}
t.filterSelection = false
if t.triggerLoad { if t.triggerLoad {
t.triggerLoad = false t.triggerLoad = false
t.eventChan <- tui.Load.AsEvent() t.eventChan <- tui.Load.AsEvent()
@@ -5922,6 +5952,7 @@ func (t *Terminal) Loop() error {
events := []util.EventType{} events := []util.EventType{}
changed := false changed := false
var newNth *[]Range var newNth *[]Range
var newWithNth *withNthSpec
var newHeaderLines *int var newHeaderLines *int
req := func(evts ...util.EventType) { req := func(evts ...util.EventType) {
for _, event := range evts { for _, event := range evts {
@@ -5939,6 +5970,7 @@ func (t *Terminal) Loop() error {
events = []util.EventType{} events = []util.EventType{}
changed = false changed = false
newNth = nil newNth = nil
newWithNth = nil
newHeaderLines = nil newHeaderLines = nil
beof := false beof := false
queryChanged := false queryChanged := false
@@ -6330,6 +6362,30 @@ func (t *Terminal) Loop() error {
t.forceRerenderList() t.forceRerenderList()
} }
}) })
case actChangeWithNth, actTransformWithNth, actBgTransformWithNth:
if !t.withNthEnabled {
break Action
}
capture(true, func(expr string) {
tokens := strings.Split(expr, "|")
withNthExpr := tokens[0]
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
}
if withNthExpr != t.withNthExpr {
if len(withNthExpr) == 0 {
newWithNth = &withNthSpec{fn: nil}
} else if factory, err := nthTransformer(withNthExpr); err == nil {
newWithNth = &withNthSpec{fn: factory(t.delimiter)}
} else {
return
}
t.withNthExpr = withNthExpr
t.filterSelection = true
changed = true
t.forceRerenderList()
}
})
case actChangeQuery: case actChangeQuery:
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)
@@ -7477,7 +7533,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil reload := changed || newCommand != nil
var reloadRequest *searchRequest var reloadRequest *searchRequest
if reload { if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, withNth: newWithNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
} }
// Dispatch queued background requests // Dispatch queued background requests

View File

@@ -1745,6 +1745,143 @@ class TestCore < TestInteractive
end end
end end
def test_change_with_nth
input = [
'foo bar baz',
'aaa bbb ccc',
'xxx yyy zzz'
]
writelines(input)
# Start with field 1 only, cycle through fields, verify $FZF_WITH_NTH via prompt
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:change-with-nth(2|3|1),result:transform-prompt:echo "[$FZF_WITH_NTH]> "' < #{tempname}), :Enter
tmux.until do |lines|
assert_equal 3, lines.item_count
assert lines.any_include?('[1]>')
assert lines.any_include?('foo')
refute lines.any_include?('bar')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('[2]>')
assert lines.any_include?('bar')
refute lines.any_include?('foo')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('[3]>')
assert lines.any_include?('baz')
refute lines.any_include?('bar')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('[1]>')
assert lines.any_include?('foo')
refute lines.any_include?('bar')
end
end
def test_change_with_nth_clear
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 1 --bind 'space:change-with-nth()'), :Enter
tmux.until do |lines|
assert_equal 2, lines.item_count
assert lines.any_include?('a')
refute lines.any_include?('b')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('a b c')
end
end
def test_transform_with_nth_search
input = [
'alpha bravo charlie',
'delta echo foxtrot',
'golf hotel india'
]
writelines(input)
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:transform-with-nth(2)' -q bravo < #{tempname}), :Enter
tmux.until do |lines|
assert_equal 0, lines.match_count
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.match_count
end
end
def test_bg_transform_with_nth_output
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 2 --bind 'space:bg-transform-with-nth(3)'), :Enter
tmux.until do |lines|
assert_equal 2, lines.item_count
assert lines.any_include?('b')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('c')
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('a b c') || lines.any_include?('d e f') }
end
def test_change_with_nth_search
input = [
'alpha bravo charlie',
'delta echo foxtrot',
'golf hotel india'
]
writelines(input)
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:change-with-nth(2)' -q bravo < #{tempname}), :Enter
tmux.until do |lines|
assert_equal 0, lines.match_count
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 1, lines.match_count
end
end
def test_change_with_nth_output
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 2 --bind 'space:change-with-nth(3)'), :Enter
tmux.until do |lines|
assert_equal 2, lines.item_count
assert lines.any_include?('b')
end
tmux.send_keys :Space
tmux.until do |lines|
assert lines.any_include?('c')
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('a b c') || lines.any_include?('d e f') }
end
def test_change_with_nth_selection
# Items: field1 has unique values, field2 has 'match' or 'miss'
input = [
'one match x',
'two miss y',
'three match z'
]
writelines(input)
# Start showing field 2 (match/miss), query 'match', select all matches, then switch to field 3
tmux.send_keys %(#{FZF} --with-nth 2 --multi --bind 'ctrl-a:select-all,space:change-with-nth(3)' -q match < #{tempname}), :Enter
tmux.until do |lines|
assert_equal 2, lines.match_count
end
# Select all matching items
tmux.send_keys 'C-a'
tmux.until do |lines|
assert lines.any_include?('(2)')
end
# Now change with-nth to field 3; 'x' and 'z' don't contain 'match'
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 0, lines.match_count
# Selections of non-matching items should be cleared
assert lines.any_include?('(0)')
end
end
def test_env_vars def test_env_vars
def env_vars def env_vars
return {} unless File.exist?(tempname) return {} unless File.exist?(tempname)