From a5beb08ed72215e8b015851ad70bf3beca526429 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 5 Jan 2025 23:02:52 +0900 Subject: [PATCH] Border around the header section Close #4159 --- CHANGELOG.md | 15 ++- man/man1/fzf.1 | 51 +++++++- src/actiontype_string.go | 204 +++++++++++++++---------------- src/options.go | 245 ++++++++++++++++++++++--------------- src/terminal.go | 254 +++++++++++++++++++++++++++++---------- src/tui/light.go | 20 ++- src/tui/tcell.go | 15 ++- src/tui/tui.go | 38 +++++- test/test_go.rb | 207 ++++++++++++++++++++++++++++--- 9 files changed, 757 insertions(+), 292 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 481f3492882..e18790b8f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ CHANGELOG - Actions - `change-list-label` - `transform-list-label` -- Border and label for the input section (prompt and info) +- Border and label for the input section (prompt line and info line) - Options - `--input-border[=STYLE]` - `--input-label=LABEL` @@ -29,6 +29,19 @@ CHANGELOG - Actions - `change-input-label` - `transform-input-label` +- Border and label for the header section + - Options + - `--header-border[=STYLE]` + - `--header-label=LABEL` + - `--header-label-pos=COL[:bottom]` + - Colors + - `header-fg` (`header`) + - `header-bg` + - `header-border` + - `header-label` + - Actions + - `change-header-label` + - `transform-header-label` - Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]` - You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup. ```sh diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8d499697c98..16421c7b9be 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -308,7 +308,7 @@ Choose the layout (default: default) A synonym for \fB\-\-layout=reverse\fB .TP -.BI "\-\-border" [=BORDER_OPT] +.BI "\-\-border" [=STYLE] Draw border around the finder .br @@ -387,6 +387,42 @@ the label. Label is printed on the top border line by default, add \fB:bottom\fR to put it on the border line on the bottom. The default value \fB0 (or \fBcenter\fR) will put the label at the center of the border line. +.TP +.BI "\-\-list\-border" [=STYLE] +Draw border around the list section + +.TP +.BI "\-\-list\-label" [=LABEL] +Label to print on the list border + +.TP +.BI "\-\-list\-label\-pos" [=N[:top|bottom]] +Position of the list label + +.TP +.BI "\-\-input\-border" [=STYLE] +Draw border around the input section + +.TP +.BI "\-\-input\-label" [=LABEL] +Label to print on the input border + +.TP +.BI "\-\-input\-label\-pos" [=N[:top|bottom]] +Position of the input label + +.TP +.BI "\-\-header\-border" [=STYLE] +Draw border around the header section + +.TP +.BI "\-\-header\-label" [=LABEL] +Label to print on the header border + +.TP +.BI "\-\-header\-label\-pos" [=N[:top|bottom]] +Position of the header label + .TP .B "\-\-no\-unicode" Use ASCII characters instead of Unicode drawing characters to draw borders, @@ -563,7 +599,8 @@ color mappings. \fBlist\-bg \fRList section background \fBselected\-bg \fRSelected line background \fBpreview\-bg \fRPreview window background - \fBinput\-bg \fRInput section background + \fBinput\-bg \fRInput window background (\fB\-\-input\-border\fR) + \fBheader\-bg \fRHeader window background (\fB\-\-header\-border\fR) \fBhl \fRHighlighted substrings \fBselected\-hl \fRHighlighted substrings in the selected line \fBcurrent\-fg (fg+) \fRText (current line) @@ -579,16 +616,18 @@ color mappings. \fBseparator \fRHorizontal separator on info line \fBpreview\-border \fRBorder around the preview window (\fB\-\-preview\fR) \fBpreview\-scrollbar \fRScrollbar - \fBinput\-border \fRBorder around the input section (\fB\-\-input\-border\fR) + \fBinput\-border \fRBorder around the input window (\fB\-\-input\-border\fR) + \fBheader\-border \fRBorder around the header window (\fB\-\-header\-border\fR) \fBlabel \fRBorder label (\fB\-\-border\-label\fR, \fB\-\-list\-label\fR, \fB\-\-input\-label\fR, and \fB\-\-preview\-label\fR) \fBlist\-label \fRBorder label of the list section (\fB\-\-list\-label\fR) \fBpreview\-label \fRBorder label of the preview window (\fB\-\-preview\-label\fR) - \fBinput\-label \fRBorder label of the input section (\fB\-\-input\-label\fR) + \fBinput\-label \fRBorder label of the input window (\fB\-\-input\-label\fR) + \fBheader\-label \fRBorder label of the header window (\fB\-\-header\-label\fR) \fBprompt \fRPrompt \fBpointer \fRPointer to the current line \fBmarker \fRMulti\-select marker \fBspinner \fRStreaming input indicator - \fBheader \fRHeader + \fBheader (header\-fg) \fRHeader .B ANSI COLORS: \fB\-1 \fRDefault terminal foreground/background color @@ -1454,6 +1493,7 @@ A key or an event can be bound to one or more of the following actions. \fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string) \fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR) + \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\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string) \fBchange\-multi\fR (enable multi-select mode with no limit) @@ -1540,6 +1580,7 @@ A key or an event can be bound to one or more of the following actions. \fBtransform(...)\fR (transform states using the output of an external command) \fBtransform\-border\-label(...)\fR (transform border label using an external command) \fBtransform\-header(...)\fR (transform header 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\-list\-label(...)\fR (transform list label using an external command) \fBtransform\-preview\-label(...)\fR (transform preview label using an external command) diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 2996ce72e2a..12162080306 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -28,110 +28,112 @@ func _() { _ = x[actChangeListLabel-17] _ = x[actChangeInputLabel-18] _ = x[actChangeHeader-19] - _ = x[actChangeMulti-20] - _ = x[actChangePreviewLabel-21] - _ = x[actChangePrompt-22] - _ = x[actChangeQuery-23] - _ = x[actClearScreen-24] - _ = x[actClearQuery-25] - _ = x[actClearSelection-26] - _ = x[actClose-27] - _ = x[actDeleteChar-28] - _ = x[actDeleteCharEof-29] - _ = x[actEndOfLine-30] - _ = x[actFatal-31] - _ = x[actForwardChar-32] - _ = x[actForwardWord-33] - _ = x[actKillLine-34] - _ = x[actKillWord-35] - _ = x[actUnixLineDiscard-36] - _ = x[actUnixWordRubout-37] - _ = x[actYank-38] - _ = x[actBackwardKillWord-39] - _ = x[actSelectAll-40] - _ = x[actDeselectAll-41] - _ = x[actToggle-42] - _ = x[actToggleSearch-43] - _ = x[actToggleAll-44] - _ = x[actToggleDown-45] - _ = x[actToggleUp-46] - _ = x[actToggleIn-47] - _ = x[actToggleOut-48] - _ = x[actToggleTrack-49] - _ = x[actToggleTrackCurrent-50] - _ = x[actToggleHeader-51] - _ = x[actToggleWrap-52] - _ = x[actToggleMultiLine-53] - _ = x[actToggleHscroll-54] - _ = x[actTrackCurrent-55] - _ = x[actUntrackCurrent-56] - _ = x[actDown-57] - _ = x[actUp-58] - _ = x[actPageUp-59] - _ = x[actPageDown-60] - _ = x[actPosition-61] - _ = x[actHalfPageUp-62] - _ = x[actHalfPageDown-63] - _ = x[actOffsetUp-64] - _ = x[actOffsetDown-65] - _ = x[actOffsetMiddle-66] - _ = x[actJump-67] - _ = x[actJumpAccept-68] - _ = x[actPrintQuery-69] - _ = x[actRefreshPreview-70] - _ = x[actReplaceQuery-71] - _ = x[actToggleSort-72] - _ = x[actShowPreview-73] - _ = x[actHidePreview-74] - _ = x[actTogglePreview-75] - _ = x[actTogglePreviewWrap-76] - _ = x[actTransform-77] - _ = x[actTransformBorderLabel-78] - _ = x[actTransformListLabel-79] - _ = x[actTransformInputLabel-80] - _ = x[actTransformHeader-81] - _ = x[actTransformPreviewLabel-82] - _ = x[actTransformPrompt-83] - _ = x[actTransformQuery-84] - _ = x[actPreview-85] - _ = x[actChangePreview-86] - _ = x[actChangePreviewWindow-87] - _ = x[actPreviewTop-88] - _ = x[actPreviewBottom-89] - _ = x[actPreviewUp-90] - _ = x[actPreviewDown-91] - _ = x[actPreviewPageUp-92] - _ = x[actPreviewPageDown-93] - _ = x[actPreviewHalfPageUp-94] - _ = x[actPreviewHalfPageDown-95] - _ = x[actPrevHistory-96] - _ = x[actPrevSelected-97] - _ = x[actPrint-98] - _ = x[actPut-99] - _ = x[actNextHistory-100] - _ = x[actNextSelected-101] - _ = x[actExecute-102] - _ = x[actExecuteSilent-103] - _ = x[actExecuteMulti-104] - _ = x[actSigStop-105] - _ = x[actFirst-106] - _ = x[actLast-107] - _ = x[actReload-108] - _ = x[actReloadSync-109] - _ = x[actDisableSearch-110] - _ = x[actEnableSearch-111] - _ = x[actSelect-112] - _ = x[actDeselect-113] - _ = x[actUnbind-114] - _ = x[actRebind-115] - _ = x[actBecome-116] - _ = x[actShowHeader-117] - _ = x[actHideHeader-118] + _ = x[actChangeHeaderLabel-20] + _ = x[actChangeMulti-21] + _ = x[actChangePreviewLabel-22] + _ = x[actChangePrompt-23] + _ = x[actChangeQuery-24] + _ = x[actClearScreen-25] + _ = x[actClearQuery-26] + _ = x[actClearSelection-27] + _ = x[actClose-28] + _ = x[actDeleteChar-29] + _ = x[actDeleteCharEof-30] + _ = x[actEndOfLine-31] + _ = x[actFatal-32] + _ = x[actForwardChar-33] + _ = x[actForwardWord-34] + _ = x[actKillLine-35] + _ = x[actKillWord-36] + _ = x[actUnixLineDiscard-37] + _ = x[actUnixWordRubout-38] + _ = x[actYank-39] + _ = x[actBackwardKillWord-40] + _ = x[actSelectAll-41] + _ = x[actDeselectAll-42] + _ = x[actToggle-43] + _ = x[actToggleSearch-44] + _ = x[actToggleAll-45] + _ = x[actToggleDown-46] + _ = x[actToggleUp-47] + _ = x[actToggleIn-48] + _ = x[actToggleOut-49] + _ = x[actToggleTrack-50] + _ = x[actToggleTrackCurrent-51] + _ = x[actToggleHeader-52] + _ = x[actToggleWrap-53] + _ = x[actToggleMultiLine-54] + _ = x[actToggleHscroll-55] + _ = x[actTrackCurrent-56] + _ = x[actUntrackCurrent-57] + _ = x[actDown-58] + _ = x[actUp-59] + _ = x[actPageUp-60] + _ = x[actPageDown-61] + _ = x[actPosition-62] + _ = x[actHalfPageUp-63] + _ = x[actHalfPageDown-64] + _ = x[actOffsetUp-65] + _ = x[actOffsetDown-66] + _ = x[actOffsetMiddle-67] + _ = x[actJump-68] + _ = x[actJumpAccept-69] + _ = x[actPrintQuery-70] + _ = x[actRefreshPreview-71] + _ = x[actReplaceQuery-72] + _ = x[actToggleSort-73] + _ = x[actShowPreview-74] + _ = x[actHidePreview-75] + _ = x[actTogglePreview-76] + _ = x[actTogglePreviewWrap-77] + _ = x[actTransform-78] + _ = x[actTransformBorderLabel-79] + _ = x[actTransformListLabel-80] + _ = x[actTransformInputLabel-81] + _ = x[actTransformHeader-82] + _ = x[actTransformHeaderLabel-83] + _ = x[actTransformPreviewLabel-84] + _ = x[actTransformPrompt-85] + _ = x[actTransformQuery-86] + _ = x[actPreview-87] + _ = x[actChangePreview-88] + _ = x[actChangePreviewWindow-89] + _ = x[actPreviewTop-90] + _ = x[actPreviewBottom-91] + _ = x[actPreviewUp-92] + _ = x[actPreviewDown-93] + _ = x[actPreviewPageUp-94] + _ = x[actPreviewPageDown-95] + _ = x[actPreviewHalfPageUp-96] + _ = x[actPreviewHalfPageDown-97] + _ = x[actPrevHistory-98] + _ = x[actPrevSelected-99] + _ = x[actPrint-100] + _ = x[actPut-101] + _ = x[actNextHistory-102] + _ = x[actNextSelected-103] + _ = x[actExecute-104] + _ = x[actExecuteSilent-105] + _ = x[actExecuteMulti-106] + _ = x[actSigStop-107] + _ = x[actFirst-108] + _ = x[actLast-109] + _ = x[actReload-110] + _ = x[actReloadSync-111] + _ = x[actDisableSearch-112] + _ = x[actEnableSearch-113] + _ = x[actSelect-114] + _ = x[actDeselect-115] + _ = x[actUnbind-116] + _ = x[actRebind-117] + _ = x[actBecome-118] + _ = x[actShowHeader-119] + _ = x[actHideHeader-120] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" +const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader" -var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 293, 314, 329, 343, 357, 370, 387, 395, 408, 424, 436, 444, 458, 472, 483, 494, 512, 529, 536, 555, 567, 581, 590, 605, 617, 630, 641, 652, 664, 678, 699, 714, 727, 745, 761, 776, 793, 800, 805, 814, 825, 836, 849, 864, 875, 888, 903, 910, 923, 936, 953, 968, 981, 995, 1009, 1025, 1045, 1057, 1080, 1101, 1123, 1141, 1165, 1183, 1200, 1210, 1226, 1248, 1261, 1277, 1289, 1303, 1319, 1337, 1357, 1379, 1393, 1408, 1416, 1422, 1436, 1451, 1461, 1477, 1492, 1502, 1510, 1517, 1526, 1539, 1555, 1570, 1579, 1590, 1599, 1608, 1617, 1630, 1643} +var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 377, 390, 407, 415, 428, 444, 456, 464, 478, 492, 503, 514, 532, 549, 556, 575, 587, 601, 610, 625, 637, 650, 661, 672, 684, 698, 719, 734, 747, 765, 781, 796, 813, 820, 825, 834, 845, 856, 869, 884, 895, 908, 923, 930, 943, 956, 973, 988, 1001, 1015, 1029, 1045, 1065, 1077, 1100, 1121, 1143, 1161, 1184, 1208, 1226, 1243, 1253, 1269, 1291, 1304, 1320, 1332, 1346, 1362, 1380, 1400, 1422, 1436, 1451, 1459, 1465, 1479, 1494, 1504, 1520, 1535, 1545, 1553, 1560, 1569, 1582, 1598, 1613, 1622, 1633, 1642, 1651, 1660, 1673, 1686} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/options.go b/src/options.go index b21396a50bb..f6b7245fec0 100644 --- a/src/options.go +++ b/src/options.go @@ -103,6 +103,14 @@ Usage: fzf [options] [POSITIVE_INTEGER: columns from left| NEGATIVE_INTEGER: columns from right][:bottom] (default: 0 or center) + --header-border[=STYLE] Draw border around the header section + [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| + top|bottom|left|right|none] (default: rounded) + --header-label=LABEL Label to print on the header border + --header-label-pos=COL Position of the header label + [POSITIVE_INTEGER: columns from left| + NEGATIVE_INTEGER: columns from right][:bottom] + (default: 0 or center) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --info=STYLE Finder info style @@ -467,104 +475,106 @@ type walkerOpts struct { // Options stores the values of command-line options type Options struct { - Input chan string - Output chan string - NoWinpty bool - Tmux *tmuxOptions - ForceTtyIn bool - ProxyScript string - Bash bool - Zsh bool - Fish bool - Man bool - Fuzzy bool - FuzzyAlgo algo.Algo - Scheme string - Extended bool - Phony bool - Case Case - Normalize bool - Nth []Range - WithNth []Range - Delimiter Delimiter - Sort int - Track trackOption - Tac bool - Tail int - Criteria []criterion - Multi int - Ansi bool - Mouse bool - Theme *tui.ColorTheme - Black bool - Bold bool - Height heightSpec - MinHeight int - Layout layoutType - Cycle bool - Wrap bool - WrapSign *string - MultiLine bool - CursorLine bool - KeepRight bool - Hscroll bool - HscrollOff int - ScrollOff int - FileWord bool - InfoStyle infoStyle - InfoPrefix string - InfoCommand string - Separator *string - JumpLabels string - Prompt string - Pointer *string - Marker *string - MarkerMulti *[3]string - Query string - Select1 bool - Exit0 bool - Filter *string - ToggleSort bool - Expect map[tui.Event]string - Keymap map[tui.Event][]*action - Preview previewOpts - PrintQuery bool - ReadZero bool - Printer func(string) - PrintSep string - Sync bool - History *History - Header []string - HeaderLines int - HeaderFirst bool - Gap int - Ellipsis *string - Scrollbar *string - Margin [4]sizeSpec - Padding [4]sizeSpec - BorderShape tui.BorderShape - ListBorderShape tui.BorderShape - InputBorderShape tui.BorderShape - InputLabel labelOpts - BorderLabel labelOpts - ListLabel labelOpts - PreviewLabel labelOpts - Unicode bool - Ambidouble bool - Tabstop int - WithShell string - ListenAddr *listenAddress - Unsafe bool - ClearOnExit bool - WalkerOpts walkerOpts - WalkerRoot []string - WalkerSkip []string - Version bool - Help bool - CPUProfile string - MEMProfile string - BlockProfile string - MutexProfile string + Input chan string + Output chan string + NoWinpty bool + Tmux *tmuxOptions + ForceTtyIn bool + ProxyScript string + Bash bool + Zsh bool + Fish bool + Man bool + Fuzzy bool + FuzzyAlgo algo.Algo + Scheme string + Extended bool + Phony bool + Case Case + Normalize bool + Nth []Range + WithNth []Range + Delimiter Delimiter + Sort int + Track trackOption + Tac bool + Tail int + Criteria []criterion + Multi int + Ansi bool + Mouse bool + Theme *tui.ColorTheme + Black bool + Bold bool + Height heightSpec + MinHeight int + Layout layoutType + Cycle bool + Wrap bool + WrapSign *string + MultiLine bool + CursorLine bool + KeepRight bool + Hscroll bool + HscrollOff int + ScrollOff int + FileWord bool + InfoStyle infoStyle + InfoPrefix string + InfoCommand string + Separator *string + JumpLabels string + Prompt string + Pointer *string + Marker *string + MarkerMulti *[3]string + Query string + Select1 bool + Exit0 bool + Filter *string + ToggleSort bool + Expect map[tui.Event]string + Keymap map[tui.Event][]*action + Preview previewOpts + PrintQuery bool + ReadZero bool + Printer func(string) + PrintSep string + Sync bool + History *History + Header []string + HeaderLines int + HeaderFirst bool + Gap int + Ellipsis *string + Scrollbar *string + Margin [4]sizeSpec + Padding [4]sizeSpec + BorderShape tui.BorderShape + ListBorderShape tui.BorderShape + InputBorderShape tui.BorderShape + HeaderBorderShape tui.BorderShape + InputLabel labelOpts + HeaderLabel labelOpts + BorderLabel labelOpts + ListLabel labelOpts + PreviewLabel labelOpts + Unicode bool + Ambidouble bool + Tabstop int + WithShell string + ListenAddr *listenAddress + Unsafe bool + ClearOnExit bool + WalkerOpts walkerOpts + WalkerRoot []string + WalkerSkip []string + Version bool + Help bool + CPUProfile string + MEMProfile string + BlockProfile string + MutexProfile string } func filterNonEmpty(input []string) []string { @@ -1251,6 +1261,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.InputBorder) case "input-label": mergeAttr(&theme.InputLabel) + case "header-border": + mergeAttr(&theme.HeaderBorder) + case "header-label": + mergeAttr(&theme.HeaderLabel) case "spinner": mergeAttr(&theme.Spinner) case "info": @@ -1259,8 +1273,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.Cursor) case "marker": mergeAttr(&theme.Marker) - case "header": + case "header", "header-fg": mergeAttr(&theme.Header) + case "header-bg": + mergeAttr(&theme.HeaderBg) default: fail() } @@ -1314,7 +1330,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|list-label|preview-label|input-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1680,6 +1696,8 @@ func isExecuteAction(str string) actionType { return actChangePreviewLabel case "change-input-label": return actChangeInputLabel + case "change-header-label": + return actChangeHeaderLabel case "change-preview-window": return actChangePreviewWindow case "change-preview": @@ -1712,6 +1730,8 @@ func isExecuteAction(str string) actionType { return actTransformPreviewLabel case "transform-input-label": return actTransformInputLabel + case "transform-header-label": + return actTransformHeaderLabel case "transform-header": return actTransformHeader case "transform-prompt": @@ -2542,6 +2562,27 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { if err := parseLabelPosition(&opts.ListLabel, pos); err != nil { return err } + case "--no-header-border": + opts.HeaderBorderShape = tui.BorderNone + case "--header-border": + hasArg, arg := optionalNextString(allArgs, &i) + if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil { + return err + } + case "--no-header-label": + opts.HeaderLabel.label = "" + case "--header-label": + if opts.HeaderLabel.label, err = nextString(allArgs, &i, "header label required"); err != nil { + return err + } + case "--header-label-pos": + pos, err := nextString(allArgs, &i, "header label position required (positive or negative integer or 'center')") + if err != nil { + return err + } + if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil { + return err + } case "--no-input-border": opts.InputBorderShape = tui.BorderNone case "--input-border": @@ -3003,6 +3044,10 @@ func postProcessOptions(opts *Options) error { opts.InputBorderShape = tui.BorderNone } + if opts.HeaderBorderShape == tui.BorderUndefined { + opts.HeaderBorderShape = tui.BorderNone + } + if opts.Pointer == nil { defaultPointer := "▌" if !opts.Unicode { diff --git a/src/terminal.go b/src/terminal.go index 8076462eded..764284a952a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -180,13 +180,13 @@ type itemLine struct { } func (t *Terminal) markEmptyLine(line int) { - if t.window != t.inputWindow { + if t.window != t.inputWindow && t.window != t.headerWindow { t.prevLines[line] = itemLine{valid: true, firstLine: line, empty: true} } } func (t *Terminal) markOtherLine(line int) { - if t.window != t.inputWindow { + if t.window != t.inputWindow && t.window != t.headerWindow { t.prevLines[line] = itemLine{valid: true, firstLine: line, other: true} } } @@ -249,6 +249,9 @@ type Terminal struct { inputLabel labelPrinter inputLabelLen int inputLabelOpts labelOpts + headerLabel labelPrinter + headerLabelLen int + headerLabelOpts labelOpts pointer string pointerLen int pointerEmpty string @@ -307,6 +310,7 @@ type Terminal struct { borderShape tui.BorderShape listBorderShape tui.BorderShape inputBorderShape tui.BorderShape + headerBorderShape tui.BorderShape listLabel labelPrinter listLabelLen int listLabelOpts labelOpts @@ -317,6 +321,8 @@ type Terminal struct { window tui.Window inputWindow tui.Window inputBorder tui.Window + headerWindow tui.Window + headerBorder tui.Window wborder tui.Window pborder tui.Window pwindow tui.Window @@ -406,6 +412,7 @@ const ( reqFullRedraw reqResize reqRedrawInputLabel + reqRedrawHeaderLabel reqRedrawListLabel reqRedrawBorderLabel reqRedrawPreviewLabel @@ -450,6 +457,7 @@ const ( actChangeListLabel actChangeInputLabel actChangeHeader + actChangeHeaderLabel actChangeMulti actChangePreviewLabel actChangePrompt @@ -512,6 +520,7 @@ const ( actTransformListLabel actTransformInputLabel actTransformHeader + actTransformHeaderLabel actTransformPreviewLabel actTransformPrompt actTransformQuery @@ -847,6 +856,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor borderShape: opts.BorderShape, listBorderShape: opts.ListBorderShape, inputBorderShape: opts.InputBorderShape, + headerBorderShape: opts.HeaderBorderShape, borderWidth: 1, listLabel: nil, listLabelOpts: opts.ListLabel, @@ -856,6 +866,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor previewLabelOpts: opts.PreviewLabel, inputLabel: nil, inputLabelOpts: opts.InputLabel, + headerLabel: nil, + headerLabelOpts: opts.HeaderLabel, cleanExit: opts.ClearOnExit, executor: executor, paused: opts.Phony, @@ -910,7 +922,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor lastFocus: minItem.Index()} // This should be called before accessing tui.Color* - tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black) + tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible()) t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) // Pre-calculated empty pointer and marker signs @@ -920,6 +932,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColPreviewLabel, false) t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(opts.InputLabel.label, &tui.ColInputLabel, false) + t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(opts.HeaderLabel.label, &tui.ColHeaderLabel, false) // Disable separator by default if input border is set if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 { @@ -1064,6 +1077,13 @@ func (t *Terminal) visibleHeaderLines() int { return len(t.header0) + t.headerLines } +func (t *Terminal) visibleHeaderLinesInList() int { + if t.headerWindow != nil { + return 0 + } + return t.visibleHeaderLines() +} + // Extra number of lines needed to display fzf func (t *Terminal) extraLines() int { extra := t.visibleHeaderLines() + 1 @@ -1148,10 +1168,10 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) // Temporarily switch 'window' so that we can use the existing windows with // a different window -func (t *Terminal) withInputWindow(f func()) { +func (t *Terminal) withWindow(w tui.Window, f func()) { prevWindow := t.window - if t.inputWindow != nil { - t.window = t.inputWindow + if w != nil { + t.window = w } f() t.window = prevWindow @@ -1184,7 +1204,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) { output := func() { wrap := t.wrap t.wrap = false - t.withInputWindow(func() { + t.withWindow(t.inputWindow, func() { line := t.promptLine() t.printHighlighted( Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, nil, nil) @@ -1605,6 +1625,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { if t.wborder != nil { t.wborder = nil } + if t.headerWindow != nil { + t.headerWindow = nil + } + if t.headerBorder != nil { + t.headerBorder = nil + } if t.inputWindow != nil { t.inputWindow = nil } @@ -1651,26 +1677,42 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { height -= paddingInt[0] + paddingInt[2] // Adjust position and size of the list window if input border is set - inputWindowHeight := 0 inputBorderHeight := 0 + availableLines := height shift := 0 shrink := 0 - hasInputWindow := t.inputBorderShape.Visible() + hasInputWindow := t.inputBorderShape.Visible() || t.headerBorderShape.Visible() if hasInputWindow { - inputWindowHeight = 2 + inputWindowHeight := 2 if t.noSeparatorLine() { inputWindowHeight-- } - inputBorderHeight = borderLines(t.inputBorderShape) + inputWindowHeight + inputBorderHeight = util.Min(availableLines, borderLines(t.inputBorderShape)+inputWindowHeight) if t.layout == layoutReverse { shift = inputBorderHeight shrink = inputBorderHeight } else { - shift = 0 shrink = inputBorderHeight } + availableLines -= inputBorderHeight } + // Adjust position and size of the list window if header border is set + hasHeaderWindow := t.visibleHeaderLines() > 0 && (t.headerBorderShape.Visible() || t.inputBorderShape.Visible()) + headerBorderHeight := 0 + if hasHeaderWindow { + headerWindowHeight := t.visibleHeaderLines() + headerBorderHeight = util.Min(availableLines, borderLines(t.headerBorderShape)+headerWindowHeight) + if t.layout == layoutReverse { + shift += headerBorderHeight + shrink += headerBorderHeight + } else { + shrink += headerBorderHeight + } + availableLines -= headerBorderHeight + } + + // Set up list border hasListBorder := t.listBorderShape.Visible() innerWidth := width innerHeight := height @@ -1729,8 +1771,6 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { // Need a column to show scrollbar pwidth -= 1 } - pwidth = util.Max(0, pwidth) - pheight = util.Max(0, pheight) t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true) if !hadPreviewWindow { t.pwindow.Erase() @@ -1760,6 +1800,14 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { if previewOpts.hidden { return } + + maxPreviewLines := availableLines + if t.wborder != nil { + maxPreviewLines -= t.wborder.Height() + } else { + maxPreviewLines -= util.Max(0, innerHeight-pheight-shrink) + } + pheight = util.Min(pheight, maxPreviewLines) if previewOpts.position == posUp { innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight) t.window = t.tui.NewWindow( @@ -1873,43 +1921,73 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { innerHeight-shrink, tui.WindowList, noBorder, true) } - // Set up input border - if hasInputWindow { - w := t.wborder - if t.wborder == nil { - w = t.window - } - if t.layout == layoutReverse { - t.inputBorder = t.tui.NewWindow( - w.Top()-shrink, - w.Left(), - w.Width(), - util.Min(shrink, screenHeight), tui.WindowInput, tui.MakeBorderStyle(t.inputBorderShape, t.unicode), true) - } else { - t.inputBorder = t.tui.NewWindow( - w.Top()+w.Height(), - w.Left(), - w.Width(), - util.Min(shrink, screenHeight), tui.WindowInput, tui.MakeBorderStyle(t.inputBorderShape, t.unicode), true) - } - top := t.inputBorder.Top() - left := t.inputBorder.Left() - if t.inputBorderShape.HasTop() { + createInnerWindow := func(b tui.Window, shape tui.BorderShape, windowType tui.WindowType) tui.Window { + top := b.Top() + left := b.Left() + if shape.HasTop() { top++ } - if t.inputBorderShape.HasLeft() { + if shape.HasLeft() { left += t.borderWidth + 1 } - width := t.inputBorder.Width() - borderColumns(t.inputBorderShape, t.borderWidth) - if t.inputBorderShape.HasRight() { + width := b.Width() - borderColumns(shape, t.borderWidth) + if shape.HasRight() { width++ } - t.inputWindow = t.tui.NewWindow( - top, - left, - width, - t.inputBorder.Height()-borderLines(t.inputBorderShape), - tui.WindowInput, noBorder, true) + height := b.Height() - borderLines(shape) + return t.tui.NewWindow(top, left, width, height, windowType, noBorder, true) + } + + // Set up input border + w := t.wborder + if t.wborder == nil { + w = t.window + } + if hasInputWindow { + var btop int + if hasHeaderWindow && t.headerFirst { + if t.layout == layoutReverse { + btop = w.Top() - inputBorderHeight + } else { + btop = w.Top() + w.Height() + } + } else { + if t.layout == layoutReverse { + btop = w.Top() - shrink + } else { + btop = w.Top() + w.Height() + headerBorderHeight + } + } + t.inputBorder = t.tui.NewWindow( + btop, + w.Left(), + w.Width(), + inputBorderHeight, tui.WindowInput, tui.MakeBorderStyle(t.inputBorderShape, t.unicode), true) + t.inputWindow = createInnerWindow(t.inputBorder, t.inputBorderShape, tui.WindowInput) + } + + // Set up header border + if hasHeaderWindow { + var btop int + if hasInputWindow && t.headerFirst { + if t.layout == layoutReverse { + btop = w.Top() - shrink + } else { + btop = w.Top() + w.Height() + inputBorderHeight + } + } else { + if t.layout == layoutReverse { + btop = w.Top() - headerBorderHeight + } else { + btop = w.Top() + w.Height() + } + } + t.headerBorder = t.tui.NewWindow( + btop, + w.Left(), + w.Width(), + headerBorderHeight, tui.WindowHeader, tui.MakeBorderStyle(t.headerBorderShape, t.unicode), true) + t.headerWindow = createInnerWindow(t.headerBorder, t.headerBorderShape, tui.WindowHeader) } // Print border label @@ -1917,6 +1995,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false) t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.border, false) t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false) + t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, false) } func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) { @@ -1960,7 +2039,7 @@ func (t *Terminal) move(y int, x int, clear bool) { case layoutDefault: y = h - y - 1 case layoutReverseList: - n := 2 + t.visibleHeaderLines() + n := 2 + t.visibleHeaderLinesInList() if t.noSeparatorLine() { n-- } @@ -2014,7 +2093,7 @@ func (t *Terminal) promptLine() int { if !t.noSeparatorLine() { max-- } - return util.Min(t.visibleHeaderLines(), max) + return util.Min(t.visibleHeaderLinesInList(), max) } return 0 } @@ -2059,9 +2138,7 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string { } func (t *Terminal) printInfo() { - t.withInputWindow(func() { - t.printInfoImpl() - }) + t.withWindow(t.inputWindow, func() { t.printInfoImpl() }) } func (t *Terminal) printInfoImpl() { @@ -2258,11 +2335,23 @@ func (t *Terminal) printInfoImpl() { } func (t *Terminal) printHeader() { + headerLines := t.visibleHeaderLines() + if t.headerBorderShape.Visible() && (t.headerWindow == nil && headerLines > 0 || t.headerWindow != nil && headerLines != t.headerWindow.Height()) { + t.resizeWindows(false, true) + t.printList() + t.printPrompt() + t.printInfo() + t.printPreview() + } + t.withWindow(t.headerWindow, func() { t.printHeaderImpl() }) +} + +func (t *Terminal) printHeaderImpl() { if t.visibleHeaderLines() == 0 { return } max := t.window.Height() - if t.inputWindow == nil && t.headerFirst { + if t.inputWindow == nil && t.headerWindow == nil && t.headerFirst { max-- if !t.noSeparatorLine() { max-- @@ -2276,13 +2365,14 @@ func (t *Terminal) printHeader() { } // Wrapping is not supported for header wrap := t.wrap + indent := strings.Repeat(" ", t.pointerLen+t.markerLen) t.wrap = false for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) { line := idx if needReverse && idx < len(t.header0) { line = len(t.header0) - idx - 1 } - if t.inputWindow == nil && !t.headerFirst { + if t.inputWindow == nil && t.headerWindow == nil && !t.headerFirst { line++ if !t.noSeparatorLine() { line++ @@ -2299,7 +2389,7 @@ func (t *Terminal) printHeader() { t.printHighlighted(Result{item: item}, tui.ColHeader, tui.ColHeader, false, false, line, line, true, - func(markerClass) { t.window.Print(strings.Repeat(" ", t.pointerLen+t.markerLen)) }, nil) + func(markerClass) { t.window.Print(indent) }, nil) } t.wrap = wrap } @@ -2327,7 +2417,7 @@ func (t *Terminal) printList() { count := t.merger.Length() - t.offset // Start line - startLine := t.promptLines() + t.visibleHeaderLines() + startLine := t.promptLines() + t.visibleHeaderLinesInList() maxy += startLine barRange := [2]int{startLine + barStart, startLine + barStart + barLength} @@ -3187,7 +3277,7 @@ func (t *Terminal) printAll() { func (t *Terminal) flush() { t.placeCursor() if !t.suppress { - windows := make([]tui.Window, 0, 7) + windows := make([]tui.Window, 0, 9) if t.border != nil { windows = append(windows, t.border) } @@ -3203,6 +3293,12 @@ func (t *Terminal) flush() { if t.window != nil { windows = append(windows, t.window) } + if t.headerBorder != nil { + windows = append(windows, t.headerBorder) + } + if t.headerWindow != nil { + windows = append(windows, t.headerWindow) + } if t.inputBorder != nil { windows = append(windows, t.inputBorder) } @@ -4110,6 +4206,8 @@ func (t *Terminal) Loop() error { } case reqRedrawInputLabel: t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true) + case reqRedrawHeaderLabel: + t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true) case reqRedrawListLabel: t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true) case reqRedrawBorderLabel: @@ -4493,6 +4591,12 @@ func (t *Terminal) Loop() error { } else { req(reqHeader) } + case actChangeHeaderLabel: + t.headerLabelOpts.label = a.a + if t.headerBorder != nil { + t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(a.a, &tui.ColHeaderLabel, false) + req(reqRedrawHeaderLabel) + } case actChangeInputLabel: t.inputLabelOpts.label = a.a if t.inputBorder != nil { @@ -4522,6 +4626,13 @@ func (t *Terminal) Loop() error { if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { return doActions(actions) } + case actTransformHeaderLabel: + label := t.executeCommand(a.a, false, true, true, true, "") + t.headerLabelOpts.label = label + if t.headerBorder != nil { + t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) + req(reqRedrawHeaderLabel) + } case actTransformInputLabel: label := t.executeCommand(a.a, false, true, true, true, "") t.inputLabelOpts.label = label @@ -5028,25 +5139,25 @@ func (t *Terminal) Loop() error { case posUp: if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 { pborderDragging = 0 - } else if t.listBorderShape.HasTop() && t.wborder.Enclose(my, mx) && my == t.wborder.Top() { + } else if t.listBorderShape.HasTop() && t.pborder.EncloseX(mx) && my == t.wborder.Top() { pborderDragging = 1 } case posDown: if t.pborder.Enclose(my, mx) && my == t.pborder.Top() { pborderDragging = 0 - } else if t.listBorderShape.HasBottom() && t.wborder.Enclose(my, mx) && my == t.wborder.Top()+t.wborder.Height()-1 { + } else if t.listBorderShape.HasBottom() && t.pborder.EncloseX(mx) && my == t.wborder.Top()+t.wborder.Height()-1 { pborderDragging = 1 } case posLeft: if t.pborder.Enclose(my, mx) && mx == t.pborder.Left()+t.pborder.Width()-1 { pborderDragging = 0 - } else if t.listBorderShape.HasLeft() && t.wborder.Enclose(my, mx) && mx == t.wborder.Left() { + } else if t.listBorderShape.HasLeft() && t.pborder.EncloseY(my) && mx == t.wborder.Left() { pborderDragging = 1 } case posRight: if t.pborder.Enclose(my, mx) && mx == t.pborder.Left() { pborderDragging = 0 - } else if t.listBorderShape.HasRight() && t.wborder.Enclose(my, mx) && mx == t.wborder.Left()+t.wborder.Width()-1 { + } else if t.listBorderShape.HasRight() && t.pborder.EncloseY(my) && mx == t.wborder.Left()+t.wborder.Width()-1 { pborderDragging = 1 } } @@ -5103,6 +5214,20 @@ func (t *Terminal) Loop() error { break } + // Inside the header window + // TODO: Should we trigger this on mouse up instead? + // Should we still trigger it when the position has changed from the down event? + if t.headerVisible && t.headerWindow != nil && t.headerWindow.Enclose(my, mx) { + mx -= t.headerWindow.Left() + t.pointerLen + t.markerLen + my -= t.headerWindow.Top() + if mx < 0 { + break + } + t.clickHeaderLine = my + 1 + t.clickHeaderColumn = mx + 1 + return doActions(actionsFor(tui.ClickHeader)) + } + // Ignored if !t.window.Enclose(my, mx) && !barDragging { break @@ -5111,7 +5236,7 @@ func (t *Terminal) Loop() error { // Translate coordinates mx -= t.window.Left() my -= t.window.Top() - min := t.promptLines() + t.visibleHeaderLines() + min := t.promptLines() + t.visibleHeaderLinesInList() h := t.window.Height() switch t.layout { case layoutDefault: @@ -5180,9 +5305,10 @@ func (t *Terminal) Loop() error { } } return doActions(actionsFor(evt)) - } else if t.headerVisible { + } else if t.headerVisible && t.headerWindow == nil { // Header - numLines := t.visibleHeaderLines() + // TODO: Should we trigger this on mouse up instead? + numLines := t.visibleHeaderLinesInList() lineOffset := 0 if t.inputWindow == nil && !t.headerFirst { // offset for info line @@ -5193,7 +5319,7 @@ func (t *Terminal) Loop() error { } } my -= lineOffset - mx -= 2 // offset gutter + mx -= t.pointerLen + t.markerLen if my >= 0 && my < numLines && mx >= 0 { if t.layout == layoutReverse { t.clickHeaderLine = my + 1 @@ -5525,7 +5651,7 @@ func (t *Terminal) promptLines() int { // Number of item lines in the list window func (t *Terminal) maxItems() int { - max := t.window.Height() - t.visibleHeaderLines() - t.promptLines() + max := t.window.Height() - t.visibleHeaderLinesInList() - t.promptLines() return util.Max(max, 0) } diff --git a/src/tui/light.go b/src/tui/light.go index 3cb1a8bcbc4..48202bced06 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -804,6 +804,9 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind case WindowInput: w.fg = r.theme.Input.Color w.bg = r.theme.InputBg.Color + case WindowHeader: + w.fg = r.theme.Header.Color + w.bg = r.theme.HeaderBg.Color case WindowPreview: w.fg = r.theme.PreviewFg.Color w.bg = r.theme.PreviewBg.Color @@ -862,6 +865,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { color = ColListBorder case WindowInput: color = ColInputBorder + case WindowHeader: + color = ColHeaderBorder case WindowPreview: color = ColPreviewBorder } @@ -885,6 +890,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) { color = ColListBorder case WindowInput: color = ColInputBorder + case WindowHeader: + color = ColHeaderBorder case WindowPreview: color = ColPreviewBorder } @@ -910,6 +917,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { color = ColListBorder case WindowInput: color = ColInputBorder + case WindowHeader: + color = ColHeaderBorder case WindowPreview: color = ColPreviewBorder } @@ -970,9 +979,16 @@ func (w *LightWindow) Y() int { return w.posy } +func (w *LightWindow) EncloseX(x int) bool { + return x >= w.left && x < (w.left+w.width) +} + +func (w *LightWindow) EncloseY(y int) bool { + return y >= w.top && y < (w.top+w.height) +} + func (w *LightWindow) Enclose(y int, x int) bool { - return x >= w.left && x < (w.left+w.width) && - y >= w.top && y < (w.top+w.height) + return w.EncloseX(x) && w.EncloseY(y) } func (w *LightWindow) Move(y int, x int) { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index becdabcd04d..991052bd158 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -557,6 +557,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, switch windowType { case WindowList: normal = ColListBorder + case WindowHeader: + normal = ColHeaderBorder case WindowInput: normal = ColInputBorder case WindowPreview: @@ -593,9 +595,16 @@ func (w *TcellWindow) EraseMaybe() bool { return true } +func (w *TcellWindow) EncloseX(x int) bool { + return x >= w.left && x < (w.left+w.width) +} + +func (w *TcellWindow) EncloseY(y int) bool { + return y >= w.top && y < (w.top+w.height) +} + func (w *TcellWindow) Enclose(y int, x int) bool { - return x >= w.left && x < (w.left+w.width) && - y >= w.top && y < (w.top+w.height) + return w.EncloseX(x) && w.EncloseY(y) } func (w *TcellWindow) Move(y int, x int) { @@ -792,6 +801,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) { style = ColBorder.style() case WindowList: style = ColListBorder.style() + case WindowHeader: + style = ColHeaderBorder.style() case WindowInput: style = ColInputBorder.style() case WindowPreview: diff --git a/src/tui/tui.go b/src/tui/tui.go index 832109ce53c..db846f75e50 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -324,6 +324,9 @@ type ColorTheme struct { Cursor ColorAttr Marker ColorAttr Header ColorAttr + HeaderBg ColorAttr + HeaderBorder ColorAttr + HeaderLabel ColorAttr Separator ColorAttr Scrollbar ColorAttr Border ColorAttr @@ -543,6 +546,7 @@ const ( WindowList WindowPreview WindowInput + WindowHeader ) type Renderer interface { @@ -583,6 +587,8 @@ type Window interface { X() int Y() int + EncloseX(x int) bool + EncloseY(y int) bool Enclose(y int, x int) bool Move(y int, x int) @@ -639,6 +645,8 @@ var ( ColSpinner ColorPair ColInfo ColorPair ColHeader ColorPair + ColHeaderBorder ColorPair + ColHeaderLabel ColorPair ColSeparator ColorPair ColScrollbar ColorPair ColBorder ColorPair @@ -691,6 +699,9 @@ func EmptyTheme() *ColorTheme { InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined}, + HeaderBg: ColorAttr{colUndefined, AttrUndefined}, + HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, + HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, } } @@ -731,6 +742,9 @@ func NoColorTheme() *ColorTheme { InputBg: ColorAttr{colDefault, AttrUndefined}, InputBorder: ColorAttr{colDefault, AttrUndefined}, InputLabel: ColorAttr{colDefault, AttrUndefined}, + HeaderBg: ColorAttr{colDefault, AttrUndefined}, + HeaderBorder: ColorAttr{colDefault, AttrUndefined}, + HeaderLabel: ColorAttr{colDefault, AttrUndefined}, } } @@ -845,10 +859,13 @@ func init() { InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined}, + HeaderBg: ColorAttr{colUndefined, AttrUndefined}, + HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, + HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, } } -func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { +func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) { if forceBlack { theme.Bg = ColorAttr{colBlack, AttrUndefined} } @@ -896,9 +913,22 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { theme.Separator = o(theme.ListBorder, theme.Separator) theme.Scrollbar = o(theme.ListBorder, theme.Scrollbar) theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar) - theme.InputBg = o(theme.Bg, o(theme.ListBg, theme.InputBg)) + if hasInputWindow { + theme.InputBg = o(theme.Bg, theme.InputBg) + } else { + // We shouldn't use input-bg if there's no separate input window + // e.g. fzf --color 'list-bg:green,input-bg:red' --no-input-border + theme.InputBg = o(theme.Bg, theme.ListBg) + } theme.InputBorder = o(theme.Border, theme.InputBorder) theme.InputLabel = o(theme.BorderLabel, theme.InputLabel) + if hasHeaderWindow { + theme.HeaderBg = o(theme.Bg, theme.HeaderBg) + } else { + theme.HeaderBg = o(theme.Bg, theme.ListBg) + } + theme.HeaderBorder = o(theme.Border, theme.HeaderBorder) + theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel) initPalette(theme) } @@ -935,7 +965,6 @@ func initPalette(theme *ColorTheme) { ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColSpinner = pair(theme.Spinner, theme.InputBg) ColInfo = pair(theme.Info, theme.InputBg) - ColHeader = pair(theme.Header, theme.ListBg) ColSeparator = pair(theme.Separator, theme.InputBg) ColScrollbar = pair(theme.Scrollbar, theme.ListBg) ColBorder = pair(theme.Border, theme.Bg) @@ -949,6 +978,9 @@ func initPalette(theme *ColorTheme) { ColListBorder = pair(theme.ListBorder, theme.ListBg) ColInputBorder = pair(theme.InputBorder, theme.InputBg) ColInputLabel = pair(theme.InputLabel, theme.InputBg) + ColHeader = pair(theme.Header, theme.HeaderBg) + ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg) + ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg) } func runeWidth(r rune) int { diff --git a/test/test_go.rb b/test/test_go.rb index 4aff448a9d5..e46e1600123 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -3485,6 +3485,24 @@ def test_input_border_and_label tmux.until { assert_block(block, _1) } end + def test_input_border_and_label_header_first + tmux.send_keys %(seq 100 | #{FZF} --border rounded --input-border bold --input-label input --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 --header-first), :Enter + block = <<~BLOCK + │ 11 + │ > 10 + │ ┏input━━━━━ + │ ┃ 19/97 + │ ┃ > 1 + │ ┗━━━━━━━━━━ + │ 3 + │ 2 + │ 1 + │ + ╰────────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + def test_list_input_border_and_label tmux.send_keys %( seq 100 | #{FZF} --border rounded --list-border double --input-border bold --list-label-pos 2:bottom --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 \ @@ -3494,32 +3512,193 @@ def test_list_input_border_and_label block = <<~BLOCK │ ║ 11 │ ║ > 10 - │ ║ 3 - │ ║ 2 - │ ║ 1 - │ ╚LIST═════ - │ ┏INPUT━━━━ + │ ╚LIST══════ + │ 3 + │ 2 + │ 1 + │ ┏INPUT━━━━━ │ ┃ 19/97 │ ┃ > 1 - │ ┗━━━━━━━━━ + │ ┗━━━━━━━━━━ │ - ╰───────────── + ╰────────────── BLOCK tmux.until { assert_block(block, _1) } tmux.send_keys :Space block = <<~BLOCK │ ║ 11 │ ║ > 10 - │ ║ 3 - │ ║ 2 - │ ║ 1 - │ ╚ list ═══ - │ ┏ input ━━ + │ ╚ list ════ + │ 3 + │ 2 + │ 1 + │ ┏ input ━━━ │ ┃ 19/97 │ ┃ > 1 - │ ┗━━━━━━━━━ + │ ┗━━━━━━━━━━ │ - ╰───────────── + ╰────────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_list_input_border_and_label_header_first + tmux.send_keys %( + seq 100 | #{FZF} --border rounded --list-border double --input-border bold --list-label-pos 2:bottom --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 \ + --bind 'start:transform-input-label(echo INPUT)+transform-list-label(echo LIST)' \ + --bind 'space:change-input-label( input )+change-list-label( list )' --header-first + ).strip, :Enter + block = <<~BLOCK + │ ║ 11 + │ ║ > 10 + │ ╚LIST══════ + │ ┏INPUT━━━━━ + │ ┃ 19/97 + │ ┃ > 1 + │ ┗━━━━━━━━━━ + │ 3 + │ 2 + │ 1 + │ + ╰────────────── + BLOCK + tmux.until { assert_block(block, _1) } + tmux.send_keys :Space + block = <<~BLOCK + │ ║ 11 + │ ║ > 10 + │ ╚ list ════ + │ ┏ input ━━━ + │ ┃ 19/97 + │ ┃ > 1 + │ ┗━━━━━━━━━━ + │ 3 + │ 2 + │ 1 + │ + ╰────────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_header_border_and_label + tmux.send_keys %(seq 100 | #{FZF} --border rounded --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2), :Enter + block = <<~BLOCK + │ 12 + │ 11 + │ > 10 + │ ┌──────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header── + │ 19/97 ─ + │ > 1 + │ + ╰──────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_header_border_and_label_header_first + tmux.send_keys %(seq 100 | #{FZF} --border rounded --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --header-first), :Enter + block = <<~BLOCK + │ 12 + │ 11 + │ > 10 + │ 19/97 ─ + │ > 1 + │ ┌──────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header── + │ + ╰──────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_header_border_and_label_with_list_border + tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2), :Enter + block = <<~BLOCK + │ ║ 12 + │ ║ 11 + │ ║ > 10 + │ ╚list════ + │ ┌──────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header── + │ 19/97 ─ + │ > 1 + │ + ╰──────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_header_border_and_label_with_list_border_header_first + tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --header-first), :Enter + block = <<~BLOCK + │ ║ 12 + │ ║ 11 + │ ║ > 10 + │ ╚list════ + │ 19/97 ─ + │ > 1 + │ ┌──────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header── + │ + ╰──────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_all_borders + tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --input-border bold --input-label input --input-label-pos 2:bottom), :Enter + block = <<~BLOCK + │ ║ 12 + │ ║ 11 + │ ║ > 10 + │ ╚list══════ + │ ┌────────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header──── + │ ┏━━━━━━━━━━ + │ ┃ 19/97 + │ ┃ > 1 + │ ┗input━━━━━ + │ + ╰────────────── + BLOCK + tmux.until { assert_block(block, _1) } + end + + def test_all_borders_header_first + tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --input-border bold --input-label input --input-label-pos 2:bottom --header-first), :Enter + block = <<~BLOCK + │ ║ 12 + │ ║ 11 + │ ║ > 10 + │ ╚list══════ + │ ┏━━━━━━━━━━ + │ ┃ 19/97 + │ ┃ > 1 + │ ┗input━━━━━ + │ ┌────────── + │ │ 3 + │ │ 2 + │ │ 1 + │ └header──── + │ + ╰────────────── BLOCK tmux.until { assert_block(block, _1) } end