Add footer

Options:
  --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)

The default border type for footer is 'line', which draws a single
separator between the footer and the list. It changes its position
depending on `--layout`, so you don't have to manually switch between
'top' and 'bottom'

The 'line' style is now supported by other border types as well.
`--list-border` is the only exception.
This commit is contained in:
Junegunn Choi
2025-06-10 00:26:57 +09:00
parent 39db026161
commit 3b68dcdd81
11 changed files with 721 additions and 182 deletions

View File

@@ -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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 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 .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -503,6 +503,8 @@ Draw border around the finder
.br .br
.BR vertical " Vertical lines on each side of the finder" .BR vertical " Vertical lines on each side of the finder"
.br .br
.BR line " Single line border (position automatically determined)"
.br
.BR top " (up)" .BR top " (up)"
.br .br
.BR bottom " (down)" .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 2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly
rendered, set \fB\-\-no\-unicode\fR. rendered, set \fB\-\-no\-unicode\fR.
\fBline\fR style draws a single separator line at the top when \fB\-\-height\fR
is used.
.TP .TP
.BI "\-\-border\-label" [=LABEL] .BI "\-\-border\-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the 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 .TP
.BI "\-\-list\-border" [=STYLE] .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 .TP
.BI "\-\-list\-label" [=LABEL] .BI "\-\-list\-label" [=LABEL]
@@ -748,7 +754,8 @@ actions are affected:
\fBkill\-word\fR \fBkill\-word\fR
.TP .TP
.BI "\-\-input\-border" [=STYLE] .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 .TP
.BI "\-\-input\-label" [=LABEL] .BI "\-\-input\-label" [=LABEL]
@@ -848,8 +855,7 @@ e.g.
.TP .TP
.BI "\-\-preview\-border" [=STYLE] .BI "\-\-preview\-border" [=STYLE]
Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other Short for \fB\-\-preview\-window=border\-STYLE\fR. \fBline\fR style draws
styles, \fBline\fR style is also supported for preview border, which draws
a single separator line between the preview window and the rest of the a single separator line between the preview window and the rest of the
interface. 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. (\fB\-\-header\-lines\fR) are present, this applies only to the normal header.
.TP .TP
.BI "\-\-header\-border" [=STYLE] .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 .TP
.BI "\-\-header\-label" [=LABEL] .BI "\-\-header\-label" [=LABEL]
@@ -1019,7 +1026,30 @@ Position of the header label
.BI "\-\-header\-lines\-border" [=STYLE] .BI "\-\-header\-lines\-border" [=STYLE]
Display header from \fB--header\-lines\fR with a separate border. Pass 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 \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 .SS SCRIPTING
.TP .TP

View File

@@ -29,128 +29,132 @@ func _() {
_ = x[actChangeBorderLabel-18] _ = x[actChangeBorderLabel-18]
_ = x[actChangeGhost-19] _ = x[actChangeGhost-19]
_ = x[actChangeHeader-20] _ = x[actChangeHeader-20]
_ = x[actChangeHeaderLabel-21] _ = x[actChangeFooter-21]
_ = x[actChangeInputLabel-22] _ = x[actChangeHeaderLabel-22]
_ = x[actChangeListLabel-23] _ = x[actChangeFooterLabel-23]
_ = x[actChangeMulti-24] _ = x[actChangeInputLabel-24]
_ = x[actChangeNth-25] _ = x[actChangeListLabel-25]
_ = x[actChangePointer-26] _ = x[actChangeMulti-26]
_ = x[actChangePreview-27] _ = x[actChangeNth-27]
_ = x[actChangePreviewLabel-28] _ = x[actChangePointer-28]
_ = x[actChangePreviewWindow-29] _ = x[actChangePreview-29]
_ = x[actChangePrompt-30] _ = x[actChangePreviewLabel-30]
_ = x[actChangeQuery-31] _ = x[actChangePreviewWindow-31]
_ = x[actClearScreen-32] _ = x[actChangePrompt-32]
_ = x[actClearQuery-33] _ = x[actChangeQuery-33]
_ = x[actClearSelection-34] _ = x[actClearScreen-34]
_ = x[actClose-35] _ = x[actClearQuery-35]
_ = x[actDeleteChar-36] _ = x[actClearSelection-36]
_ = x[actDeleteCharEof-37] _ = x[actClose-37]
_ = x[actEndOfLine-38] _ = x[actDeleteChar-38]
_ = x[actFatal-39] _ = x[actDeleteCharEof-39]
_ = x[actForwardChar-40] _ = x[actEndOfLine-40]
_ = x[actForwardWord-41] _ = x[actFatal-41]
_ = x[actKillLine-42] _ = x[actForwardChar-42]
_ = x[actKillWord-43] _ = x[actForwardWord-43]
_ = x[actUnixLineDiscard-44] _ = x[actKillLine-44]
_ = x[actUnixWordRubout-45] _ = x[actKillWord-45]
_ = x[actYank-46] _ = x[actUnixLineDiscard-46]
_ = x[actBackwardKillWord-47] _ = x[actUnixWordRubout-47]
_ = x[actSelectAll-48] _ = x[actYank-48]
_ = x[actDeselectAll-49] _ = x[actBackwardKillWord-49]
_ = x[actToggle-50] _ = x[actSelectAll-50]
_ = x[actToggleSearch-51] _ = x[actDeselectAll-51]
_ = x[actToggleAll-52] _ = x[actToggle-52]
_ = x[actToggleDown-53] _ = x[actToggleSearch-53]
_ = x[actToggleUp-54] _ = x[actToggleAll-54]
_ = x[actToggleIn-55] _ = x[actToggleDown-55]
_ = x[actToggleOut-56] _ = x[actToggleUp-56]
_ = x[actToggleTrack-57] _ = x[actToggleIn-57]
_ = x[actToggleTrackCurrent-58] _ = x[actToggleOut-58]
_ = x[actToggleHeader-59] _ = x[actToggleTrack-59]
_ = x[actToggleWrap-60] _ = x[actToggleTrackCurrent-60]
_ = x[actToggleMultiLine-61] _ = x[actToggleHeader-61]
_ = x[actToggleHscroll-62] _ = x[actToggleWrap-62]
_ = x[actTrackCurrent-63] _ = x[actToggleMultiLine-63]
_ = x[actToggleInput-64] _ = x[actToggleHscroll-64]
_ = x[actHideInput-65] _ = x[actTrackCurrent-65]
_ = x[actShowInput-66] _ = x[actToggleInput-66]
_ = x[actUntrackCurrent-67] _ = x[actHideInput-67]
_ = x[actDown-68] _ = x[actShowInput-68]
_ = x[actUp-69] _ = x[actUntrackCurrent-69]
_ = x[actPageUp-70] _ = x[actDown-70]
_ = x[actPageDown-71] _ = x[actUp-71]
_ = x[actPosition-72] _ = x[actPageUp-72]
_ = x[actHalfPageUp-73] _ = x[actPageDown-73]
_ = x[actHalfPageDown-74] _ = x[actPosition-74]
_ = x[actOffsetUp-75] _ = x[actHalfPageUp-75]
_ = x[actOffsetDown-76] _ = x[actHalfPageDown-76]
_ = x[actOffsetMiddle-77] _ = x[actOffsetUp-77]
_ = x[actJump-78] _ = x[actOffsetDown-78]
_ = x[actJumpAccept-79] _ = x[actOffsetMiddle-79]
_ = x[actPrintQuery-80] _ = x[actJump-80]
_ = x[actRefreshPreview-81] _ = x[actJumpAccept-81]
_ = x[actReplaceQuery-82] _ = x[actPrintQuery-82]
_ = x[actToggleSort-83] _ = x[actRefreshPreview-83]
_ = x[actShowPreview-84] _ = x[actReplaceQuery-84]
_ = x[actHidePreview-85] _ = x[actToggleSort-85]
_ = x[actTogglePreview-86] _ = x[actShowPreview-86]
_ = x[actTogglePreviewWrap-87] _ = x[actHidePreview-87]
_ = x[actTransform-88] _ = x[actTogglePreview-88]
_ = x[actTransformBorderLabel-89] _ = x[actTogglePreviewWrap-89]
_ = x[actTransformGhost-90] _ = x[actTransform-90]
_ = x[actTransformHeader-91] _ = x[actTransformBorderLabel-91]
_ = x[actTransformHeaderLabel-92] _ = x[actTransformGhost-92]
_ = x[actTransformInputLabel-93] _ = x[actTransformHeader-93]
_ = x[actTransformListLabel-94] _ = x[actTransformFooter-94]
_ = x[actTransformNth-95] _ = x[actTransformHeaderLabel-95]
_ = x[actTransformPointer-96] _ = x[actTransformFooterLabel-96]
_ = x[actTransformPreviewLabel-97] _ = x[actTransformInputLabel-97]
_ = x[actTransformPrompt-98] _ = x[actTransformListLabel-98]
_ = x[actTransformQuery-99] _ = x[actTransformNth-99]
_ = x[actTransformSearch-100] _ = x[actTransformPointer-100]
_ = x[actSearch-101] _ = x[actTransformPreviewLabel-101]
_ = x[actPreview-102] _ = x[actTransformPrompt-102]
_ = x[actPreviewTop-103] _ = x[actTransformQuery-103]
_ = x[actPreviewBottom-104] _ = x[actTransformSearch-104]
_ = x[actPreviewUp-105] _ = x[actSearch-105]
_ = x[actPreviewDown-106] _ = x[actPreview-106]
_ = x[actPreviewPageUp-107] _ = x[actPreviewTop-107]
_ = x[actPreviewPageDown-108] _ = x[actPreviewBottom-108]
_ = x[actPreviewHalfPageUp-109] _ = x[actPreviewUp-109]
_ = x[actPreviewHalfPageDown-110] _ = x[actPreviewDown-110]
_ = x[actPrevHistory-111] _ = x[actPreviewPageUp-111]
_ = x[actPrevSelected-112] _ = x[actPreviewPageDown-112]
_ = x[actPrint-113] _ = x[actPreviewHalfPageUp-113]
_ = x[actPut-114] _ = x[actPreviewHalfPageDown-114]
_ = x[actNextHistory-115] _ = x[actPrevHistory-115]
_ = x[actNextSelected-116] _ = x[actPrevSelected-116]
_ = x[actExecute-117] _ = x[actPrint-117]
_ = x[actExecuteSilent-118] _ = x[actPut-118]
_ = x[actExecuteMulti-119] _ = x[actNextHistory-119]
_ = x[actSigStop-120] _ = x[actNextSelected-120]
_ = x[actFirst-121] _ = x[actExecute-121]
_ = x[actLast-122] _ = x[actExecuteSilent-122]
_ = x[actReload-123] _ = x[actExecuteMulti-123]
_ = x[actReloadSync-124] _ = x[actSigStop-124]
_ = x[actDisableSearch-125] _ = x[actFirst-125]
_ = x[actEnableSearch-126] _ = x[actLast-126]
_ = x[actSelect-127] _ = x[actReload-127]
_ = x[actDeselect-128] _ = x[actReloadSync-128]
_ = x[actUnbind-129] _ = x[actDisableSearch-129]
_ = x[actRebind-130] _ = x[actEnableSearch-130]
_ = x[actToggleBind-131] _ = x[actSelect-131]
_ = x[actBecome-132] _ = x[actDeselect-132]
_ = x[actShowHeader-133] _ = x[actUnbind-133]
_ = x[actHideHeader-134] _ = x[actRebind-134]
_ = x[actBell-135] _ = x[actToggleBind-135]
_ = x[actExclude-136] _ = x[actBecome-136]
_ = x[actExcludeMulti-137] _ = 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 { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -39,7 +39,7 @@ func (r revision) compatible(other revision) bool {
// Run starts fzf // Run starts fzf
func Run(opts *Options) (int, error) { func Run(opts *Options) (int, error) {
if opts.Filter == nil { 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) return runTmux(os.Args, opts)
} }

View File

@@ -83,7 +83,7 @@ Usage: fzf [options]
--padding=PADDING Padding inside border (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)
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [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=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -140,7 +140,7 @@ Usage: fzf [options]
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section --input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [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=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label --input-label-pos=COL Position of the input label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -168,7 +168,7 @@ Usage: fzf [options]
--header-first Print header before the prompt line --header-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section --header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [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] --header-lines-border[=STYLE]
Display header from --header-lines with a separate border. Display header from --header-lines with a separate border.
Pass 'none' to still separate it but without a border. Pass 'none' to still separate it but without a border.
@@ -178,6 +178,17 @@ Usage: fzf [options]
NEGATIVE_INTEGER: columns from right][:bottom] NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center) (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 SCRIPTING
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
@@ -599,6 +610,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Footer []string
Gap int Gap int
GapLine *string GapLine *string
Ellipsis *string Ellipsis *string
@@ -610,8 +622,10 @@ type Options struct {
InputBorderShape tui.BorderShape InputBorderShape tui.BorderShape
HeaderBorderShape tui.BorderShape HeaderBorderShape tui.BorderShape
HeaderLinesShape tui.BorderShape HeaderLinesShape tui.BorderShape
FooterBorderShape tui.BorderShape
InputLabel labelOpts InputLabel labelOpts
HeaderLabel labelOpts HeaderLabel labelOpts
FooterLabel labelOpts
BorderLabel labelOpts BorderLabel labelOpts
ListLabel labelOpts ListLabel labelOpts
PreviewLabel labelOpts PreviewLabel labelOpts
@@ -716,6 +730,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Footer: make([]string, 0),
Gap: 0, Gap: 0,
Ellipsis: nil, Ellipsis: nil,
Scrollbar: nil, Scrollbar: nil,
@@ -880,12 +895,9 @@ func parseAlgo(str string) (algo.Algo, error) {
return nil, errors.New("invalid algorithm (expected: v1 or v2)") 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 { switch str {
case "line": case "line":
if !allowLine {
return tui.BorderNone, errors.New("'line' is only allowed for preview border")
}
return tui.BorderLine, nil return tui.BorderLine, nil
case "rounded": case "rounded":
return tui.BorderRounded, nil return tui.BorderRounded, nil
@@ -1348,6 +1360,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.HeaderBorder) mergeAttr(&theme.HeaderBorder)
case "header-label": case "header-label":
mergeAttr(&theme.HeaderLabel) mergeAttr(&theme.HeaderLabel)
case "footer-border":
mergeAttr(&theme.FooterBorder)
case "footer-label":
mergeAttr(&theme.FooterLabel)
case "spinner": case "spinner":
mergeAttr(&theme.Spinner) mergeAttr(&theme.Spinner)
case "info": case "info":
@@ -1360,6 +1376,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.Header) mergeAttr(&theme.Header)
case "header-bg": case "header-bg":
mergeAttr(&theme.HeaderBg) mergeAttr(&theme.HeaderBg)
case "footer", "footer-fg":
mergeAttr(&theme.Footer)
case "footer-bg":
mergeAttr(&theme.FooterBg)
case "gap-line": case "gap-line":
mergeAttr(&theme.GapLine) mergeAttr(&theme.GapLine)
default: default:
@@ -1415,7 +1435,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( 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("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1800,6 +1820,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-header": case "change-header":
return actChangeHeader return actChangeHeader
case "change-footer":
return actChangeFooter
case "change-list-label": case "change-list-label":
return actChangeListLabel return actChangeListLabel
case "change-border-label": case "change-border-label":
@@ -1810,6 +1832,8 @@ func isExecuteAction(str string) actionType {
return actChangeInputLabel return actChangeInputLabel
case "change-header-label": case "change-header-label":
return actChangeHeaderLabel return actChangeHeaderLabel
case "change-footer-label":
return actChangeFooterLabel
case "change-ghost": case "change-ghost":
return actChangeGhost return actChangeGhost
case "change-pointer": case "change-pointer":
@@ -1850,6 +1874,10 @@ func isExecuteAction(str string) actionType {
return actTransformInputLabel return actTransformInputLabel
case "transform-header-label": case "transform-header-label":
return actTransformHeaderLabel return actTransformHeaderLabel
case "transform-footer-label":
return actTransformFooterLabel
case "transform-footer":
return actTransformFooter
case "transform-header": case "transform-header":
return actTransformHeader return actTransformHeader
case "transform-ghost": 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 { if opts.HeaderLines, err = nextInt("number of header lines required"); err != nil {
return err 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": case "--header-first":
opts.HeaderFirst = true opts.HeaderFirst = true
case "--no-header-first": case "--no-header-first":
@@ -2773,7 +2809,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Preview.border = tui.BorderNone opts.Preview.border = tui.BorderNone
case "--preview-border": case "--preview-border":
hasArg, arg := optionalNextString() 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 return err
} }
case "--height": case "--height":
@@ -2812,14 +2848,17 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.BorderShape = tui.BorderNone opts.BorderShape = tui.BorderNone
case "--border": case "--border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.BorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--list-border": case "--list-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.ListBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.ListBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
if opts.ListBorderShape == tui.BorderLine {
return errors.New("list border cannot be 'line'")
}
case "--no-list-border": case "--no-list-border":
opts.ListBorderShape = tui.BorderNone opts.ListBorderShape = tui.BorderNone
case "--no-list-label": case "--no-list-label":
@@ -2841,14 +2880,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderBorderShape = tui.BorderNone opts.HeaderBorderShape = tui.BorderNone
case "--header-border": case "--header-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-header-lines-border": case "--no-header-lines-border":
opts.HeaderLinesShape = tui.BorderNone opts.HeaderLinesShape = tui.BorderNone
case "--header-lines-border": case "--header-lines-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-header-label": 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 { if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil {
return err 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": case "--no-input-border":
opts.InputBorderShape = tui.BorderNone opts.InputBorderShape = tui.BorderNone
case "--input-border": case "--input-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.InputBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.InputBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-input-label": case "--no-input-label":
@@ -3077,6 +3137,7 @@ func applyPreset(opts *Options, preset string) error {
opts.ListBorderShape = tui.BorderUndefined opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderUndefined
opts.Preview.border = defaultBorderShape opts.Preview.border = defaultBorderShape
opts.Preview.info = true opts.Preview.info = true
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
@@ -3088,6 +3149,7 @@ func applyPreset(opts *Options, preset string) error {
opts.ListBorderShape = tui.BorderUndefined opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderLine
opts.Preview.border = tui.BorderLine opts.Preview.border = tui.BorderLine
opts.Preview.info = false opts.Preview.info = false
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
@@ -3103,16 +3165,22 @@ func applyPreset(opts *Options, preset string) error {
} }
if len(tokens) == 2 && len(tokens[1]) > 0 { if len(tokens) == 2 && len(tokens[1]) > 0 {
var err error var err error
defaultBorderShape, err = parseBorder(tokens[1], false, false) defaultBorderShape, err = parseBorder(tokens[1], false)
if err != nil { if err != nil {
return err return err
} }
} }
opts.ListBorderShape = defaultBorderShape if defaultBorderShape != tui.BorderLine {
opts.ListBorderShape = defaultBorderShape
}
opts.InputBorderShape = defaultBorderShape opts.InputBorderShape = defaultBorderShape
opts.HeaderBorderShape = defaultBorderShape opts.HeaderBorderShape = defaultBorderShape
opts.FooterBorderShape = defaultBorderShape
opts.Preview.border = defaultBorderShape opts.Preview.border = defaultBorderShape
if defaultBorderShape == tui.BorderLine {
opts.BorderShape = defaultBorderShape
}
opts.Preview.info = true opts.Preview.info = true
opts.InfoStyle = infoInlineRight opts.InfoStyle = infoInlineRight
opts.Theme.Gutter = tui.NewColorAttr() opts.Theme.Gutter = tui.NewColorAttr()
@@ -3185,6 +3253,10 @@ func noSeparatorLine(style infoStyle, separator bool) bool {
return false 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 { func (opts *Options) noSeparatorLine() bool {
if opts.Inputless { if opts.Inputless {
return true return true
@@ -3216,6 +3288,10 @@ func postProcessOptions(opts *Options) error {
opts.HeaderBorderShape = tui.BorderNone opts.HeaderBorderShape = tui.BorderNone
} }
if opts.FooterBorderShape == tui.BorderUndefined {
opts.FooterBorderShape = tui.BorderLine
}
if opts.HeaderLinesShape == tui.BorderNone { if opts.HeaderLinesShape == tui.BorderNone {
opts.HeaderLinesShape = tui.BorderPhantom opts.HeaderLinesShape = tui.BorderPhantom
} }

View File

@@ -180,14 +180,18 @@ type itemLine struct {
other bool 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) { 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} t.prevLines[line] = itemLine{valid: true, firstLine: line, empty: true}
} }
} }
func (t *Terminal) markOtherLine(line int) { 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} t.prevLines[line] = itemLine{valid: true, firstLine: line, other: true}
} }
} }
@@ -254,6 +258,9 @@ type Terminal struct {
headerLabel labelPrinter headerLabel labelPrinter
headerLabelLen int headerLabelLen int
headerLabelOpts labelOpts headerLabelOpts labelOpts
footerLabel labelPrinter
footerLabelLen int
footerLabelOpts labelOpts
pointer string pointer string
pointerLen int pointerLen int
pointerEmpty string pointerEmpty string
@@ -301,6 +308,7 @@ type Terminal struct {
headerLines int headerLines int
header []string header []string
header0 []string header0 []string
footer []string
ellipsis string ellipsis string
scrollbar string scrollbar string
previewScrollbar string previewScrollbar string
@@ -322,6 +330,7 @@ type Terminal struct {
inputBorderShape tui.BorderShape inputBorderShape tui.BorderShape
headerBorderShape tui.BorderShape headerBorderShape tui.BorderShape
headerLinesShape tui.BorderShape headerLinesShape tui.BorderShape
footerBorderShape tui.BorderShape
listLabel labelPrinter listLabel labelPrinter
listLabelLen int listLabelLen int
listLabelOpts labelOpts listLabelOpts labelOpts
@@ -337,6 +346,8 @@ type Terminal struct {
headerBorder tui.Window headerBorder tui.Window
headerLinesWindow tui.Window headerLinesWindow tui.Window
headerLinesBorder tui.Window headerLinesBorder tui.Window
footerWindow tui.Window
footerBorder tui.Window
wborder tui.Window wborder tui.Window
pborder tui.Window pborder tui.Window
pwindow tui.Window pwindow tui.Window
@@ -426,6 +437,7 @@ const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
reqInfo reqInfo
reqHeader reqHeader
reqFooter
reqList reqList
reqJump reqJump
reqActivate reqActivate
@@ -434,6 +446,7 @@ const (
reqResize reqResize
reqRedrawInputLabel reqRedrawInputLabel
reqRedrawHeaderLabel reqRedrawHeaderLabel
reqRedrawFooterLabel
reqRedrawListLabel reqRedrawListLabel
reqRedrawBorderLabel reqRedrawBorderLabel
reqRedrawPreviewLabel reqRedrawPreviewLabel
@@ -479,7 +492,9 @@ const (
actChangeBorderLabel actChangeBorderLabel
actChangeGhost actChangeGhost
actChangeHeader actChangeHeader
actChangeFooter
actChangeHeaderLabel actChangeHeaderLabel
actChangeFooterLabel
actChangeInputLabel actChangeInputLabel
actChangeListLabel actChangeListLabel
actChangeMulti actChangeMulti
@@ -550,7 +565,9 @@ const (
actTransformBorderLabel actTransformBorderLabel
actTransformGhost actTransformGhost
actTransformHeader actTransformHeader
actTransformFooter
actTransformHeaderLabel actTransformHeaderLabel
actTransformFooterLabel
actTransformInputLabel actTransformInputLabel
actTransformListLabel actTransformListLabel
actTransformNth actTransformNth
@@ -907,6 +924,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
inputBorderShape: opts.InputBorderShape, inputBorderShape: opts.InputBorderShape,
headerBorderShape: opts.HeaderBorderShape, headerBorderShape: opts.HeaderBorderShape,
headerLinesShape: opts.HeaderLinesShape, headerLinesShape: opts.HeaderLinesShape,
footerBorderShape: opts.FooterBorderShape,
borderWidth: 1, borderWidth: 1,
listLabel: nil, listLabel: nil,
listLabelOpts: opts.ListLabel, listLabelOpts: opts.ListLabel,
@@ -918,6 +936,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
inputLabelOpts: opts.InputLabel, inputLabelOpts: opts.InputLabel,
headerLabel: nil, headerLabel: nil,
headerLabelOpts: opts.HeaderLabel, headerLabelOpts: opts.HeaderLabel,
footerLabel: nil,
footerLabelOpts: opts.FooterLabel,
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
executor: executor, executor: executor,
paused: opts.Phony, paused: opts.Phony,
@@ -929,6 +949,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
gap: opts.Gap, gap: opts.Gap,
header: []string{}, header: []string{},
footer: opts.Footer,
header0: opts.Header, header0: opts.Header,
ansi: opts.Ansi, ansi: opts.Ansi,
nthAttr: opts.Theme.Nth.Attr, 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.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColPreviewLabel, false)
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(opts.InputLabel.label, &tui.ColInputLabel, 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.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 // Disable separator by default if input border is set
if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 { 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 extra += t.headerLines
} }
if len(t.footer) > 0 {
extra += borderLines(t.footerBorderShape)
extra += len(t.footer)
}
return extra return extra
} }
@@ -1475,6 +1546,16 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw 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 // UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) { func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock() t.mutex.Lock()
@@ -1835,6 +1916,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if t.headerBorder != nil { if t.headerBorder != nil {
t.headerBorder = nil t.headerBorder = nil
} }
if t.footerWindow != nil {
t.footerWindow = nil
}
if t.footerBorder != nil {
t.footerBorder = nil
}
if t.headerLinesWindow != nil { if t.headerLinesWindow != nil {
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 // Adjust position and size of the list window if input border is set
inputBorderHeight := 0 inputBorderHeight := 0
availableLines := height availableLines := height
shift := 0 shift := 0
shrink := 0 shrink := 0
hasHeaderWindow := t.hasHeaderWindow() hasHeaderWindow := t.hasHeaderWindow()
hasFooterWindow := len(t.footer) > 0
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape() hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow) hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow)
inputWindowHeight := 2
if t.noSeparatorLine() {
inputWindowHeight--
}
if hasInputWindow { if hasInputWindow {
inputWindowHeight := 2 inputBorderHeight = util.Constrain(borderLines(t.inputBorderShape)+inputWindowHeight, 0, availableLines)
if t.noSeparatorLine() {
inputWindowHeight--
}
inputBorderHeight = util.Min(availableLines, borderLines(t.inputBorderShape)+inputWindowHeight)
if t.layout == layoutReverse { if t.layout == layoutReverse {
shift = inputBorderHeight shift = inputBorderHeight
shrink = inputBorderHeight shrink = inputBorderHeight
@@ -1907,6 +1996,17 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
shrink = inputBorderHeight shrink = inputBorderHeight
} }
availableLines -= 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 // 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 { if hasHeaderLinesWindow {
headerWindowHeight -= t.headerLines headerWindowHeight -= t.headerLines
} }
headerBorderHeight = util.Min(availableLines, borderLines(t.headerBorderShape)+headerWindowHeight) headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
if t.layout == layoutReverse { if t.layout == layoutReverse {
shift += headerBorderHeight shift += headerBorderHeight
shrink += headerBorderHeight shrink += headerBorderHeight
@@ -1928,7 +2028,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
headerLinesHeight := 0 headerLinesHeight := 0
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
headerLinesHeight = util.Min(availableLines, borderLines(headerLinesShape)+t.headerLines) headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
if t.layout != layoutDefault { if t.layout != layoutDefault {
shift += headerLinesHeight shift += headerLinesHeight
shrink += headerLinesHeight shrink += headerLinesHeight
@@ -1938,6 +2038,17 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
availableLines -= headerLinesHeight 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 // Set up list border
hasListBorder := t.listBorderShape.Visible() hasListBorder := t.listBorderShape.Visible()
innerWidth := width innerWidth := width
@@ -2041,13 +2152,8 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
width++ width++
} }
maxPreviewLines := availableLines pheight = util.Constrain(pheight, minPreviewHeight, 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 { if previewOpts.position == posUp {
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight) innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
@@ -2210,7 +2316,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
case layoutDefault: case layoutDefault:
btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight
case layoutReverse: case layoutReverse:
btop = w.Top() - shrink btop = w.Top() - shrink + footerBorderHeight
case layoutReverseList: case layoutReverseList:
btop = w.Top() + w.Height() + headerBorderHeight btop = w.Top() + w.Height() + headerBorderHeight
} }
@@ -2238,7 +2344,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
var btop int var btop int
if hasInputWindow && t.headerFirst { if hasInputWindow && t.headerFirst {
if t.layout == layoutReverse { if t.layout == layoutReverse {
btop = w.Top() - shrink btop = w.Top() - shrink + footerBorderHeight
} else if t.layout == layoutReverseList { } else if t.layout == layoutReverseList {
btop = w.Top() + w.Height() + inputBorderHeight btop = w.Top() + w.Height() + inputBorderHeight
} else { } else {
@@ -2294,12 +2400,31 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, headerLinesShape, tui.WindowHeader, 0) 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 // Print border label
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false) 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.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.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.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.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) { 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: case layoutDefault:
y = h - y - 1 y = h - y - 1
case layoutReverseList: case layoutReverseList:
if t.window == t.inputWindow || t.window == t.headerWindow { if !t.inListWindow() && t.window != t.headerLinesWindow {
// From bottom to top // From bottom to top
y = h - y - 1 y = h - y - 1
} else { } else {
@@ -2690,8 +2815,10 @@ func (t *Terminal) resizeIfNeeded() bool {
if t.hasHeaderLinesWindow() { if t.hasHeaderLinesWindow() {
primaryHeaderLines -= t.headerLines 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() 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()) || (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 || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) ||
!needHeaderLinesWindow && t.headerLinesWindow != nil { !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 { func (t *Terminal) headerIndent(borderShape tui.BorderShape) int {
indentSize := t.pointerLen + t.markerLen indentSize := t.pointerLen + t.markerLen
if t.listBorderShape.HasLeft() { if t.listBorderShape.HasLeft() {
@@ -2792,7 +2954,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
} }
func (t *Terminal) canSpanMultiLines() bool { 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) { func (t *Terminal) renderBar(line int, barRange [2]int) {
@@ -3767,6 +3929,7 @@ func (t *Terminal) printAll() {
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
t.printFooter()
t.printPreview() t.printPreview()
} }
@@ -4515,6 +4678,7 @@ func (t *Terminal) Loop() error {
t.reqBox.Set(reqPrompt, nil) t.reqBox.Set(reqPrompt, nil)
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqHeader, nil) t.reqBox.Set(reqHeader, nil)
t.reqBox.Set(reqFooter, nil)
if t.initDelay > 0 { if t.initDelay > 0 {
go func() { go func() {
timer := time.NewTimer(t.initDelay) timer := time.NewTimer(t.initDelay)
@@ -4797,6 +4961,10 @@ func (t *Terminal) Loop() error {
if !t.resizeIfNeeded() { if !t.resizeIfNeeded() {
t.printHeader() t.printHeader()
} }
case reqFooter:
if !t.resizeIfNeeded() {
t.printFooter()
}
case reqActivate: case reqActivate:
t.suppress = false t.suppress = false
if t.hasPreviewer() { 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) t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
case reqRedrawHeaderLabel: case reqRedrawHeaderLabel:
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true) 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: case reqRedrawListLabel:
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true) t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
case reqRedrawBorderLabel: case reqRedrawBorderLabel:
@@ -4996,7 +5166,7 @@ func (t *Terminal) Loop() error {
} }
updatePreviewWindow := func(forcePreview bool) { updatePreviewWindow := func(forcePreview bool) {
t.resizeWindows(forcePreview, false) t.resizeWindows(forcePreview, false)
req(reqPrompt, reqList, reqInfo, reqHeader) req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter)
} }
toggle := func() bool { toggle := func() bool {
current := t.currentItem() current := t.currentItem()
@@ -5271,6 +5441,16 @@ func (t *Terminal) Loop() error {
} else { } else {
req(reqHeader) 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: case actChangeHeaderLabel, actTransformHeaderLabel:
label := a.a label := a.a
if a.t == actTransformHeaderLabel { if a.t == actTransformHeaderLabel {
@@ -5279,6 +5459,14 @@ func (t *Terminal) Loop() error {
t.headerLabelOpts.label = label t.headerLabelOpts.label = label
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel) 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: case actChangeInputLabel, actTransformInputLabel:
label := a.a label := a.a
if a.t == actTransformInputLabel { if a.t == actTransformInputLabel {

View File

@@ -11,10 +11,14 @@ func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments // Prepare arguments
fzf, rest := args[0], args[1:] fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"} 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` // We append --border option at the end, because `--style=full:STYLE`
// may have changed the default border 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() { if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1") args = append(args, "--margin=0,1")

View File

@@ -829,11 +829,14 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind
case WindowHeader: case WindowHeader:
w.fg = r.theme.Header.Color w.fg = r.theme.Header.Color
w.bg = r.theme.HeaderBg.Color w.bg = r.theme.HeaderBg.Color
case WindowFooter:
w.fg = r.theme.Footer.Color
w.bg = r.theme.FooterBg.Color
case WindowPreview: case WindowPreview:
w.fg = r.theme.PreviewFg.Color w.fg = r.theme.PreviewFg.Color
w.bg = r.theme.PreviewBg.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 // fzf --color bg:blue --border --padding 1,2
w.Erase() w.Erase()
} }
@@ -889,6 +892,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }
@@ -914,6 +919,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }
@@ -941,6 +948,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }

View File

@@ -600,6 +600,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
normal = ColNormal normal = ColNormal
case WindowHeader: case WindowHeader:
normal = ColHeader normal = ColHeader
case WindowFooter:
normal = ColFooter
case WindowInput: case WindowInput:
normal = ColInput normal = ColInput
case WindowPreview: case WindowPreview:
@@ -865,6 +867,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = ColListBorder.style() style = ColListBorder.style()
case WindowHeader: case WindowHeader:
style = ColHeaderBorder.style() style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput: case WindowInput:
style = ColInputBorder.style() style = ColInputBorder.style()
case WindowPreview: case WindowPreview:

View File

@@ -359,6 +359,10 @@ type ColorTheme struct {
HeaderBg ColorAttr HeaderBg ColorAttr
HeaderBorder ColorAttr HeaderBorder ColorAttr
HeaderLabel ColorAttr HeaderLabel ColorAttr
Footer ColorAttr
FooterBg ColorAttr
FooterBorder ColorAttr
FooterLabel ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
Border ColorAttr Border ColorAttr
@@ -612,6 +616,7 @@ const (
WindowPreview WindowPreview
WindowInput WindowInput
WindowHeader WindowHeader
WindowFooter
) )
type Renderer interface { type Renderer interface {
@@ -720,6 +725,9 @@ var (
ColHeader ColorPair ColHeader ColorPair
ColHeaderBorder ColorPair ColHeaderBorder ColorPair
ColHeaderLabel ColorPair ColHeaderLabel ColorPair
ColFooter ColorPair
ColFooterBorder ColorPair
ColFooterLabel ColorPair
ColSeparator ColorPair ColSeparator ColorPair
ColScrollbar ColorPair ColScrollbar ColorPair
ColGapLine ColorPair ColGapLine ColorPair
@@ -758,6 +766,7 @@ func EmptyTheme() *ColorTheme {
Cursor: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined}, Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Footer: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined}, ListLabel: ColorAttr{colUndefined, AttrUndefined},
@@ -778,6 +787,9 @@ func EmptyTheme() *ColorTheme {
HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
FooterBg: ColorAttr{colUndefined, AttrUndefined},
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined},
} }
@@ -825,6 +837,9 @@ func NoColorTheme() *ColorTheme {
HeaderBg: ColorAttr{colDefault, AttrUndefined}, HeaderBg: ColorAttr{colDefault, AttrUndefined},
HeaderBorder: ColorAttr{colDefault, AttrUndefined}, HeaderBorder: ColorAttr{colDefault, AttrUndefined},
HeaderLabel: ColorAttr{colDefault, AttrUndefined}, HeaderLabel: ColorAttr{colDefault, AttrUndefined},
FooterBg: ColorAttr{colDefault, AttrUndefined},
FooterBorder: ColorAttr{colDefault, AttrUndefined},
FooterLabel: ColorAttr{colDefault, AttrUndefined},
GapLine: ColorAttr{colDefault, AttrUndefined}, GapLine: ColorAttr{colDefault, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined},
} }
@@ -852,6 +867,7 @@ func init() {
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Footer: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined},
Ghost: ColorAttr{colUndefined, Dim}, Ghost: ColorAttr{colUndefined, Dim},
@@ -869,6 +885,12 @@ func init() {
InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: 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}, GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined},
} }
@@ -893,6 +915,7 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
Ghost: ColorAttr{colUndefined, Dim}, Ghost: ColorAttr{colUndefined, Dim},
@@ -910,6 +933,12 @@ func init() {
InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: 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}, GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined}, Nth: ColorAttr{colUndefined, AttrUndefined},
} }
@@ -934,6 +963,7 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Footer: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
Ghost: ColorAttr{colUndefined, Dim}, Ghost: ColorAttr{colUndefined, Dim},
@@ -954,6 +984,9 @@ func init() {
HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
FooterBg: ColorAttr{colUndefined, AttrUndefined},
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined}, GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: 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.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Marker = o(baseTheme.Marker, theme.Marker) theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Footer = o(baseTheme.Footer, theme.Footer)
theme.Border = o(baseTheme.Border, theme.Border) theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) 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.HeaderBorder = o(theme.Border, theme.HeaderBorder)
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel) 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) initPalette(theme)
} }
@@ -1095,6 +1133,9 @@ func initPalette(theme *ColorTheme) {
ColHeader = pair(theme.Header, theme.HeaderBg) ColHeader = pair(theme.Header, theme.HeaderBg)
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg) ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
ColHeaderLabel = pair(theme.HeaderLabel, 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 { func runeWidth(r rune) int {

View File

@@ -97,24 +97,12 @@ func Min32(first int32, second int32) int32 {
// Constrain32 limits the given 32-bit integer with the upper and lower bounds // Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 { func Constrain32(val int32, min int32, max int32) int32 {
if val < min { return Max32(Min32(val, max), min)
return min
}
if val > max {
return max
}
return val
} }
// Constrain limits the given integer with the upper and lower bounds // Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int { func Constrain(val int, min int, max int) int {
if val < min { return Max(Min(val, max), min)
return min
}
if val > max {
return max
}
return val
} }
func AsUint16(val int) uint16 { func AsUint16(val int) uint16 {

View File

@@ -979,6 +979,126 @@ class TestLayout < TestInteractive
end end
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 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 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 block = <<~BLOCK
@@ -1033,4 +1153,79 @@ class TestLayout < TestInteractive
BLOCK BLOCK
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
end 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 end