Fix inconsistent placement of header-lines with border options

fzf displayed --header-lines inconsistently depending on the presence of borders:

  # --header and --header-lines co-located
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first

  # --header and --header-lines separated
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first --header-lines-border

This commit fixes the inconsistency with the following logic:

* If only one of --header or --header-lines is provided, --header-first
  applies to that single header.
* If both are present, --header-first affects only the regular --header,
  not --header-lines.
This commit is contained in:
Junegunn Choi
2025-06-05 22:02:22 +09:00
parent f6c589c606
commit 39db026161
4 changed files with 108 additions and 47 deletions

View File

@@ -1001,7 +1001,8 @@ The first N lines of the input are treated as the sticky header. When
lines that follow.
.TP
.B "\-\-header\-first"
Print header before the prompt line
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

View File

@@ -3218,15 +3218,6 @@ func postProcessOptions(opts *Options) error {
if opts.HeaderLinesShape == tui.BorderNone {
opts.HeaderLinesShape = tui.BorderPhantom
} else if opts.HeaderLinesShape == tui.BorderUndefined {
// In reverse-list layout, header lines should be at the top, while
// ordinary header should be at the bottom. So let's use a separate
// window for the header lines.
if opts.Layout == layoutReverseList {
opts.HeaderLinesShape = tui.BorderPhantom
} else {
opts.HeaderLinesShape = tui.BorderNone
}
}
if opts.Pointer == nil {

View File

@@ -1203,8 +1203,8 @@ func (t *Terminal) extraLines() int {
extra += borderLines(t.headerBorderShape)
}
extra += len(t.header0)
if t.hasHeaderLinesWindow() {
extra += borderLines(t.headerLinesShape)
if w, shape := t.determineHeaderLinesShape(); w {
extra += borderLines(shape)
}
extra += t.headerLines
}
@@ -1770,7 +1770,46 @@ func (t *Terminal) hasHeaderWindow() bool {
}
func (t *Terminal) hasHeaderLinesWindow() bool {
return t.headerVisible && t.headerLines > 0 && t.headerLinesShape.Visible()
w, _ := t.determineHeaderLinesShape()
return w
}
func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
if !t.headerVisible || t.headerLines == 0 {
return false, tui.BorderNone
}
// --header-lines-border is set
if t.headerLinesShape != tui.BorderUndefined {
return true, t.headerLinesShape
}
// --header-lines-border is not set, determine if we should use
// the style of --header-border
shape := tui.BorderNone
if len(t.header0) == 0 {
shape = t.headerBorderShape
}
if shape == tui.BorderNone {
shape = tui.BorderPhantom
}
// --layout reverse-list is set
if t.layout == layoutReverseList {
return true, shape
}
// Use header window instead
if len(t.header0) == 0 {
return false, t.headerBorderShape
}
// We have both types of headers, and we want to separate the two
if t.headerFirst {
return true, shape
}
return false, tui.BorderNone
}
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
@@ -1853,7 +1892,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
shift := 0
shrink := 0
hasHeaderWindow := t.hasHeaderWindow()
hasHeaderLinesWindow := t.hasHeaderLinesWindow()
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow)
if hasInputWindow {
inputWindowHeight := 2
@@ -1889,7 +1928,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
headerLinesHeight := 0
if hasHeaderLinesWindow {
headerLinesHeight = util.Min(availableLines, borderLines(t.headerLinesShape)+t.headerLines)
headerLinesHeight = util.Min(availableLines, borderLines(headerLinesShape)+t.headerLines)
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
@@ -2147,23 +2186,33 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if t.wborder == nil {
w = t.window
}
if hasInputWindow {
var btop int
if hasHeaderWindow && t.headerFirst {
if t.layout == layoutReverse {
btop = w.Top() - inputBorderHeight - headerLinesHeight
} else if t.layout == layoutReverseList {
if (hasHeaderWindow || hasHeaderLinesWindow) && t.headerFirst {
switch t.layout {
case layoutDefault:
btop = w.Top() + w.Height()
// If both headers are present, the header lines are displayed with the list
if hasHeaderWindow && hasHeaderLinesWindow {
btop += headerLinesHeight
}
case layoutReverse:
btop = w.Top() - inputBorderHeight
if hasHeaderWindow && hasHeaderLinesWindow {
btop -= headerLinesHeight
}
case layoutReverseList:
btop = w.Top() + w.Height()
} else {
btop = w.Top() + w.Height() + headerLinesHeight
}
} else {
if t.layout == layoutReverse {
btop = w.Top() - shrink
} else if t.layout == layoutReverseList {
btop = w.Top() + w.Height() + headerBorderHeight
} else {
switch t.layout {
case layoutDefault:
btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight
case layoutReverse:
btop = w.Top() - shrink
case layoutReverseList:
btop = w.Top() + w.Height() + headerBorderHeight
}
}
shift := 0
@@ -2215,17 +2264,34 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
// Set up header lines border
if hasHeaderLinesWindow {
var btop int
if t.layout != layoutDefault {
btop = w.Top() - headerLinesHeight
// NOTE: We still have to handle --header-first here in case
// --header-lines-border is set. Can't we just use header window instead
// with the style? So we can display header label.
// fzf --header-lines 3 --header-label hello --header-border
// fzf --header-lines 3 --header-label hello --header-lines-border
headerFirst := t.headerFirst && len(t.header0) == 0
if headerFirst {
if t.layout == layoutDefault {
btop = w.Top() + w.Height() + inputBorderHeight
} else if t.layout == layoutReverse {
btop = w.Top() - headerLinesHeight - inputBorderHeight
} else {
btop = w.Top() - headerLinesHeight
}
} else {
btop = w.Top() + w.Height()
if t.layout != layoutDefault {
btop = w.Top() - headerLinesHeight
} else {
btop = w.Top() + w.Height()
}
}
t.headerLinesBorder = t.tui.NewWindow(
btop,
w.Left(),
w.Width(),
headerLinesHeight, tui.WindowHeader, tui.MakeBorderStyle(t.headerLinesShape, t.unicode), true)
t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, t.headerLinesShape, tui.WindowHeader, 0)
headerLinesHeight, tui.WindowHeader, tui.MakeBorderStyle(headerLinesShape, t.unicode), true)
t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, headerLinesShape, tui.WindowHeader, 0)
}
// Print border label
@@ -2621,12 +2687,14 @@ func (t *Terminal) resizeIfNeeded() bool {
// Check if the header borders are used and header has changed
allHeaderLines := t.visibleHeaderLines()
primaryHeaderLines := allHeaderLines
if t.headerLinesShape.Visible() {
if t.hasHeaderLinesWindow() {
primaryHeaderLines -= t.headerLines
}
if (t.headerBorderShape.Visible() || t.headerLinesShape.Visible()) &&
needHeaderLinesWindow := t.hasHeaderLinesWindow()
if (t.headerBorderShape.Visible() || t.hasHeaderLinesWindow()) &&
(t.headerWindow == nil && primaryHeaderLines > 0 || t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
t.headerLinesShape.Visible() && (t.headerLinesWindow == nil && t.headerLines > 0 || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
needHeaderLinesWindow && (t.headerLinesWindow == nil || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) ||
!needHeaderLinesWindow && t.headerLinesWindow != nil {
t.printAll()
return true
}
@@ -2640,14 +2708,14 @@ func (t *Terminal) printHeader() {
t.withWindow(t.headerWindow, func() {
var lines []string
if !t.headerLinesShape.Visible() {
if !t.hasHeaderLinesWindow() {
lines = t.header
}
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, lines)
})
if t.headerLinesShape.Visible() {
if w, shape := t.determineHeaderLinesShape(); w {
t.withWindow(t.headerLinesWindow, func() {
t.printHeaderImpl(t.headerLinesWindow, t.headerLinesShape, nil, t.header)
t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
})
}
}
@@ -5863,7 +5931,8 @@ func (t *Terminal) Loop() error {
}
if clicked && t.headerVisible && t.headerLinesWindow != nil && t.headerLinesWindow.Enclose(my, mx) {
mx -= t.headerLinesWindow.Left() + t.headerIndent(t.headerLinesShape)
_, shape := t.determineHeaderLinesShape()
mx -= t.headerLinesWindow.Left() + t.headerIndent(shape)
my -= t.headerLinesWindow.Top()
if mx < 0 {
break

View File

@@ -39,11 +39,11 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
block = <<~OUTPUT
> 4
997/997
>
3
2
1
997/997
>
foobar
OUTPUT
tmux.until { assert_block(block, it) }
@@ -53,10 +53,10 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
block = <<~OUTPUT
foobar
> < 997/997
1
2
3
> < 997/997
> 4
OUTPUT
tmux.until { assert_block(block, it) }
@@ -148,10 +148,10 @@ class TestLayout < TestInteractive
4
> 3
2/2
>
2
1
2/2
>
foo
OUTPUT
@@ -609,11 +609,11 @@ class TestLayout < TestInteractive
4
> 3
2
1
98/98
>
2
1
hello
BLOCK
@@ -666,12 +666,12 @@ class TestLayout < TestInteractive
4
> 3
98/98
>
2
1
98/98
>
BLOCK
tmux.until { assert_block(block1, it) }