m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 23:03:47 -05:00

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. lines that follow.
.TP .TP
.B "\-\-header\-first" .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 .TP
.BI "\-\-header\-border" [=STYLE] .BI "\-\-header\-border" [=STYLE]
Draw border around the header section Draw border around the header section

View File

@@ -3218,15 +3218,6 @@ func postProcessOptions(opts *Options) error {
if opts.HeaderLinesShape == tui.BorderNone { if opts.HeaderLinesShape == tui.BorderNone {
opts.HeaderLinesShape = tui.BorderPhantom 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 { if opts.Pointer == nil {

View File

@@ -1203,8 +1203,8 @@ func (t *Terminal) extraLines() int {
extra += borderLines(t.headerBorderShape) extra += borderLines(t.headerBorderShape)
} }
extra += len(t.header0) extra += len(t.header0)
if t.hasHeaderLinesWindow() { if w, shape := t.determineHeaderLinesShape(); w {
extra += borderLines(t.headerLinesShape) extra += borderLines(shape)
} }
extra += t.headerLines extra += t.headerLines
} }
@@ -1770,7 +1770,46 @@ func (t *Terminal) hasHeaderWindow() bool {
} }
func (t *Terminal) hasHeaderLinesWindow() 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) { func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
@@ -1853,7 +1892,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
shift := 0 shift := 0
shrink := 0 shrink := 0
hasHeaderWindow := t.hasHeaderWindow() hasHeaderWindow := t.hasHeaderWindow()
hasHeaderLinesWindow := t.hasHeaderLinesWindow() hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow) hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow)
if hasInputWindow { if hasInputWindow {
inputWindowHeight := 2 inputWindowHeight := 2
@@ -1889,7 +1928,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
headerLinesHeight := 0 headerLinesHeight := 0
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
headerLinesHeight = util.Min(availableLines, borderLines(t.headerLinesShape)+t.headerLines) headerLinesHeight = util.Min(availableLines, borderLines(headerLinesShape)+t.headerLines)
if t.layout != layoutDefault { if t.layout != layoutDefault {
shift += headerLinesHeight shift += headerLinesHeight
shrink += headerLinesHeight shrink += headerLinesHeight
@@ -2147,23 +2186,33 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if t.wborder == nil { if t.wborder == nil {
w = t.window w = t.window
} }
if hasInputWindow { if hasInputWindow {
var btop int var btop int
if hasHeaderWindow && t.headerFirst { if (hasHeaderWindow || hasHeaderLinesWindow) && t.headerFirst {
if t.layout == layoutReverse { switch t.layout {
btop = w.Top() - inputBorderHeight - headerLinesHeight case layoutDefault:
} else if t.layout == layoutReverseList { 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() btop = w.Top() + w.Height()
} else {
btop = w.Top() + w.Height() + headerLinesHeight
} }
} else { } else {
if t.layout == layoutReverse { switch t.layout {
btop = w.Top() - shrink case layoutDefault:
} else if t.layout == layoutReverseList {
btop = w.Top() + w.Height() + headerBorderHeight
} else {
btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight btop = w.Top() + w.Height() + headerBorderHeight + headerLinesHeight
case layoutReverse:
btop = w.Top() - shrink
case layoutReverseList:
btop = w.Top() + w.Height() + headerBorderHeight
} }
} }
shift := 0 shift := 0
@@ -2215,17 +2264,34 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
// Set up header lines border // Set up header lines border
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
var btop int var btop int
// 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 {
if t.layout != layoutDefault { if t.layout != layoutDefault {
btop = w.Top() - headerLinesHeight btop = w.Top() - headerLinesHeight
} else { } else {
btop = w.Top() + w.Height() btop = w.Top() + w.Height()
} }
}
t.headerLinesBorder = t.tui.NewWindow( t.headerLinesBorder = t.tui.NewWindow(
btop, btop,
w.Left(), w.Left(),
w.Width(), w.Width(),
headerLinesHeight, tui.WindowHeader, tui.MakeBorderStyle(t.headerLinesShape, t.unicode), true) headerLinesHeight, tui.WindowHeader, tui.MakeBorderStyle(headerLinesShape, t.unicode), true)
t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, t.headerLinesShape, tui.WindowHeader, 0) t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, headerLinesShape, tui.WindowHeader, 0)
} }
// Print border label // Print border label
@@ -2621,12 +2687,14 @@ func (t *Terminal) resizeIfNeeded() bool {
// Check if the header borders are used and header has changed // Check if the header borders are used and header has changed
allHeaderLines := t.visibleHeaderLines() allHeaderLines := t.visibleHeaderLines()
primaryHeaderLines := allHeaderLines primaryHeaderLines := allHeaderLines
if t.headerLinesShape.Visible() { if t.hasHeaderLinesWindow() {
primaryHeaderLines -= t.headerLines 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.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() t.printAll()
return true return true
} }
@@ -2640,14 +2708,14 @@ func (t *Terminal) printHeader() {
t.withWindow(t.headerWindow, func() { t.withWindow(t.headerWindow, func() {
var lines []string var lines []string
if !t.headerLinesShape.Visible() { if !t.hasHeaderLinesWindow() {
lines = t.header lines = t.header
} }
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, lines) 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.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) { 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() my -= t.headerLinesWindow.Top()
if mx < 0 { if mx < 0 {
break break

View File

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