diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c5f13622..834df205 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "May 2025" "fzf 0.62.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jun 2025" "fzf 0.63.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -503,6 +503,8 @@ Draw border around the finder .br .BR vertical " Vertical lines on each side of the finder" .br +.BR line " Single line border (position automatically determined)" +.br .BR top " (up)" .br .BR bottom " (down)" @@ -518,6 +520,9 @@ If you use a terminal emulator where each box-drawing character takes 2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly rendered, set \fB\-\-no\-unicode\fR. +\fBline\fR style draws a single separator line at the top when \fB\-\-height\fR +is used. + .TP .BI "\-\-border\-label" [=LABEL] Label to print on the horizontal border line. Should be used with one of the @@ -661,7 +666,8 @@ Do not display scrollbar. A synonym for \fB\-\-scrollbar=''\fB .TP .BI "\-\-list\-border" [=STYLE] -Draw border around the list section +Draw border around the list section. \fBline\fR style is not supported for +this border. .TP .BI "\-\-list\-label" [=LABEL] @@ -748,7 +754,8 @@ actions are affected: \fBkill\-word\fR .TP .BI "\-\-input\-border" [=STYLE] -Draw border around the input section +Draw border around the input section. \fBline\fR style draws a single separator +line between the input section and the list section. .TP .BI "\-\-input\-label" [=LABEL] @@ -848,8 +855,7 @@ e.g. .TP .BI "\-\-preview\-border" [=STYLE] -Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other -styles, \fBline\fR style is also supported for preview border, which draws +Short for \fB\-\-preview\-window=border\-STYLE\fR. \fBline\fR style draws a single separator line between the preview window and the rest of the interface. @@ -1005,7 +1011,8 @@ Print header before the prompt line. When both normal header and header lines (\fB\-\-header\-lines\fR) are present, this applies only to the normal header. .TP .BI "\-\-header\-border" [=STYLE] -Draw border around the header section +Draw border around the header section. \fBline\fR style draws a single +separator line between the header window and the list section. .TP .BI "\-\-header\-label" [=LABEL] @@ -1019,7 +1026,30 @@ Position of the header label .BI "\-\-header\-lines\-border" [=STYLE] Display header from \fB--header\-lines\fR with a separate border. Pass \fBnone\fR to still separate the header lines but without a border. To combine -two headers, use \fB\-\-no\-header\-lines\-border\fR. +two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws +a single separator line between the header lines and the list section. + +.SS FOOTER + +.TP +.BI "\-\-footer=" "STR" +The given string will be printed as the sticky footer. The lines are displayed +in the given order from top to bottom regardless of \fB\-\-layout\fR option, and +are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even when +\fB\-\-ansi\fR is not set. + +.TP +.BI "\-\-footer\-border" [=STYLE] +Draw border around the header section. \fBline\fR style draws a single +separator line between the footer and the list section. + +.TP +.BI "\-\-footer\-label" [=LABEL] +Label to print on the footer border + +.TP +.BI "\-\-footer\-label\-pos" [=N[:top|bottom]] +Position of the footer label .SS SCRIPTING .TP diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 8b78008f..bd141fde 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -29,128 +29,132 @@ func _() { _ = x[actChangeBorderLabel-18] _ = x[actChangeGhost-19] _ = x[actChangeHeader-20] - _ = x[actChangeHeaderLabel-21] - _ = x[actChangeInputLabel-22] - _ = x[actChangeListLabel-23] - _ = x[actChangeMulti-24] - _ = x[actChangeNth-25] - _ = x[actChangePointer-26] - _ = x[actChangePreview-27] - _ = x[actChangePreviewLabel-28] - _ = x[actChangePreviewWindow-29] - _ = x[actChangePrompt-30] - _ = x[actChangeQuery-31] - _ = x[actClearScreen-32] - _ = x[actClearQuery-33] - _ = x[actClearSelection-34] - _ = x[actClose-35] - _ = x[actDeleteChar-36] - _ = x[actDeleteCharEof-37] - _ = x[actEndOfLine-38] - _ = x[actFatal-39] - _ = x[actForwardChar-40] - _ = x[actForwardWord-41] - _ = x[actKillLine-42] - _ = x[actKillWord-43] - _ = x[actUnixLineDiscard-44] - _ = x[actUnixWordRubout-45] - _ = x[actYank-46] - _ = x[actBackwardKillWord-47] - _ = x[actSelectAll-48] - _ = x[actDeselectAll-49] - _ = x[actToggle-50] - _ = x[actToggleSearch-51] - _ = x[actToggleAll-52] - _ = x[actToggleDown-53] - _ = x[actToggleUp-54] - _ = x[actToggleIn-55] - _ = x[actToggleOut-56] - _ = x[actToggleTrack-57] - _ = x[actToggleTrackCurrent-58] - _ = x[actToggleHeader-59] - _ = x[actToggleWrap-60] - _ = x[actToggleMultiLine-61] - _ = x[actToggleHscroll-62] - _ = x[actTrackCurrent-63] - _ = x[actToggleInput-64] - _ = x[actHideInput-65] - _ = x[actShowInput-66] - _ = x[actUntrackCurrent-67] - _ = x[actDown-68] - _ = x[actUp-69] - _ = x[actPageUp-70] - _ = x[actPageDown-71] - _ = x[actPosition-72] - _ = x[actHalfPageUp-73] - _ = x[actHalfPageDown-74] - _ = x[actOffsetUp-75] - _ = x[actOffsetDown-76] - _ = x[actOffsetMiddle-77] - _ = x[actJump-78] - _ = x[actJumpAccept-79] - _ = x[actPrintQuery-80] - _ = x[actRefreshPreview-81] - _ = x[actReplaceQuery-82] - _ = x[actToggleSort-83] - _ = x[actShowPreview-84] - _ = x[actHidePreview-85] - _ = x[actTogglePreview-86] - _ = x[actTogglePreviewWrap-87] - _ = x[actTransform-88] - _ = x[actTransformBorderLabel-89] - _ = x[actTransformGhost-90] - _ = x[actTransformHeader-91] - _ = x[actTransformHeaderLabel-92] - _ = x[actTransformInputLabel-93] - _ = x[actTransformListLabel-94] - _ = x[actTransformNth-95] - _ = x[actTransformPointer-96] - _ = x[actTransformPreviewLabel-97] - _ = x[actTransformPrompt-98] - _ = x[actTransformQuery-99] - _ = x[actTransformSearch-100] - _ = x[actSearch-101] - _ = x[actPreview-102] - _ = x[actPreviewTop-103] - _ = x[actPreviewBottom-104] - _ = x[actPreviewUp-105] - _ = x[actPreviewDown-106] - _ = x[actPreviewPageUp-107] - _ = x[actPreviewPageDown-108] - _ = x[actPreviewHalfPageUp-109] - _ = x[actPreviewHalfPageDown-110] - _ = x[actPrevHistory-111] - _ = x[actPrevSelected-112] - _ = x[actPrint-113] - _ = x[actPut-114] - _ = x[actNextHistory-115] - _ = x[actNextSelected-116] - _ = x[actExecute-117] - _ = x[actExecuteSilent-118] - _ = x[actExecuteMulti-119] - _ = x[actSigStop-120] - _ = x[actFirst-121] - _ = x[actLast-122] - _ = x[actReload-123] - _ = x[actReloadSync-124] - _ = x[actDisableSearch-125] - _ = x[actEnableSearch-126] - _ = x[actSelect-127] - _ = x[actDeselect-128] - _ = x[actUnbind-129] - _ = x[actRebind-130] - _ = x[actToggleBind-131] - _ = x[actBecome-132] - _ = x[actShowHeader-133] - _ = x[actHideHeader-134] - _ = x[actBell-135] - _ = x[actExclude-136] - _ = x[actExcludeMulti-137] + _ = x[actChangeFooter-21] + _ = x[actChangeHeaderLabel-22] + _ = x[actChangeFooterLabel-23] + _ = x[actChangeInputLabel-24] + _ = x[actChangeListLabel-25] + _ = x[actChangeMulti-26] + _ = x[actChangeNth-27] + _ = x[actChangePointer-28] + _ = x[actChangePreview-29] + _ = x[actChangePreviewLabel-30] + _ = x[actChangePreviewWindow-31] + _ = x[actChangePrompt-32] + _ = x[actChangeQuery-33] + _ = x[actClearScreen-34] + _ = x[actClearQuery-35] + _ = x[actClearSelection-36] + _ = x[actClose-37] + _ = x[actDeleteChar-38] + _ = x[actDeleteCharEof-39] + _ = x[actEndOfLine-40] + _ = x[actFatal-41] + _ = x[actForwardChar-42] + _ = x[actForwardWord-43] + _ = x[actKillLine-44] + _ = x[actKillWord-45] + _ = x[actUnixLineDiscard-46] + _ = x[actUnixWordRubout-47] + _ = x[actYank-48] + _ = x[actBackwardKillWord-49] + _ = x[actSelectAll-50] + _ = x[actDeselectAll-51] + _ = x[actToggle-52] + _ = x[actToggleSearch-53] + _ = x[actToggleAll-54] + _ = x[actToggleDown-55] + _ = x[actToggleUp-56] + _ = x[actToggleIn-57] + _ = x[actToggleOut-58] + _ = x[actToggleTrack-59] + _ = x[actToggleTrackCurrent-60] + _ = x[actToggleHeader-61] + _ = x[actToggleWrap-62] + _ = x[actToggleMultiLine-63] + _ = x[actToggleHscroll-64] + _ = x[actTrackCurrent-65] + _ = x[actToggleInput-66] + _ = x[actHideInput-67] + _ = x[actShowInput-68] + _ = x[actUntrackCurrent-69] + _ = x[actDown-70] + _ = x[actUp-71] + _ = x[actPageUp-72] + _ = x[actPageDown-73] + _ = x[actPosition-74] + _ = x[actHalfPageUp-75] + _ = x[actHalfPageDown-76] + _ = x[actOffsetUp-77] + _ = x[actOffsetDown-78] + _ = x[actOffsetMiddle-79] + _ = x[actJump-80] + _ = x[actJumpAccept-81] + _ = x[actPrintQuery-82] + _ = x[actRefreshPreview-83] + _ = x[actReplaceQuery-84] + _ = x[actToggleSort-85] + _ = x[actShowPreview-86] + _ = x[actHidePreview-87] + _ = x[actTogglePreview-88] + _ = x[actTogglePreviewWrap-89] + _ = x[actTransform-90] + _ = x[actTransformBorderLabel-91] + _ = x[actTransformGhost-92] + _ = x[actTransformHeader-93] + _ = x[actTransformFooter-94] + _ = x[actTransformHeaderLabel-95] + _ = x[actTransformFooterLabel-96] + _ = x[actTransformInputLabel-97] + _ = x[actTransformListLabel-98] + _ = x[actTransformNth-99] + _ = x[actTransformPointer-100] + _ = x[actTransformPreviewLabel-101] + _ = x[actTransformPrompt-102] + _ = x[actTransformQuery-103] + _ = x[actTransformSearch-104] + _ = x[actSearch-105] + _ = x[actPreview-106] + _ = x[actPreviewTop-107] + _ = x[actPreviewBottom-108] + _ = x[actPreviewUp-109] + _ = x[actPreviewDown-110] + _ = x[actPreviewPageUp-111] + _ = x[actPreviewPageDown-112] + _ = x[actPreviewHalfPageUp-113] + _ = x[actPreviewHalfPageDown-114] + _ = x[actPrevHistory-115] + _ = x[actPrevSelected-116] + _ = x[actPrint-117] + _ = x[actPut-118] + _ = x[actNextHistory-119] + _ = x[actNextSelected-120] + _ = x[actExecute-121] + _ = x[actExecuteSilent-122] + _ = x[actExecuteMulti-123] + _ = x[actSigStop-124] + _ = x[actFirst-125] + _ = x[actLast-126] + _ = x[actReload-127] + _ = x[actReloadSync-128] + _ = x[actDisableSearch-129] + _ = x[actEnableSearch-130] + _ = x[actSelect-131] + _ = x[actDeselect-132] + _ = x[actUnbind-133] + _ = x[actRebind-134] + _ = x[actToggleBind-135] + _ = x[actBecome-136] + _ = x[actShowHeader-137] + _ = x[actHideHeader-138] + _ = x[actBell-139] + _ = x[actExclude-140] + _ = x[actExcludeMulti-141] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti" -var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 318, 337, 355, 369, 381, 397, 413, 434, 456, 471, 485, 499, 512, 529, 537, 550, 566, 578, 586, 600, 614, 625, 636, 654, 671, 678, 697, 709, 723, 732, 747, 759, 772, 783, 794, 806, 820, 841, 856, 869, 887, 903, 918, 932, 944, 956, 973, 980, 985, 994, 1005, 1016, 1029, 1044, 1055, 1068, 1083, 1090, 1103, 1116, 1133, 1148, 1161, 1175, 1189, 1205, 1225, 1237, 1260, 1277, 1295, 1318, 1340, 1361, 1376, 1395, 1419, 1437, 1454, 1472, 1481, 1491, 1504, 1520, 1532, 1546, 1562, 1580, 1600, 1622, 1636, 1651, 1659, 1665, 1679, 1694, 1704, 1720, 1735, 1745, 1753, 1760, 1769, 1782, 1798, 1813, 1822, 1833, 1842, 1851, 1864, 1873, 1886, 1899, 1906, 1916, 1931} +var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 313, 333, 353, 372, 390, 404, 416, 432, 448, 469, 491, 506, 520, 534, 547, 564, 572, 585, 601, 613, 621, 635, 649, 660, 671, 689, 706, 713, 732, 744, 758, 767, 782, 794, 807, 818, 829, 841, 855, 876, 891, 904, 922, 938, 953, 967, 979, 991, 1008, 1015, 1020, 1029, 1040, 1051, 1064, 1079, 1090, 1103, 1118, 1125, 1138, 1151, 1168, 1183, 1196, 1210, 1224, 1240, 1260, 1272, 1295, 1312, 1330, 1348, 1371, 1394, 1416, 1437, 1452, 1471, 1495, 1513, 1530, 1548, 1557, 1567, 1580, 1596, 1608, 1622, 1638, 1656, 1676, 1698, 1712, 1727, 1735, 1741, 1755, 1770, 1780, 1796, 1811, 1821, 1829, 1836, 1845, 1858, 1874, 1889, 1898, 1909, 1918, 1927, 1940, 1949, 1962, 1975, 1982, 1992, 2007} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/core.go b/src/core.go index dffc4f36..46cd957a 100644 --- a/src/core.go +++ b/src/core.go @@ -39,7 +39,7 @@ func (r revision) compatible(other revision) bool { // Run starts fzf func Run(opts *Options) (int, error) { if opts.Filter == nil { - if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index { + if opts.useTmux() { return runTmux(os.Args, opts) } diff --git a/src/options.go b/src/options.go index 142179e5..2fd6f821 100644 --- a/src/options.go +++ b/src/options.go @@ -83,7 +83,7 @@ Usage: fzf [options] --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --border[=STYLE] Draw border around the finder [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| - top|bottom|left|right|none] (default: rounded) + top|bottom|left|right|line|none] (default: rounded) --border-label=LABEL Label to print on the border --border-label-pos=COL Position of the border label [POSITIVE_INTEGER: columns from left| @@ -140,7 +140,7 @@ Usage: fzf [options] --filepath-word Make word-wise movements respect path separators --input-border[=STYLE] Draw border around the input section [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| - top|bottom|left|right|none] (default: rounded) + top|bottom|left|right|line|none] (default: rounded) --input-label=LABEL Label to print on the input border --input-label-pos=COL Position of the input label [POSITIVE_INTEGER: columns from left| @@ -168,7 +168,7 @@ Usage: fzf [options] --header-first Print header before the prompt line --header-border[=STYLE] Draw border around the header section [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| - top|bottom|left|right|none] (default: rounded) + top|bottom|left|right|line|none] (default: rounded) --header-lines-border[=STYLE] Display header from --header-lines with a separate border. Pass 'none' to still separate it but without a border. @@ -178,6 +178,17 @@ Usage: fzf [options] NEGATIVE_INTEGER: columns from right][:bottom] (default: 0 or center) + FOOTER + --footer=STR String to print as footer + --footer-border[=STYLE] Draw border around the footer section + [rounded|sharp|bold|block|thinblock|double|horizontal|vertical| + top|bottom|left|right|line|none] (default: line) + --footer-label=LABEL Label to print on the footer border + --footer-label-pos=COL Position of the footer label + [POSITIVE_INTEGER: columns from left| + NEGATIVE_INTEGER: columns from right][:bottom] + (default: 0 or center) + SCRIPTING -q, --query=STR Start the finder with the given query -1, --select-1 Automatically select the only match @@ -599,6 +610,7 @@ type Options struct { Header []string HeaderLines int HeaderFirst bool + Footer []string Gap int GapLine *string Ellipsis *string @@ -610,8 +622,10 @@ type Options struct { InputBorderShape tui.BorderShape HeaderBorderShape tui.BorderShape HeaderLinesShape tui.BorderShape + FooterBorderShape tui.BorderShape InputLabel labelOpts HeaderLabel labelOpts + FooterLabel labelOpts BorderLabel labelOpts ListLabel labelOpts PreviewLabel labelOpts @@ -716,6 +730,7 @@ func defaultOptions() *Options { Header: make([]string, 0), HeaderLines: 0, HeaderFirst: false, + Footer: make([]string, 0), Gap: 0, Ellipsis: nil, Scrollbar: nil, @@ -880,12 +895,9 @@ func parseAlgo(str string) (algo.Algo, error) { return nil, errors.New("invalid algorithm (expected: v1 or v2)") } -func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) { +func parseBorder(str string, optional bool) (tui.BorderShape, error) { switch str { case "line": - if !allowLine { - return tui.BorderNone, errors.New("'line' is only allowed for preview border") - } return tui.BorderLine, nil case "rounded": return tui.BorderRounded, nil @@ -1348,6 +1360,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.HeaderBorder) case "header-label": mergeAttr(&theme.HeaderLabel) + case "footer-border": + mergeAttr(&theme.FooterBorder) + case "footer-label": + mergeAttr(&theme.FooterLabel) case "spinner": mergeAttr(&theme.Spinner) case "info": @@ -1360,6 +1376,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.Header) case "header-bg": mergeAttr(&theme.HeaderBg) + case "footer", "footer-fg": + mergeAttr(&theme.Footer) + case "footer-bg": + mergeAttr(&theme.FooterBg) case "gap-line": mergeAttr(&theme.GapLine) default: @@ -1415,7 +1435,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1800,6 +1820,8 @@ func isExecuteAction(str string) actionType { return actPreview case "change-header": return actChangeHeader + case "change-footer": + return actChangeFooter case "change-list-label": return actChangeListLabel case "change-border-label": @@ -1810,6 +1832,8 @@ func isExecuteAction(str string) actionType { return actChangeInputLabel case "change-header-label": return actChangeHeaderLabel + case "change-footer-label": + return actChangeFooterLabel case "change-ghost": return actChangeGhost case "change-pointer": @@ -1850,6 +1874,10 @@ func isExecuteAction(str string) actionType { return actTransformInputLabel case "transform-header-label": return actTransformHeaderLabel + case "transform-footer-label": + return actTransformFooterLabel + case "transform-footer": + return actTransformFooter case "transform-header": return actTransformHeader case "transform-ghost": @@ -2729,6 +2757,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { if opts.HeaderLines, err = nextInt("number of header lines required"); err != nil { return err } + case "--no-footer": + opts.Footer = []string{} + case "--footer": + str, err := nextString("footer string required") + if err != nil { + return err + } + opts.Footer = strLines(str) case "--header-first": opts.HeaderFirst = true case "--no-header-first": @@ -2773,7 +2809,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { opts.Preview.border = tui.BorderNone case "--preview-border": hasArg, arg := optionalNextString() - if opts.Preview.border, err = parseBorder(arg, !hasArg, true); err != nil { + if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil { return err } case "--height": @@ -2812,14 +2848,17 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { opts.BorderShape = tui.BorderNone case "--border": hasArg, arg := optionalNextString() - if opts.BorderShape, err = parseBorder(arg, !hasArg, false); err != nil { + if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil { return err } case "--list-border": hasArg, arg := optionalNextString() - if opts.ListBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { + if opts.ListBorderShape, err = parseBorder(arg, !hasArg); err != nil { return err } + if opts.ListBorderShape == tui.BorderLine { + return errors.New("list border cannot be 'line'") + } case "--no-list-border": opts.ListBorderShape = tui.BorderNone case "--no-list-label": @@ -2841,14 +2880,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { opts.HeaderBorderShape = tui.BorderNone case "--header-border": hasArg, arg := optionalNextString() - if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { + if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil { return err } case "--no-header-lines-border": opts.HeaderLinesShape = tui.BorderNone case "--header-lines-border": hasArg, arg := optionalNextString() - if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg, false); err != nil { + if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg); err != nil { return err } case "--no-header-label": @@ -2865,11 +2904,32 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil { return err } + case "--no-footer-border": + opts.FooterBorderShape = tui.BorderNone + case "--footer-border": + hasArg, arg := optionalNextString() + if opts.FooterBorderShape, err = parseBorder(arg, !hasArg); err != nil { + return err + } + case "--no-footer-label": + opts.FooterLabel.label = "" + case "--footer-label": + if opts.FooterLabel.label, err = nextString("footer label required"); err != nil { + return err + } + case "--footer-label-pos": + pos, err := nextString("footer label position required (positive or negative integer or 'center')") + if err != nil { + return err + } + if err := parseLabelPosition(&opts.FooterLabel, pos); err != nil { + return err + } case "--no-input-border": opts.InputBorderShape = tui.BorderNone case "--input-border": hasArg, arg := optionalNextString() - if opts.InputBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { + if opts.InputBorderShape, err = parseBorder(arg, !hasArg); err != nil { return err } case "--no-input-label": @@ -3077,6 +3137,7 @@ func applyPreset(opts *Options, preset string) error { opts.ListBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined + opts.FooterBorderShape = tui.BorderUndefined opts.Preview.border = defaultBorderShape opts.Preview.info = true opts.InfoStyle = infoDefault @@ -3088,6 +3149,7 @@ func applyPreset(opts *Options, preset string) error { opts.ListBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined + opts.FooterBorderShape = tui.BorderLine opts.Preview.border = tui.BorderLine opts.Preview.info = false opts.InfoStyle = infoDefault @@ -3103,16 +3165,22 @@ func applyPreset(opts *Options, preset string) error { } if len(tokens) == 2 && len(tokens[1]) > 0 { var err error - defaultBorderShape, err = parseBorder(tokens[1], false, false) + defaultBorderShape, err = parseBorder(tokens[1], false) if err != nil { return err } } - opts.ListBorderShape = defaultBorderShape + if defaultBorderShape != tui.BorderLine { + opts.ListBorderShape = defaultBorderShape + } opts.InputBorderShape = defaultBorderShape opts.HeaderBorderShape = defaultBorderShape + opts.FooterBorderShape = defaultBorderShape opts.Preview.border = defaultBorderShape + if defaultBorderShape == tui.BorderLine { + opts.BorderShape = defaultBorderShape + } opts.Preview.info = true opts.InfoStyle = infoInlineRight opts.Theme.Gutter = tui.NewColorAttr() @@ -3185,6 +3253,10 @@ func noSeparatorLine(style infoStyle, separator bool) bool { return false } +func (opts *Options) useTmux() bool { + return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index +} + func (opts *Options) noSeparatorLine() bool { if opts.Inputless { return true @@ -3216,6 +3288,10 @@ func postProcessOptions(opts *Options) error { opts.HeaderBorderShape = tui.BorderNone } + if opts.FooterBorderShape == tui.BorderUndefined { + opts.FooterBorderShape = tui.BorderLine + } + if opts.HeaderLinesShape == tui.BorderNone { opts.HeaderLinesShape = tui.BorderPhantom } diff --git a/src/terminal.go b/src/terminal.go index a89551da..898d34c7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -180,14 +180,18 @@ type itemLine struct { other bool } +func (t *Terminal) inListWindow() bool { + return t.window != t.inputWindow && t.window != t.headerWindow && t.window != t.headerLinesWindow && t.window != t.footerWindow +} + func (t *Terminal) markEmptyLine(line int) { - if t.window != t.inputWindow && t.window != t.headerWindow { + if t.inListWindow() { t.prevLines[line] = itemLine{valid: true, firstLine: line, empty: true} } } func (t *Terminal) markOtherLine(line int) { - if t.window != t.inputWindow && t.window != t.headerWindow { + if t.inListWindow() { t.prevLines[line] = itemLine{valid: true, firstLine: line, other: true} } } @@ -254,6 +258,9 @@ type Terminal struct { headerLabel labelPrinter headerLabelLen int headerLabelOpts labelOpts + footerLabel labelPrinter + footerLabelLen int + footerLabelOpts labelOpts pointer string pointerLen int pointerEmpty string @@ -301,6 +308,7 @@ type Terminal struct { headerLines int header []string header0 []string + footer []string ellipsis string scrollbar string previewScrollbar string @@ -322,6 +330,7 @@ type Terminal struct { inputBorderShape tui.BorderShape headerBorderShape tui.BorderShape headerLinesShape tui.BorderShape + footerBorderShape tui.BorderShape listLabel labelPrinter listLabelLen int listLabelOpts labelOpts @@ -337,6 +346,8 @@ type Terminal struct { headerBorder tui.Window headerLinesWindow tui.Window headerLinesBorder tui.Window + footerWindow tui.Window + footerBorder tui.Window wborder tui.Window pborder tui.Window pwindow tui.Window @@ -426,6 +437,7 @@ const ( reqPrompt util.EventType = iota reqInfo reqHeader + reqFooter reqList reqJump reqActivate @@ -434,6 +446,7 @@ const ( reqResize reqRedrawInputLabel reqRedrawHeaderLabel + reqRedrawFooterLabel reqRedrawListLabel reqRedrawBorderLabel reqRedrawPreviewLabel @@ -479,7 +492,9 @@ const ( actChangeBorderLabel actChangeGhost actChangeHeader + actChangeFooter actChangeHeaderLabel + actChangeFooterLabel actChangeInputLabel actChangeListLabel actChangeMulti @@ -550,7 +565,9 @@ const ( actTransformBorderLabel actTransformGhost actTransformHeader + actTransformFooter actTransformHeaderLabel + actTransformFooterLabel actTransformInputLabel actTransformListLabel actTransformNth @@ -907,6 +924,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor inputBorderShape: opts.InputBorderShape, headerBorderShape: opts.HeaderBorderShape, headerLinesShape: opts.HeaderLinesShape, + footerBorderShape: opts.FooterBorderShape, borderWidth: 1, listLabel: nil, listLabelOpts: opts.ListLabel, @@ -918,6 +936,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor inputLabelOpts: opts.InputLabel, headerLabel: nil, headerLabelOpts: opts.HeaderLabel, + footerLabel: nil, + footerLabelOpts: opts.FooterLabel, cleanExit: opts.ClearOnExit, executor: executor, paused: opts.Phony, @@ -929,6 +949,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor headerLines: opts.HeaderLines, gap: opts.Gap, header: []string{}, + footer: opts.Footer, header0: opts.Header, ansi: opts.Ansi, nthAttr: opts.Theme.Nth.Attr, @@ -992,6 +1013,52 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor 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) + t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(opts.FooterLabel.label, &tui.ColFooterLabel, false) + + // Determine border shape + if t.borderShape == tui.BorderLine { + if t.fullscreen { + t.borderShape = tui.BorderNone + } else { + t.borderShape = tui.BorderTop + } + } + + // Determine input border shape + if t.inputBorderShape == tui.BorderLine { + if t.layout == layoutReverse { + t.inputBorderShape = tui.BorderBottom + } else { + t.inputBorderShape = tui.BorderTop + } + } + + // Determine header border shape + if t.headerBorderShape == tui.BorderLine { + if t.layout == layoutReverse { + t.headerBorderShape = tui.BorderBottom + } else { + t.headerBorderShape = tui.BorderTop + } + } + + // Determine header lines border shape + if t.headerLinesShape == tui.BorderLine { + if t.layout == layoutDefault { + t.headerLinesShape = tui.BorderTop + } else { + t.headerLinesShape = tui.BorderBottom + } + } + + // Determine footer border shape + if t.footerBorderShape == tui.BorderLine { + if t.layout == layoutReverse { + t.footerBorderShape = tui.BorderTop + } else { + t.footerBorderShape = tui.BorderBottom + } + } // Disable separator by default if input border is set if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 { @@ -1208,6 +1275,10 @@ func (t *Terminal) extraLines() int { } extra += t.headerLines } + if len(t.footer) > 0 { + extra += borderLines(t.footerBorderShape) + extra += len(t.footer) + } return extra } @@ -1475,6 +1546,16 @@ func (t *Terminal) changeHeader(header string) bool { return needFullRedraw } +func (t *Terminal) changeFooter(footer string) bool { + var lines []string + if len(footer) > 0 { + lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") + } + needFullRedraw := len(t.footer) != len(lines) + t.footer = lines + return needFullRedraw +} + // UpdateHeader updates the header func (t *Terminal) UpdateHeader(header []string) { t.mutex.Lock() @@ -1835,6 +1916,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { if t.headerBorder != nil { t.headerBorder = nil } + if t.footerWindow != nil { + t.footerWindow = nil + } + if t.footerBorder != nil { + t.footerBorder = nil + } if t.headerLinesWindow != nil { t.headerLinesWindow = nil } @@ -1889,17 +1976,19 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { // Adjust position and size of the list window if input border is set inputBorderHeight := 0 availableLines := height + shift := 0 shrink := 0 hasHeaderWindow := t.hasHeaderWindow() + hasFooterWindow := len(t.footer) > 0 hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape() hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow) + inputWindowHeight := 2 + if t.noSeparatorLine() { + inputWindowHeight-- + } if hasInputWindow { - inputWindowHeight := 2 - if t.noSeparatorLine() { - inputWindowHeight-- - } - inputBorderHeight = util.Min(availableLines, borderLines(t.inputBorderShape)+inputWindowHeight) + inputBorderHeight = util.Constrain(borderLines(t.inputBorderShape)+inputWindowHeight, 0, availableLines) if t.layout == layoutReverse { shift = inputBorderHeight shrink = inputBorderHeight @@ -1907,6 +1996,17 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { shrink = inputBorderHeight } availableLines -= inputBorderHeight + } else if !t.inputless { + availableLines -= inputWindowHeight + } + + // FIXME: Needed? + if t.needPreviewWindow() { + _, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts) + switch t.activePreviewOpts.position { + case posUp, posDown: + availableLines -= minPreviewHeight + } } // Adjust position and size of the list window if header border is set @@ -1916,7 +2016,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { if hasHeaderLinesWindow { headerWindowHeight -= t.headerLines } - headerBorderHeight = util.Min(availableLines, borderLines(t.headerBorderShape)+headerWindowHeight) + headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines) if t.layout == layoutReverse { shift += headerBorderHeight shrink += headerBorderHeight @@ -1928,7 +2028,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { headerLinesHeight := 0 if hasHeaderLinesWindow { - headerLinesHeight = util.Min(availableLines, borderLines(headerLinesShape)+t.headerLines) + headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines) if t.layout != layoutDefault { shift += headerLinesHeight shrink += headerLinesHeight @@ -1938,6 +2038,17 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { availableLines -= headerLinesHeight } + footerBorderHeight := 0 + if hasFooterWindow { + // Footer lines should not take all available lines + footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines) + shrink += footerBorderHeight + if t.layout != layoutReverse { + shift += footerBorderHeight + } + availableLines -= footerBorderHeight + } + // Set up list border hasListBorder := t.listBorderShape.Visible() innerWidth := width @@ -2041,13 +2152,8 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { width++ } - maxPreviewLines := availableLines - if t.wborder != nil { - maxPreviewLines -= t.wborder.Height() - } else { - maxPreviewLines -= util.Max(0, innerHeight-pheight-shrink) - } - pheight = util.Min(pheight, maxPreviewLines) + pheight = util.Constrain(pheight, minPreviewHeight, availableLines) + if previewOpts.position == posUp { innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight) t.window = t.tui.NewWindow( @@ -2210,7 +2316,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { case layoutDefault: btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight case layoutReverse: - btop = w.Top() - shrink + btop = w.Top() - shrink + footerBorderHeight case layoutReverseList: btop = w.Top() + w.Height() + headerBorderHeight } @@ -2238,7 +2344,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { var btop int if hasInputWindow && t.headerFirst { if t.layout == layoutReverse { - btop = w.Top() - shrink + btop = w.Top() - shrink + footerBorderHeight } else if t.layout == layoutReverseList { btop = w.Top() + w.Height() + inputBorderHeight } else { @@ -2294,12 +2400,31 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, headerLinesShape, tui.WindowHeader, 0) } + // Set up footer + if hasFooterWindow { + var btop int + if t.layout == layoutReverse { + btop = w.Top() + w.Height() + } else if t.layout == layoutReverseList { + btop = w.Top() - footerBorderHeight - headerLinesHeight + } else { + btop = w.Top() - footerBorderHeight + } + t.footerBorder = t.tui.NewWindow( + btop, + w.Left(), + w.Width(), + footerBorderHeight, tui.WindowFooter, tui.MakeBorderStyle(t.footerBorderShape, t.unicode), true) + t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0) + } + // Print border label t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false) 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) + t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false) } func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) { @@ -2343,7 +2468,7 @@ func (t *Terminal) move(y int, x int, clear bool) { case layoutDefault: y = h - y - 1 case layoutReverseList: - if t.window == t.inputWindow || t.window == t.headerWindow { + if !t.inListWindow() && t.window != t.headerLinesWindow { // From bottom to top y = h - y - 1 } else { @@ -2690,8 +2815,10 @@ func (t *Terminal) resizeIfNeeded() bool { if t.hasHeaderLinesWindow() { primaryHeaderLines -= t.headerLines } + // FIXME: Full redraw is triggered if there are too many lines in the header + // so that the header window cannot display all of them. needHeaderLinesWindow := t.hasHeaderLinesWindow() - if (t.headerBorderShape.Visible() || t.hasHeaderLinesWindow()) && + if (t.headerBorderShape.Visible() || needHeaderLinesWindow) && (t.headerWindow == nil && primaryHeaderLines > 0 || t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) || needHeaderLinesWindow && (t.headerLinesWindow == nil || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) || !needHeaderLinesWindow && t.headerLinesWindow != nil { @@ -2720,6 +2847,41 @@ func (t *Terminal) printHeader() { } } +func (t *Terminal) printFooter() { + if len(t.footer) == 0 { + return + } + indentSize := t.headerIndent(t.footerBorderShape) + indent := strings.Repeat(" ", indentSize) + max := util.Min(len(t.footer), t.footerWindow.Height()) + + // Wrapping is not supported for footer + wrap := t.wrap + t.wrap = false + t.withWindow(t.footerWindow, func() { + var state *ansiState + for idx, lineStr := range t.footer[:max] { + line := idx + if t.layout != layoutReverse { + line = max - idx - 1 + } + trimmed, colors, newState := extractColor(lineStr, state, nil) + state = newState + item := &Item{ + text: util.ToChars([]byte(trimmed)), + colors: colors} + + t.printHighlighted(Result{item: item}, + tui.ColFooter, tui.ColFooter, false, false, line, line, true, + func(markerClass) int { + t.footerWindow.Print(indent) + return indentSize + }, nil) + } + }) + t.wrap = wrap +} + func (t *Terminal) headerIndent(borderShape tui.BorderShape) int { indentSize := t.pointerLen + t.markerLen if t.listBorderShape.HasLeft() { @@ -2792,7 +2954,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap } func (t *Terminal) canSpanMultiLines() bool { - return t.multiLine || t.wrap || t.gap > 0 + return (t.multiLine || t.wrap || t.gap > 0) && t.inListWindow() } func (t *Terminal) renderBar(line int, barRange [2]int) { @@ -3767,6 +3929,7 @@ func (t *Terminal) printAll() { t.printPrompt() t.printInfo() t.printHeader() + t.printFooter() t.printPreview() } @@ -4515,6 +4678,7 @@ func (t *Terminal) Loop() error { t.reqBox.Set(reqPrompt, nil) t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqHeader, nil) + t.reqBox.Set(reqFooter, nil) if t.initDelay > 0 { go func() { timer := time.NewTimer(t.initDelay) @@ -4797,6 +4961,10 @@ func (t *Terminal) Loop() error { if !t.resizeIfNeeded() { t.printHeader() } + case reqFooter: + if !t.resizeIfNeeded() { + t.printFooter() + } case reqActivate: t.suppress = false if t.hasPreviewer() { @@ -4806,6 +4974,8 @@ func (t *Terminal) Loop() error { 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 reqRedrawFooterLabel: + t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true) case reqRedrawListLabel: t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true) case reqRedrawBorderLabel: @@ -4996,7 +5166,7 @@ func (t *Terminal) Loop() error { } updatePreviewWindow := func(forcePreview bool) { t.resizeWindows(forcePreview, false) - req(reqPrompt, reqList, reqInfo, reqHeader) + req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter) } toggle := func() bool { current := t.currentItem() @@ -5271,6 +5441,16 @@ func (t *Terminal) Loop() error { } else { req(reqHeader) } + case actChangeFooter, actTransformFooter: + footer := a.a + if a.t == actTransformFooter { + footer = t.captureLines(a.a) + } + if t.changeFooter(footer) { + req(reqFullRedraw) + } else { + req(reqFooter) + } case actChangeHeaderLabel, actTransformHeaderLabel: label := a.a if a.t == actTransformHeaderLabel { @@ -5279,6 +5459,14 @@ func (t *Terminal) Loop() error { t.headerLabelOpts.label = label t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) req(reqRedrawHeaderLabel) + case actChangeFooterLabel, actTransformFooterLabel: + label := a.a + if a.t == actTransformFooterLabel { + label = t.captureLine(a.a) + } + t.footerLabelOpts.label = label + t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) + req(reqRedrawFooterLabel) case actChangeInputLabel, actTransformInputLabel: label := a.a if a.t == actTransformInputLabel { diff --git a/src/tmux.go b/src/tmux.go index 87ea38cc..2e8499c5 100644 --- a/src/tmux.go +++ b/src/tmux.go @@ -11,10 +11,14 @@ func runTmux(args []string, opts *Options) (int, error) { // Prepare arguments fzf, rest := args[0], args[1:] args = []string{"--bind=ctrl-z:ignore"} - if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined { + if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) { // We append --border option at the end, because `--style=full:STYLE` // may have changed the default border style. - rest = append(rest, "--border") + if tui.DefaultBorderShape == tui.BorderRounded { + rest = append(rest, "--border=rounded") + } else { + rest = append(rest, "--border=sharp") + } } if opts.Tmux.border && opts.Margin == defaultMargin() { args = append(args, "--margin=0,1") diff --git a/src/tui/light.go b/src/tui/light.go index eb3de098..54abbe5c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -829,11 +829,14 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind case WindowHeader: w.fg = r.theme.Header.Color w.bg = r.theme.HeaderBg.Color + case WindowFooter: + w.fg = r.theme.Footer.Color + w.bg = r.theme.FooterBg.Color case WindowPreview: w.fg = r.theme.PreviewFg.Color w.bg = r.theme.PreviewBg.Color } - if erase && !w.bg.IsDefault() && w.border.shape != BorderNone { + if erase && !w.bg.IsDefault() && w.border.shape != BorderNone && w.height > 0 { // fzf --color bg:blue --border --padding 1,2 w.Erase() } @@ -889,6 +892,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { color = ColInputBorder case WindowHeader: color = ColHeaderBorder + case WindowFooter: + color = ColFooterBorder case WindowPreview: color = ColPreviewBorder } @@ -914,6 +919,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) { color = ColInputBorder case WindowHeader: color = ColHeaderBorder + case WindowFooter: + color = ColFooterBorder case WindowPreview: color = ColPreviewBorder } @@ -941,6 +948,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { color = ColInputBorder case WindowHeader: color = ColHeaderBorder + case WindowFooter: + color = ColFooterBorder case WindowPreview: color = ColPreviewBorder } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index a2630463..762768da 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -600,6 +600,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, normal = ColNormal case WindowHeader: normal = ColHeader + case WindowFooter: + normal = ColFooter case WindowInput: normal = ColInput case WindowPreview: @@ -865,6 +867,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) { style = ColListBorder.style() case WindowHeader: style = ColHeaderBorder.style() + case WindowFooter: + style = ColFooterBorder.style() case WindowInput: style = ColInputBorder.style() case WindowPreview: diff --git a/src/tui/tui.go b/src/tui/tui.go index 06dbccb0..c8844753 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -359,6 +359,10 @@ type ColorTheme struct { HeaderBg ColorAttr HeaderBorder ColorAttr HeaderLabel ColorAttr + Footer ColorAttr + FooterBg ColorAttr + FooterBorder ColorAttr + FooterLabel ColorAttr Separator ColorAttr Scrollbar ColorAttr Border ColorAttr @@ -612,6 +616,7 @@ const ( WindowPreview WindowInput WindowHeader + WindowFooter ) type Renderer interface { @@ -720,6 +725,9 @@ var ( ColHeader ColorPair ColHeaderBorder ColorPair ColHeaderLabel ColorPair + ColFooter ColorPair + ColFooterBorder ColorPair + ColFooterLabel ColorPair ColSeparator ColorPair ColScrollbar ColorPair ColGapLine ColorPair @@ -758,6 +766,7 @@ func EmptyTheme() *ColorTheme { Cursor: ColorAttr{colUndefined, AttrUndefined}, Marker: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined}, + Footer: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined}, ListLabel: ColorAttr{colUndefined, AttrUndefined}, @@ -778,6 +787,9 @@ func EmptyTheme() *ColorTheme { HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, + FooterBg: ColorAttr{colUndefined, AttrUndefined}, + FooterBorder: ColorAttr{colUndefined, AttrUndefined}, + FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, } @@ -825,6 +837,9 @@ func NoColorTheme() *ColorTheme { HeaderBg: ColorAttr{colDefault, AttrUndefined}, HeaderBorder: ColorAttr{colDefault, AttrUndefined}, HeaderLabel: ColorAttr{colDefault, AttrUndefined}, + FooterBg: ColorAttr{colDefault, AttrUndefined}, + FooterBorder: ColorAttr{colDefault, AttrUndefined}, + FooterLabel: ColorAttr{colDefault, AttrUndefined}, GapLine: ColorAttr{colDefault, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, } @@ -852,6 +867,7 @@ func init() { Cursor: ColorAttr{colRed, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined}, + Footer: ColorAttr{colCyan, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined}, Ghost: ColorAttr{colUndefined, Dim}, @@ -869,6 +885,12 @@ 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}, + FooterBg: ColorAttr{colUndefined, AttrUndefined}, + FooterBorder: ColorAttr{colUndefined, AttrUndefined}, + FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, } @@ -893,6 +915,7 @@ func init() { Cursor: ColorAttr{161, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined}, Header: ColorAttr{109, AttrUndefined}, + Footer: ColorAttr{109, AttrUndefined}, Border: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined}, Ghost: ColorAttr{colUndefined, Dim}, @@ -910,6 +933,12 @@ 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}, + FooterBg: ColorAttr{colUndefined, AttrUndefined}, + FooterBorder: ColorAttr{colUndefined, AttrUndefined}, + FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, } @@ -934,6 +963,7 @@ func init() { Cursor: ColorAttr{161, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined}, Header: ColorAttr{31, AttrUndefined}, + Footer: ColorAttr{31, AttrUndefined}, Border: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined}, Ghost: ColorAttr{colUndefined, Dim}, @@ -954,6 +984,9 @@ func init() { HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, + FooterBg: ColorAttr{colUndefined, AttrUndefined}, + FooterBorder: ColorAttr{colUndefined, AttrUndefined}, + FooterLabel: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined}, } @@ -989,6 +1022,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Marker = o(baseTheme.Marker, theme.Marker) theme.Header = o(baseTheme.Header, theme.Header) + theme.Footer = o(baseTheme.Footer, theme.Footer) theme.Border = o(baseTheme.Border, theme.Border) theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) @@ -1042,6 +1076,10 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp theme.HeaderBorder = o(theme.Border, theme.HeaderBorder) theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel) + theme.FooterBg = o(theme.Bg, theme.FooterBg) + theme.FooterBorder = o(theme.Border, theme.FooterBorder) + theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel) + initPalette(theme) } @@ -1095,6 +1133,9 @@ func initPalette(theme *ColorTheme) { ColHeader = pair(theme.Header, theme.HeaderBg) ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg) ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg) + ColFooter = pair(theme.Footer, theme.FooterBg) + ColFooterBorder = pair(theme.FooterBorder, theme.FooterBg) + ColFooterLabel = pair(theme.FooterLabel, theme.FooterBg) } func runeWidth(r rune) int { diff --git a/src/util/util.go b/src/util/util.go index c8301363..f11a0887 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -97,24 +97,12 @@ func Min32(first int32, second int32) int32 { // Constrain32 limits the given 32-bit integer with the upper and lower bounds func Constrain32(val int32, min int32, max int32) int32 { - if val < min { - return min - } - if val > max { - return max - } - return val + return Max32(Min32(val, max), min) } // Constrain limits the given integer with the upper and lower bounds func Constrain(val int, min int, max int) int { - if val < min { - return min - } - if val > max { - return max - } - return val + return Max(Min(val, max), min) } func AsUint16(val int) uint16 { diff --git a/test/test_layout.rb b/test/test_layout.rb index 152abb8c..f8b61bbc 100644 --- a/test/test_layout.rb +++ b/test/test_layout.rb @@ -979,6 +979,126 @@ class TestLayout < TestInteractive end end + def test_layout_default_with_footer + prefix = %[ + seq 3 | #{FZF} --no-list-border --height ~100% \ + --border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \ + --header-label HEAD --header-label-pos 3:bottom \ + --bind 'space:transform-footer-label(echo foot)+change-header-label(head)' + ].strip + ' ' + suffixes = [ + %(), + %[--header "$(seq 101 102)"], + %[--header "$(seq 101 102)" --header-first], + %[--header "$(seq 101 102)" --header-lines 2], + %[--header "$(seq 101 102)" --header-lines 2 --header-first], + %[--header "$(seq 101 102)" --header-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-first], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input], + %[--header "$(seq 101 102)" --footer-border sharp --input-border line], + %[--header "$(seq 101 102)" --style full:sharp --header-first] + ] + output = <<~BLOCK + ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌───────── ┌──────── ┌──────── ┌───────── + │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ ┌─FOOT─ │ ┌─FOOT── + │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ │ 201 │ │ 201 + │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT── │ ──FOOT─ │ │ 202 │ │ 202 + │ 3 │ 3 │ 3 │ > 3 │ > 3 │ 3 │ 3 │ > 3 │ > 3 │ > 3 │ > 3 │ └────── │ └─────── + │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ ┌────── │ ┌────── │ ┌─────── │ ┌────── │ 3 │ ┌─────── + │ > 1 │ > 1 │ > 1 │ 1 │ 1 │ > 1 │ > 1 │ │ 2 │ │ 2 │ │ 2 │ │ 2 │ 2 │ │ 3 + │ 3/3 ─ │ 101 │ 3/3 ─ │ 101 │ 1/1 ─ │ ┌────── │ 3/3 ─ │ │ 1 │ │ 1 │ │ 1 │ │ 1 │ > 1 │ │ 2 + │ > │ 102 │ > │ 102 │ > │ │ 101 │ > │ │ 101 │ └────── │ └─────── │ └────── │ 101 │ │ > 1 + └──────── │ 3/3 ─ │ 101 │ 1/1 ─ │ 101 │ │ 102 │ ┌────── │ │ 102 │ ┌────── │ ┌─────── │ ┌────── │ 102 │ └─────── + │ > │ 102 │ > │ 102 │ └─HEAD─ │ │ 101 │ └─HEAD─ │ │ 101 │ │ 1/1 │ │ 101 │ ─────── │ ┌─────── + └──────── └──────── └──────── └──────── │ 3/3 ─ │ │ 102 │ 1/1 ─ │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ > + │ > │ └─HEAD─ │ > │ └─HEAD─ │ └─────── │ └─HEAD─ │ > │ └─────── + └──────── └──────── └──────── │ 1/1 ─ │ ┌─────── └──────── └──────── │ ┌─────── + │ > │ │ 101 │ │ 101 + └──────── │ │ 102 │ │ 102 + │ └─HEAD── │ └─HEAD── + └───────── └───────── + BLOCK + + expects = [] + output.each_line.first.scan(/\S+/) do + offset = Regexp.last_match.offset(0) + expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n") + end + + suffixes.zip(expects).each do |suffix, block| + tmux.send_keys(prefix + suffix, :Enter) + tmux.until { assert_block(block, it) } + tmux.send_keys :Space + tmux.until { assert_block(block.downcase, it) } + + teardown + setup + end + end + + def test_layout_reverse_list_with_footer + prefix = %[ + seq 3 | #{FZF} --layout reverse-list --no-list-border --height ~100% \ + --border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \ + --header-label HEAD --header-label-pos 3:bottom \ + --bind 'space:transform-footer-label(echo foot)+change-header-label(head)' + ].strip + ' ' + suffixes = [ + %(), + %[--header "$(seq 101 102)"], + %[--header "$(seq 101 102)" --header-first], + %[--header "$(seq 101 102)" --header-lines 2], + %[--header "$(seq 101 102)" --header-lines 2 --header-first], + %[--header "$(seq 101 102)" --header-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-first], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp], + %[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input], + %[--header "$(seq 101 102)" --footer-border sharp --input-border line], + %[--header "$(seq 101 102)" --style full:sharp --header-first] + ] + output = <<~BLOCK + ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌───────── ┌──────── ┌──────── ┌───────── + │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ 201 │ ┌─FOOT─ │ ┌─FOOT── + │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ 202 │ │ 201 │ │ 201 + │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT─ │ ──FOOT── │ ──FOOT─ │ │ 202 │ │ 202 + │ > 1 │ > 1 │ > 1 │ 1 │ 1 │ > 1 │ > 1 │ 1 │ ┌────── │ ┌─────── │ ┌────── │ └────── │ └─────── + │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ │ 1 │ │ 1 │ │ 1 │ > 1 │ ┌─────── + │ 3 │ 3 │ 3 │ > 3 │ > 3 │ 3 │ 3 │ > 3 │ │ 2 │ │ 2 │ │ 2 │ 2 │ │ > 1 + │ 3/3 ─ │ 101 │ 3/3 ─ │ 101 │ 1/1 ─ │ ┌────── │ 3/3 ─ │ ┌────── │ └────── │ └─────── │ └────── │ 3 │ │ 2 + │ > │ 102 │ > │ 102 │ > │ │ 101 │ > │ │ 101 │ > 3 │ > 3 │ > 3 │ 101 │ │ 3 + └──────── │ 3/3 ─ │ 101 │ 1/1 ─ │ 101 │ │ 102 │ ┌────── │ │ 102 │ ┌────── │ ┌─────── │ ┌────── │ 102 │ └─────── + │ > │ 102 │ > │ 102 │ └─HEAD─ │ │ 101 │ └─HEAD─ │ │ 101 │ │ 1/1 │ │ 101 │ ─────── │ ┌─────── + └──────── └──────── └──────── └──────── │ 3/3 ─ │ │ 102 │ 1/1 ─ │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ > + │ > │ └─HEAD─ │ > │ └─HEAD─ │ └─────── │ └─HEAD─ │ > │ └─────── + └──────── └──────── └──────── │ 1/1 ─ │ ┌─────── └──────── └──────── │ ┌─────── + │ > │ │ 101 │ │ 101 + └──────── │ │ 102 │ │ 102 + │ └─HEAD── │ └─HEAD── + └───────── └───────── + BLOCK + + expects = [] + output.each_line.first.scan(/\S+/) do + offset = Regexp.last_match.offset(0) + expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n") + end + + suffixes.zip(expects).each do |suffix, block| + tmux.send_keys(prefix + suffix, :Enter) + tmux.until { assert_block(block, it) } + tmux.send_keys :Space + tmux.until { assert_block(block.downcase, it) } + + teardown + setup + end + end + def test_change_header_and_label_at_once tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter block = <<~BLOCK @@ -1033,4 +1153,79 @@ class TestLayout < TestInteractive BLOCK tmux.until { assert_block(block, it) } end + + def test_combinations + skip unless ENV['LONGTEST'] + + base = [ + '--pointer=@', + '--exact', + '--query=123', + '--header="$(seq 101 103)"', + '--header-lines=3', + '--footer "$(seq 201 203)"', + '--preview "echo foobar"' + ] + options = [ + ['--separator==', '--no-separator'], + ['--info=default', '--info=inline', '--info=inline-right'], + ['--no-input-border', '--input-border'], + ['--no-header-border', '--header-border=none', '--header-border'], + ['--no-header-lines-border', '--header-lines-border'], + ['--no-footer-border', '--footer-border'], + ['--no-list-border', '--list-border'], + ['--preview-window=right', '--preview-window=up', '--preview-window=down', '--preview-window=left'], + ['--header-first', '--no-header-first'], + ['--layout=default', '--layout=reverse', '--layout=reverse-list'] + ] + # Combination of all options + combinations = options[0].product(*options.drop(1)) + combinations.each_with_index do |combination, index| + opts = base + combination + command = %(seq 1001 2000 | #{FZF} #{opts.join(' ')}) + puts "# #{index + 1}/#{combinations.length}\n#{command}" + tmux.send_keys command, :Enter + tmux.until do |lines| + layout = combination.find { it.start_with?('--layout=') }.split('=').last + header_first = combination.include?('--header-first') + + # Input + input = lines.index { it.include?('> 123') } + assert(input) + + # Info + info = lines.index { it.include?('11/997') } + assert(info) + + assert(layout == 'reverse' ? input <= info : input >= info) + + # List + item1 = lines.index { it.include?('1230') } + item2 = lines.index { it.include?('1231') } + assert_equal(item1, layout == 'default' ? item2 + 1 : item2 - 1) + + # Preview + assert(lines.any? { it.include?('foobar') }) + + # Header + header1 = lines.index { it.include?('101') } + header2 = lines.index { it.include?('102') } + assert_equal(header2, header1 + 1) + assert((layout == 'reverse') == header_first ? input > header1 : input < header1) + + # Footer + footer1 = lines.index { it.include?('201') } + footer2 = lines.index { it.include?('202') } + assert_equal(footer2, footer1 + 1) + assert(layout == 'reverse' ? footer1 > item2 : footer1 < item2) + + # Header lines + hline1 = lines.index { it.include?('1001') } + hline2 = lines.index { it.include?('1002') } + assert_equal(hline1, layout == 'default' ? hline2 + 1 : hline2 - 1) + assert(layout == 'reverse' ? hline1 > header1 : hline1 < header1) + end + tmux.send_keys :Enter + end + end end