Add change-header-lines action to dynamically change --header-lines

All input lines now enter the chunklist with sequential indices, and
header lines are excluded from matching via Pattern.startIndex and
PassMerger offset. This allows the number of header lines to be changed
at runtime with change-header-lines(N), transform-header-lines, and
bg-transform-header-lines actions.

- Remove EvtHeader event; header items are read directly from chunks
- Add startIndex to Pattern and PassMerger for skipping header items
- Add targetIndex field to Terminal for cursor repositioning across
  header-lines changes

Close #4659
This commit is contained in:
Junegunn Choi
2026-02-23 01:45:47 +09:00
parent 98a3b1fff8
commit b9804f5873
13 changed files with 420 additions and 227 deletions

View File

@@ -1881,6 +1881,7 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string) \fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
\fBchange\-ghost(...)\fR (change ghost text to the given string) \fBchange\-ghost(...)\fR (change ghost text to the given string)
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR) \fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
\fBchange\-header\-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\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string) \fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string) \fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
@@ -1987,6 +1988,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-border\-label(...)\fR (transform border label using an external command) \fBtransform\-border\-label(...)\fR (transform border label using an external command)
\fBtransform\-ghost(...)\fR (transform ghost text using an external command) \fBtransform\-ghost(...)\fR (transform ghost text using an external command)
\fBtransform\-header(...)\fR (transform header using an external command) \fBtransform\-header(...)\fR (transform header using an external command)
\fBtransform\-header\-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\-header\-label(...)\fR (transform header label using an external command)
\fBtransform\-input\-label(...)\fR (transform input label using an external command) \fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command) \fBtransform\-list\-label(...)\fR (transform list label using an external command)

View File

@@ -30,161 +30,164 @@ func _() {
_ = x[actChangeBorderLabel-19] _ = x[actChangeBorderLabel-19]
_ = x[actChangeGhost-20] _ = x[actChangeGhost-20]
_ = x[actChangeHeader-21] _ = x[actChangeHeader-21]
_ = x[actChangeFooter-22] _ = x[actChangeHeaderLines-22]
_ = x[actChangeHeaderLabel-23] _ = x[actChangeFooter-23]
_ = x[actChangeFooterLabel-24] _ = x[actChangeHeaderLabel-24]
_ = x[actChangeInputLabel-25] _ = x[actChangeFooterLabel-25]
_ = x[actChangeListLabel-26] _ = x[actChangeInputLabel-26]
_ = x[actChangeMulti-27] _ = x[actChangeListLabel-27]
_ = x[actChangeNth-28] _ = x[actChangeMulti-28]
_ = x[actChangePointer-29] _ = x[actChangeNth-29]
_ = x[actChangePreview-30] _ = x[actChangePointer-30]
_ = x[actChangePreviewLabel-31] _ = x[actChangePreview-31]
_ = x[actChangePreviewWindow-32] _ = x[actChangePreviewLabel-32]
_ = x[actChangePrompt-33] _ = x[actChangePreviewWindow-33]
_ = x[actChangeQuery-34] _ = x[actChangePrompt-34]
_ = x[actClearScreen-35] _ = x[actChangeQuery-35]
_ = x[actClearQuery-36] _ = x[actClearScreen-36]
_ = x[actClearSelection-37] _ = x[actClearQuery-37]
_ = x[actClose-38] _ = x[actClearSelection-38]
_ = x[actDeleteChar-39] _ = x[actClose-39]
_ = x[actDeleteCharEof-40] _ = x[actDeleteChar-40]
_ = x[actEndOfLine-41] _ = x[actDeleteCharEof-41]
_ = x[actFatal-42] _ = x[actEndOfLine-42]
_ = x[actForwardChar-43] _ = x[actFatal-43]
_ = x[actForwardWord-44] _ = x[actForwardChar-44]
_ = x[actForwardSubWord-45] _ = x[actForwardWord-45]
_ = x[actKillLine-46] _ = x[actForwardSubWord-46]
_ = x[actKillWord-47] _ = x[actKillLine-47]
_ = x[actKillSubWord-48] _ = x[actKillWord-48]
_ = x[actUnixLineDiscard-49] _ = x[actKillSubWord-49]
_ = x[actUnixWordRubout-50] _ = x[actUnixLineDiscard-50]
_ = x[actYank-51] _ = x[actUnixWordRubout-51]
_ = x[actBackwardKillWord-52] _ = x[actYank-52]
_ = x[actBackwardKillSubWord-53] _ = x[actBackwardKillWord-53]
_ = x[actSelectAll-54] _ = x[actBackwardKillSubWord-54]
_ = x[actDeselectAll-55] _ = x[actSelectAll-55]
_ = x[actToggle-56] _ = x[actDeselectAll-56]
_ = x[actToggleSearch-57] _ = x[actToggle-57]
_ = x[actToggleAll-58] _ = x[actToggleSearch-58]
_ = x[actToggleDown-59] _ = x[actToggleAll-59]
_ = x[actToggleUp-60] _ = x[actToggleDown-60]
_ = x[actToggleIn-61] _ = x[actToggleUp-61]
_ = x[actToggleOut-62] _ = x[actToggleIn-62]
_ = x[actToggleTrack-63] _ = x[actToggleOut-63]
_ = x[actToggleTrackCurrent-64] _ = x[actToggleTrack-64]
_ = x[actToggleHeader-65] _ = x[actToggleTrackCurrent-65]
_ = x[actToggleWrap-66] _ = x[actToggleHeader-66]
_ = x[actToggleWrapWord-67] _ = x[actToggleWrap-67]
_ = x[actToggleMultiLine-68] _ = x[actToggleWrapWord-68]
_ = x[actToggleHscroll-69] _ = x[actToggleMultiLine-69]
_ = x[actToggleRaw-70] _ = x[actToggleHscroll-70]
_ = x[actEnableRaw-71] _ = x[actToggleRaw-71]
_ = x[actDisableRaw-72] _ = x[actEnableRaw-72]
_ = x[actTrackCurrent-73] _ = x[actDisableRaw-73]
_ = x[actToggleInput-74] _ = x[actTrackCurrent-74]
_ = x[actHideInput-75] _ = x[actToggleInput-75]
_ = x[actShowInput-76] _ = x[actHideInput-76]
_ = x[actUntrackCurrent-77] _ = x[actShowInput-77]
_ = x[actDown-78] _ = x[actUntrackCurrent-78]
_ = x[actDownMatch-79] _ = x[actDown-79]
_ = x[actUp-80] _ = x[actDownMatch-80]
_ = x[actUpMatch-81] _ = x[actUp-81]
_ = x[actPageUp-82] _ = x[actUpMatch-82]
_ = x[actPageDown-83] _ = x[actPageUp-83]
_ = x[actPosition-84] _ = x[actPageDown-84]
_ = x[actHalfPageUp-85] _ = x[actPosition-85]
_ = x[actHalfPageDown-86] _ = x[actHalfPageUp-86]
_ = x[actOffsetUp-87] _ = x[actHalfPageDown-87]
_ = x[actOffsetDown-88] _ = x[actOffsetUp-88]
_ = x[actOffsetMiddle-89] _ = x[actOffsetDown-89]
_ = x[actJump-90] _ = x[actOffsetMiddle-90]
_ = x[actJumpAccept-91] _ = x[actJump-91]
_ = x[actPrintQuery-92] _ = x[actJumpAccept-92]
_ = x[actRefreshPreview-93] _ = x[actPrintQuery-93]
_ = x[actReplaceQuery-94] _ = x[actRefreshPreview-94]
_ = x[actToggleSort-95] _ = x[actReplaceQuery-95]
_ = x[actShowPreview-96] _ = x[actToggleSort-96]
_ = x[actHidePreview-97] _ = x[actShowPreview-97]
_ = x[actTogglePreview-98] _ = x[actHidePreview-98]
_ = x[actTogglePreviewWrap-99] _ = x[actTogglePreview-99]
_ = x[actTogglePreviewWrapWord-100] _ = x[actTogglePreviewWrap-100]
_ = x[actTransform-101] _ = x[actTogglePreviewWrapWord-101]
_ = x[actTransformBorderLabel-102] _ = x[actTransform-102]
_ = x[actTransformGhost-103] _ = x[actTransformBorderLabel-103]
_ = x[actTransformHeader-104] _ = x[actTransformGhost-104]
_ = x[actTransformFooter-105] _ = x[actTransformHeader-105]
_ = x[actTransformHeaderLabel-106] _ = x[actTransformHeaderLines-106]
_ = x[actTransformFooterLabel-107] _ = x[actTransformFooter-107]
_ = x[actTransformInputLabel-108] _ = x[actTransformHeaderLabel-108]
_ = x[actTransformListLabel-109] _ = x[actTransformFooterLabel-109]
_ = x[actTransformNth-110] _ = x[actTransformInputLabel-110]
_ = x[actTransformPointer-111] _ = x[actTransformListLabel-111]
_ = x[actTransformPreviewLabel-112] _ = x[actTransformNth-112]
_ = x[actTransformPrompt-113] _ = x[actTransformPointer-113]
_ = x[actTransformQuery-114] _ = x[actTransformPreviewLabel-114]
_ = x[actTransformSearch-115] _ = x[actTransformPrompt-115]
_ = x[actTrigger-116] _ = x[actTransformQuery-116]
_ = x[actBgTransform-117] _ = x[actTransformSearch-117]
_ = x[actBgTransformBorderLabel-118] _ = x[actTrigger-118]
_ = x[actBgTransformGhost-119] _ = x[actBgTransform-119]
_ = x[actBgTransformHeader-120] _ = x[actBgTransformBorderLabel-120]
_ = x[actBgTransformFooter-121] _ = x[actBgTransformGhost-121]
_ = x[actBgTransformHeaderLabel-122] _ = x[actBgTransformHeader-122]
_ = x[actBgTransformFooterLabel-123] _ = x[actBgTransformHeaderLines-123]
_ = x[actBgTransformInputLabel-124] _ = x[actBgTransformFooter-124]
_ = x[actBgTransformListLabel-125] _ = x[actBgTransformHeaderLabel-125]
_ = x[actBgTransformNth-126] _ = x[actBgTransformFooterLabel-126]
_ = x[actBgTransformPointer-127] _ = x[actBgTransformInputLabel-127]
_ = x[actBgTransformPreviewLabel-128] _ = x[actBgTransformListLabel-128]
_ = x[actBgTransformPrompt-129] _ = x[actBgTransformNth-129]
_ = x[actBgTransformQuery-130] _ = x[actBgTransformPointer-130]
_ = x[actBgTransformSearch-131] _ = x[actBgTransformPreviewLabel-131]
_ = x[actBgCancel-132] _ = x[actBgTransformPrompt-132]
_ = x[actSearch-133] _ = x[actBgTransformQuery-133]
_ = x[actPreview-134] _ = x[actBgTransformSearch-134]
_ = x[actPreviewTop-135] _ = x[actBgCancel-135]
_ = x[actPreviewBottom-136] _ = x[actSearch-136]
_ = x[actPreviewUp-137] _ = x[actPreview-137]
_ = x[actPreviewDown-138] _ = x[actPreviewTop-138]
_ = x[actPreviewPageUp-139] _ = x[actPreviewBottom-139]
_ = x[actPreviewPageDown-140] _ = x[actPreviewUp-140]
_ = x[actPreviewHalfPageUp-141] _ = x[actPreviewDown-141]
_ = x[actPreviewHalfPageDown-142] _ = x[actPreviewPageUp-142]
_ = x[actPrevHistory-143] _ = x[actPreviewPageDown-143]
_ = x[actPrevSelected-144] _ = x[actPreviewHalfPageUp-144]
_ = x[actPrint-145] _ = x[actPreviewHalfPageDown-145]
_ = x[actPut-146] _ = x[actPrevHistory-146]
_ = x[actNextHistory-147] _ = x[actPrevSelected-147]
_ = x[actNextSelected-148] _ = x[actPrint-148]
_ = x[actExecute-149] _ = x[actPut-149]
_ = x[actExecuteSilent-150] _ = x[actNextHistory-150]
_ = x[actExecuteMulti-151] _ = x[actNextSelected-151]
_ = x[actSigStop-152] _ = x[actExecute-152]
_ = x[actBest-153] _ = x[actExecuteSilent-153]
_ = x[actFirst-154] _ = x[actExecuteMulti-154]
_ = x[actLast-155] _ = x[actSigStop-155]
_ = x[actReload-156] _ = x[actBest-156]
_ = x[actReloadSync-157] _ = x[actFirst-157]
_ = x[actDisableSearch-158] _ = x[actLast-158]
_ = x[actEnableSearch-159] _ = x[actReload-159]
_ = x[actSelect-160] _ = x[actReloadSync-160]
_ = x[actDeselect-161] _ = x[actDisableSearch-161]
_ = x[actUnbind-162] _ = x[actEnableSearch-162]
_ = x[actRebind-163] _ = x[actSelect-163]
_ = x[actToggleBind-164] _ = x[actDeselect-164]
_ = x[actBecome-165] _ = x[actUnbind-165]
_ = x[actShowHeader-166] _ = x[actRebind-166]
_ = x[actHideHeader-167] _ = x[actToggleBind-167]
_ = x[actBell-168] _ = x[actBecome-168]
_ = x[actExclude-169] _ = x[actShowHeader-169]
_ = x[actExcludeMulti-170] _ = x[actHideHeader-170]
_ = x[actAsync-171] _ = x[actBell-171]
_ = x[actExclude-172]
_ = x[actExcludeMulti-173]
_ = x[actAsync-174]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
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, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 992, 1010, 1026, 1038, 1050, 1063, 1078, 1092, 1104, 1116, 1133, 1140, 1152, 1157, 1167, 1176, 1187, 1198, 1211, 1226, 1237, 1250, 1265, 1272, 1285, 1298, 1315, 1330, 1343, 1357, 1371, 1387, 1407, 1431, 1443, 1466, 1483, 1501, 1519, 1542, 1565, 1587, 1608, 1623, 1642, 1666, 1684, 1701, 1719, 1729, 1743, 1768, 1787, 1807, 1827, 1852, 1877, 1901, 1924, 1941, 1962, 1988, 2008, 2027, 2047, 2058, 2067, 2077, 2090, 2106, 2118, 2132, 2148, 2166, 2186, 2208, 2222, 2237, 2245, 2251, 2265, 2280, 2290, 2306, 2321, 2331, 2338, 2346, 2353, 2362, 2375, 2391, 2406, 2415, 2426, 2435, 2444, 2457, 2466, 2479, 2492, 2499, 2509, 2524, 2532} 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}
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

@@ -52,6 +52,20 @@ func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1] return cl.chunks[len(cl.chunks)-1]
} }
// GetItems returns the first n items from the given chunks
func GetItems(chunks []*Chunk, n int) []Item {
items := make([]Item, 0, n)
for _, chunk := range chunks {
for i := 0; i < chunk.count && len(items) < n; i++ {
items = append(items, chunk.items[i])
}
if len(items) >= n {
break
}
}
return items
}
// CountItems returns the total number of Items // CountItems returns the total number of Items
func CountItems(cs []*Chunk) int { func CountItems(cs []*Chunk) int {
if len(cs) == 0 { if len(cs) == 0 {

View File

@@ -65,7 +65,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader
EvtReady EvtReady
EvtQuit EvtQuit
) )

View File

@@ -17,7 +17,6 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/ */
type revision struct { type revision struct {
@@ -113,14 +112,8 @@ func Run(opts *Options) (int, error) {
cache := NewChunkCache() cache := NewChunkCache()
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32 var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
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 {
if len(header) < opts.HeaderLines {
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(data) item.text, item.colors = ansiProcessor(data)
item.text.Index = itemIndex item.text.Index = itemIndex
itemIndex++ itemIndex++
@@ -147,11 +140,6 @@ func Run(opts *Options) (int, error) {
} }
} }
transformed := nthTransformer(tokens, itemIndex) transformed := nthTransformer(tokens, itemIndex)
if len(header) < opts.HeaderLines {
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(stringBytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
// We should not trim trailing whitespaces with background colors // We should not trim trailing whitespaces with background colors
@@ -236,13 +224,15 @@ func Run(opts *Options) (int, error) {
denylist = make(map[int32]struct{}) denylist = make(map[int32]struct{})
denyMutex.Unlock() denyMutex.Unlock()
} }
headerLines := int32(opts.HeaderLines)
headerUpdated := false
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock() denyMutex.Lock()
denylistCopy := maps.Clone(denylist) denylistCopy := maps.Clone(denylist)
denyMutex.Unlock() denyMutex.Unlock()
return BuildPattern(cache, patternCache, return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy) opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
} }
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
@@ -265,6 +255,9 @@ func Run(opts *Options) (int, error) {
func(runes []byte) bool { func(runes []byte) bool {
item := Item{} item := Item{}
if chunkList.trans(&item, runes) { if chunkList.trans(&item, runes) {
if item.Index() < headerLines {
return false
}
mutex.Lock() mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(transformer(&item)) opts.Printer(transformer(&item))
@@ -349,11 +342,11 @@ func Run(opts *Options) (int, error) {
clearDenylist() clearDenylist()
} }
reading = true reading = true
headerUpdated = false
startTick = ticks startTick = ticks
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision.bumpMajor() inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
readyChan := make(chan bool) readyChan := make(chan bool)
go reader.restart(command, environ, readyChan) go reader.restart(command, environ, readyChan)
<-readyChan <-readyChan
@@ -411,7 +404,11 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
if headerLines > 0 && !headerUpdated {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
headerUpdated = int32(total) >= headerLines
}
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
} }
@@ -421,6 +418,7 @@ func Run(opts *Options) (int, error) {
var command *commandSpec var command *commandSpec
var environ []string var environ []string
var changed bool var changed bool
headerLinesChanged := false
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
@@ -441,6 +439,12 @@ func Run(opts *Options) (int, error) {
nth = *val.nth nth = *val.nth
bump = true bump = true
} }
if val.headerLines != nil {
headerLines = int32(*val.headerLines)
headerUpdated = false
headerLinesChanged = true
bump = true
}
if bump { if bump {
patternCache = make(map[string]*Pattern) patternCache = make(map[string]*Pattern)
cache.Clear() cache.Clear()
@@ -477,6 +481,14 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
} }
if headerLinesChanged {
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
if headerLines > 0 {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
} else {
terminal.UpdateHeader(nil)
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision) matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false
@@ -486,11 +498,6 @@ func Run(opts *Options) (int, error) {
terminal.UpdateProgress(val) terminal.UpdateProgress(val)
} }
case EvtHeader:
headerPadded := make([]string, opts.HeaderLines)
copy(headerPadded, value.([]string))
terminal.UpdateHeader(headerPadded)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case MatchResult: case MatchResult:

View File

@@ -174,7 +174,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
return MatchResult{m, m, false} return MatchResult{m, m, false}
} }
pattern := request.pattern pattern := request.pattern
passMerger := PassMerger(&request.chunks, m.tac, request.revision) passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
if pattern.IsEmpty() { if pattern.IsEmpty() {
return MatchResult{passMerger, passMerger, false} return MatchResult{passMerger, passMerger, false}
} }

View File

@@ -10,42 +10,46 @@ func EmptyMerger(revision revision) *Merger {
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
type Merger struct { type Merger struct {
pattern *Pattern pattern *Pattern
lists [][]Result lists [][]Result
merged []Result merged []Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
tac bool tac bool
final bool final bool
count int count int
pass bool pass bool
revision revision startIndex int
minIndex int32 revision revision
maxIndex int32 minIndex int32
maxIndex int32
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order. startIndex items are skipped from the beginning.
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
var minIndex, maxIndex int32 var minIndex, maxIndex int32
if len(*chunks) > 0 { if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index() minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex) maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
} }
si := int(startIndex)
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0, count: 0,
pass: true, pass: true,
revision: revision, startIndex: si,
minIndex: minIndex, revision: revision,
maxIndex: maxIndex} minIndex: minIndex + startIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
} }
mg.count = max(0, mg.count-si)
return &mg return &mg
} }
@@ -113,6 +117,7 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
idx += mg.startIndex
firstChunk := (*mg.chunks)[0] firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count { if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count idx -= firstChunk.count

View File

@@ -1626,7 +1626,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|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|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-]+")
} }
@@ -2037,6 +2037,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-header": case "change-header":
return actChangeHeader return actChangeHeader
case "change-header-lines":
return actChangeHeaderLines
case "change-footer": case "change-footer":
return actChangeFooter return actChangeFooter
case "change-list-label": case "change-list-label":
@@ -2097,6 +2099,8 @@ func isExecuteAction(str string) actionType {
return actTransformFooter return actTransformFooter
case "transform-header": case "transform-header":
return actTransformHeader return actTransformHeader
case "transform-header-lines":
return actTransformHeaderLines
case "transform-ghost": case "transform-ghost":
return actTransformGhost return actTransformGhost
case "transform-nth": case "transform-nth":
@@ -2127,6 +2131,8 @@ func isExecuteAction(str string) actionType {
return actBgTransformFooter return actBgTransformFooter
case "bg-transform-header": case "bg-transform-header":
return actBgTransformHeader return actBgTransformHeader
case "bg-transform-header-lines":
return actBgTransformHeaderLines
case "bg-transform-ghost": case "bg-transform-ghost":
return actBgTransformGhost return actBgTransformGhost
case "bg-transform-nth": case "bg-transform-nth":

View File

@@ -64,6 +64,7 @@ type Pattern struct {
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{} denylist map[int32]struct{}
startIndex int32
} }
var _splitRegex *regexp.Regexp var _splitRegex *regexp.Regexp
@@ -74,7 +75,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
var asString string var asString string
if extended { if extended {
@@ -146,6 +147,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
delimiter: delimiter, delimiter: delimiter,
cache: cache, cache: cache,
denylist: denylist, denylist: denylist,
startIndex: startIndex,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@@ -301,10 +303,19 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{} matches := []Result{}
// Skip header items in chunks that contain them
startIdx := 0
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
startIdx = int(p.startIndex - chunk.items[0].Index())
if startIdx >= chunk.count {
return matches
}
}
if len(p.denylist) == 0 { if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups // Huge code duplication for minimizing unnecessary map lookups
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := startIdx; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
@@ -320,7 +331,7 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
} }
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := startIdx; idx < chunk.count; idx++ {
if _, prs := p.denylist[chunk.items[idx].Index()]; prs { if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
continue continue
} }

View File

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

View File

@@ -314,6 +314,7 @@ type Terminal struct {
sort bool sort bool
toggleSort bool toggleSort bool
track trackOption track trackOption
targetIndex int32
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]*action keymap map[tui.Event][]*action
@@ -327,7 +328,7 @@ type Terminal struct {
headerVisible bool headerVisible bool
headerFirst bool headerFirst bool
headerLines int headerLines int
header []string header []Item
header0 []string header0 []string
footer []string footer []string
ellipsis string ellipsis string
@@ -542,6 +543,7 @@ const (
actChangeBorderLabel actChangeBorderLabel
actChangeGhost actChangeGhost
actChangeHeader actChangeHeader
actChangeHeaderLines
actChangeFooter actChangeFooter
actChangeHeaderLabel actChangeHeaderLabel
actChangeFooterLabel actChangeFooterLabel
@@ -627,6 +629,7 @@ const (
actTransformBorderLabel actTransformBorderLabel
actTransformGhost actTransformGhost
actTransformHeader actTransformHeader
actTransformHeaderLines
actTransformFooter actTransformFooter
actTransformHeaderLabel actTransformHeaderLabel
actTransformFooterLabel actTransformFooterLabel
@@ -645,6 +648,7 @@ const (
actBgTransformBorderLabel actBgTransformBorderLabel
actBgTransformGhost actBgTransformGhost
actBgTransformHeader actBgTransformHeader
actBgTransformHeaderLines
actBgTransformFooter actBgTransformFooter
actBgTransformHeaderLabel actBgTransformHeaderLabel
actBgTransformFooterLabel actBgTransformFooterLabel
@@ -710,6 +714,7 @@ func processExecution(action actionType) bool {
actTransformBorderLabel, actTransformBorderLabel,
actTransformGhost, actTransformGhost,
actTransformHeader, actTransformHeader,
actTransformHeaderLines,
actTransformFooter, actTransformFooter,
actTransformHeaderLabel, actTransformHeaderLabel,
actTransformFooterLabel, actTransformFooterLabel,
@@ -725,6 +730,7 @@ func processExecution(action actionType) bool {
actBgTransformBorderLabel, actBgTransformBorderLabel,
actBgTransformGhost, actBgTransformGhost,
actBgTransformHeader, actBgTransformHeader,
actBgTransformHeaderLines,
actBgTransformFooter, actBgTransformFooter,
actBgTransformHeaderLabel, actBgTransformHeaderLabel,
actBgTransformFooterLabel, actBgTransformFooterLabel,
@@ -761,14 +767,15 @@ type placeholderFlags struct {
} }
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool sync bool
nth *[]Range nth *[]Range
command *commandSpec headerLines *int
environ []string command *commandSpec
changed bool environ []string
denylist []int32 changed bool
revision revision denylist []int32
revision revision
} }
type previewRequest struct { type previewRequest struct {
@@ -1022,6 +1029,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
track: opts.Track, track: opts.Track,
targetIndex: minItem.Index(),
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
@@ -1063,7 +1071,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
gap: opts.Gap, gap: opts.Gap,
header: []string{}, header: []Item{},
footer: opts.Footer, footer: opts.Footer,
header0: opts.Header, header0: opts.Header,
ansi: opts.Ansi, ansi: opts.Ansi,
@@ -1364,7 +1372,7 @@ func (t *Terminal) environImpl(forPreview bool) []string {
} }
} }
env = append(env, "FZF_INPUT_STATE="+inputState) env = append(env, "FZF_INPUT_STATE="+inputState)
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count)) env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", max(0, t.count-t.headerLines)))
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length())) env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length()))
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected))) env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines)) env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
@@ -1755,8 +1763,14 @@ func (t *Terminal) changeFooter(footer string) {
} }
// UpdateHeader updates the header // UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) { func (t *Terminal) UpdateHeader(header []Item) {
t.mutex.Lock() t.mutex.Lock()
// Pad to t.headerLines so that click coordinate mapping works correctly
if len(header) < t.headerLines {
padded := make([]Item, t.headerLines)
copy(padded, header)
header = padded
}
t.header = header t.header = header
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil) t.reqBox.Set(reqHeader, nil)
@@ -1788,6 +1802,10 @@ func (t *Terminal) UpdateList(result MatchResult) {
prevIndex = merger.First().item.Index() prevIndex = merger.First().item.Index()
} }
} }
if t.targetIndex != minItem.Index() {
prevIndex = t.targetIndex
t.targetIndex = minItem.Index()
}
t.progress = 100 t.progress = 100
t.merger = merger t.merger = merger
t.resultMerger = merger t.resultMerger = merger
@@ -3079,11 +3097,11 @@ func (t *Terminal) printHeader() {
} }
t.withWindow(t.headerWindow, func() { t.withWindow(t.headerWindow, func() {
var lines []string var headerItems []Item
if !t.hasHeaderLinesWindow() { if !t.hasHeaderLinesWindow() {
lines = t.header headerItems = t.header
} }
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, lines) t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
}) })
if w, shape := t.determineHeaderLinesShape(); w { if w, shape := t.determineHeaderLinesShape(); w {
t.withWindow(t.headerLinesWindow, func() { t.withWindow(t.headerLinesWindow, func() {
@@ -3145,7 +3163,7 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
return indentSize return indentSize
} }
func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []string) { func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) {
max := t.window.Height() max := t.window.Height()
if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst { if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst {
max-- max--
@@ -3172,7 +3190,8 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
} }
indent := strings.Repeat(" ", indentSize) indent := strings.Repeat(" ", indentSize)
t.wrap = false t.wrap = false
for idx, lineStr := range append(append([]string{}, lines1...), lines2...) { totalLines := len(lines1) + len(lines2)
for idx := 0; idx < totalLines; idx++ {
line := idx line := idx
if needReverse && idx < len(lines1) { if needReverse && idx < len(lines1) {
line = len(lines1) - idx - 1 line = len(lines1) - idx - 1
@@ -3186,11 +3205,18 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
if line >= max { if line >= max {
continue continue
} }
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState var item *Item
item := &Item{ if idx < len(lines1) {
text: util.ToChars([]byte(trimmed)), trimmed, colors, newState := extractColor(lines1[idx], state, nil)
colors: colors} state = newState
item = &Item{
text: util.ToChars([]byte(trimmed)),
colors: colors}
} else {
headerItem := lines2[idx-len(lines1)]
item = &headerItem
}
t.printHighlighted(Result{item: item}, t.printHighlighted(Result{item: item},
tui.ColHeader, tui.ColHeader, false, false, false, line, line, true, tui.ColHeader, tui.ColHeader, false, false, false, line, line, true,
@@ -5288,9 +5314,13 @@ func (t *Terminal) addClickHeaderWord(env []string) []string {
return env return env
} }
// NOTE: t.header is padded with empty strings so that its size is equal to t.headerLines
nthBase := 0 nthBase := 0
headers := [2][]string{t.header, t.header0} // Convert header items to strings for click handling
headerStrs := make([]string, len(t.header))
for i, item := range t.header {
headerStrs[i] = item.text.ToString()
}
headers := [2][]string{headerStrs, t.header0}
if t.layout == layoutReverse { if t.layout == layoutReverse {
headers[0], headers[1] = headers[1], headers[0] headers[0], headers[1] = headers[1], headers[0]
} }
@@ -5892,6 +5922,7 @@ func (t *Terminal) Loop() error {
events := []util.EventType{} events := []util.EventType{}
changed := false changed := false
var newNth *[]Range var newNth *[]Range
var newHeaderLines *int
req := func(evts ...util.EventType) { req := func(evts ...util.EventType) {
for _, event := range evts { for _, event := range evts {
events = append(events, event) events = append(events, event)
@@ -5908,6 +5939,7 @@ func (t *Terminal) Loop() error {
events = []util.EventType{} events = []util.EventType{}
changed = false changed = false
newNth = nil newNth = nil
newHeaderLines = nil
beof := false beof := false
queryChanged := false queryChanged := false
denylist := []int32{} denylist := []int32{}
@@ -6247,6 +6279,23 @@ func (t *Terminal) Loop() error {
} }
case actPrintQuery: case actPrintQuery:
req(reqPrintQuery) req(reqPrintQuery)
case actChangeHeaderLines, actTransformHeaderLines, actBgTransformHeaderLines:
capture(true, func(expr string) {
if n, err := strconv.Atoi(expr); err == nil && n >= 0 && n != t.headerLines {
t.headerLines = n
newHeaderLines = &n
changed = true
// Deselect items that are now part of the header
for idx := range t.selected {
if idx < int32(n) {
delete(t.selected, idx)
}
}
// Tell UpdateList to reposition cursor to the current item
t.targetIndex = t.currentIndex()
req(reqList, reqPrompt, reqInfo, reqHeader)
}
})
case actChangeMulti: case actChangeMulti:
multi := t.multi multi := t.multi
if a.a == "" { if a.a == "" {
@@ -7428,7 +7477,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil reload := changed || newCommand != nil
var reloadRequest *searchRequest var reloadRequest *searchRequest
if reload { if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, 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

@@ -2176,6 +2176,80 @@ class TestCore < TestInteractive
end end
end end
def test_change_header_lines
tmux.send_keys %(seq 10 | #{FZF} --header-lines 3 --bind 'space:change-header-lines(5),enter:transform-header-lines(echo 1)'), :Enter
tmux.until do |lines|
assert_equal 7, lines.item_count
assert lines.any_include?('> 4')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 5, lines.item_count
assert lines.any_include?('> 6')
end
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal 9, lines.item_count
assert lines.any_include?('> 6')
end
end
def test_change_header_lines_to_zero
tmux.send_keys %(seq 5 | #{FZF} --header-lines 3 --bind 'space:bg-transform-header-lines(echo 0)'), :Enter
tmux.until do |lines|
assert_equal 2, lines.item_count
assert lines.any_include?('> 4')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 5, lines.item_count
# All items are now in the list, cursor stays on item 4
assert lines.any_include?('> 4')
end
end
def test_change_header_lines_deselect
# Selected items that become part of the header should be deselected
tmux.send_keys %(seq 10 | #{FZF} --multi --header-lines 0 --bind 'space:change-header-lines(3),enter:change-header-lines(1)'), :Enter
tmux.until do |lines|
assert_equal 10, lines.item_count
assert lines.any_include?('> 1')
end
# Select items 1, 2, 3 (these will become header lines)
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal 3, lines.select_count }
# Also select item 4 (this should remain selected)
tmux.send_keys :BTab
tmux.until { |lines| assert_equal 4, lines.select_count }
# Change header-lines to 3: items 1, 2, 3 become headers and should be deselected
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 7, lines.item_count
assert_equal 1, lines.select_count
assert lines.any_include?('> 5')
end
# Change header-lines to 1
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal 9, lines.item_count
assert_equal 1, lines.select_count
assert lines.any_include?('> 5')
end
end
def test_change_header_lines_reverse
tmux.send_keys %(seq 10 | #{FZF} --header-lines 2 --reverse --bind 'space:change-header-lines(4)'), :Enter
tmux.until do |lines|
assert_equal 8, lines.item_count
assert lines.any_include?('> 3')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 6, lines.item_count
assert lines.any_include?('> 5')
end
end
def test_zero_width_characters def test_zero_width_characters
tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter
tmux.until do |lines| tmux.until do |lines|

View File

@@ -326,4 +326,27 @@ class TestFilter < TestBase
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing']) writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
end end
def test_header_lines_filter
assert_equal %w[4 5 6 7 8 9 10],
`seq 10 | #{FZF} --header-lines 3 -f ""`.lines(chomp: true)
assert_equal %w[5],
`seq 10 | #{FZF} --header-lines 3 -f 5`.lines(chomp: true)
# Header items should not be matched
assert_empty `seq 10 | #{FZF} --header-lines 3 -f "^1$"`.lines(chomp: true)
end
def test_header_lines_filter_with_nth
writelines(%w[a:1 b:2 c:3 d:4 e:5])
assert_equal %w[c:3 d:4 e:5],
`#{FZF} --header-lines 2 -d: --with-nth 2 -f "" < #{tempname}`.lines(chomp: true)
assert_equal %w[d:4],
`#{FZF} --header-lines 2 -d: --with-nth 2 -f 4 < #{tempname}`.lines(chomp: true)
end
def test_header_lines_all_headers
# When all lines are header lines, no results
assert_empty `seq 3 | #{FZF} --header-lines 10 -f ""`.chomp
assert_equal 1, $CHILD_STATUS.exitstatus
end
end end