Hide default separator when a border separates input from list

- Applies when adjacent section (header, header lines, or list) draws
  border line facing input section

    # No separator below header border
    fzf --style full --input-border none --header foo

    # Separator shown; nothing separates input from list
    fzf --style full --input-border none --header foo --no-header-border

- Evaluated dynamically so change-header/toggle-header update it
- Inline header is drawn inside list border and not adjacent to input;
  decide from section actually next to it
- Separator is kept when a preview window can be at 'next' position,
  right next to the input section
This commit is contained in:
Junegunn Choi
2026-07-18 10:51:49 +09:00
parent e877b6f846
commit c60bcf67a5
5 changed files with 117 additions and 33 deletions
+8
View File
@@ -3,6 +3,14 @@ CHANGELOG
0.74.1
------
- The default separator on the info line is no longer shown when the input section is already visually separated from the list section by a border line
```sh
# No separator shown below the header border
fzf --style full --input-border none --header foo
# Separator shown; no border separates the input section from the list section
fzf --style full --input-border none --header foo --no-header-border
```
- Rendering improvements
- Each frame is now wrapped in synchronized update mode (mode 2026) to reduce flickering on supported terminals
- Reduced rendering output by 10-23% by skipping redundant SGR sequences
+4
View File
@@ -823,6 +823,10 @@ A synonym for \fB\-\-info=hidden\fB
The given string will be repeated to form the horizontal separator on the info
line (default: '─' or '\-' depending on \fB\-\-no\-unicode\fR).
Unless explicitly specified, the separator is not displayed if the input
section is already visually separated from the list section by a border line
(e.g. \fB\-\-input\-border\fR or \fB\-\-header\-border\fR).
ANSI color codes are supported.
.TP
+3
View File
@@ -3696,6 +3696,9 @@ func (opts *Options) noSeparatorLine() bool {
if opts.Inputless {
return true
}
// NOTE: This does not know that the default separator can be suppressed at
// runtime (see Terminal.separatorLength), so the minimum heights derived
// from it can be one line taller than strictly necessary.
sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
return noSeparatorLine(opts.InfoStyle, sep)
}
+83 -14
View File
@@ -271,6 +271,7 @@ type Terminal struct {
ghost string
separator labelPrinter
separatorLen int
separatorAuto bool
spinner []string
promptString string
prompt func()
@@ -1299,14 +1300,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
}
}
// Disable separator by default if input border is set
if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 {
bar := "─"
if opts.Separator != nil {
bar = *opts.Separator
} else if !t.unicode {
bar = "-"
}
// The default separator can be suppressed at runtime depending on the
// window layout (see separatorLength)
t.separatorAuto = opts.Separator == nil
bar := "─"
if opts.Separator != nil {
bar = *opts.Separator
} else if !t.unicode {
bar = "-"
}
if len(bar) > 0 {
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
@@ -1695,7 +1698,72 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
}
func (t *Terminal) noSeparatorLine() bool {
return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLen > 0)
return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLength() > 0)
}
// Effective length of the horizontal separator. The default separator is
// suppressed when the input section is already visually separated from the
// list section by a border line.
func (t *Terminal) separatorLength() int {
if t.separatorAuto && t.separatedByBorder() {
return 0
}
return t.separatorLen
}
// The default horizontal separator is redundant if the input section is
// already visually separated from the list section by a border line
func (t *Terminal) separatedByBorder() bool {
if t.inputBorderShape.Visible() {
return true
}
for po := &t.previewOpts; po != nil; po = po.alternative {
if po.position == posNext {
// A preview window at 'next' position sits right next to the input
// section instead. Keep the separator, as the visibility and the
// border of the preview window can change at runtime.
return false
}
}
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
hasHeaderWindow := t.hasHeaderWindow()
if !hasHeaderWindow && !hasHeaderLinesWindow {
// No separate window is created for the input section in this case
// (see hasInputWindow in resizeWindows), i.e. the input section is
// embedded in the list window and no border separates the two
return false
}
// The input section has its own window. Determine which section it is
// facing, and check if the border of that section draws a line toward it.
if hasHeaderWindow && !t.headerFirst && t.headerBorderShape != tui.BorderInline {
// Header section; an inline header is not a separate section, it is
// drawn inside the list border, so it falls through to the branches
// below
return t.borderFacingInput(t.headerBorderShape)
}
// The header lines section is next to the input section, except in
// reverse-list layout where it is on the other side of the list section,
// or with --header-first and no header section where it is moved past the
// input section
if hasHeaderLinesWindow && t.layout != layoutReverseList && (hasHeaderWindow || !t.headerFirst) {
return t.borderFacingInput(headerLinesShape)
}
// List section
return t.borderFacingInput(t.listBorderShape)
}
// Whether the shape draws a horizontal line facing the info line when the
// section is placed between the input section and the list section
func (t *Terminal) borderFacingInput(shape tui.BorderShape) bool {
if shape == tui.BorderInline {
// Embedded between the horizontal lines of the list border
return true
}
if t.layout == layoutReverse {
return shape.HasTop()
}
return shape.HasBottom()
}
func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
@@ -3330,6 +3398,7 @@ func (t *Terminal) printInfoImpl() {
}
pos := 0
line := 0
separatorLen := t.separatorLength()
maxHeight := t.window.Height()
move := func(y int, x int, clear bool) bool {
if y < 0 || y >= maxHeight {
@@ -3357,7 +3426,7 @@ func (t *Terminal) printInfoImpl() {
str = string(trimmed)
width = maxWidth
}
move(line, pos, t.separatorLen == 0)
move(line, pos, separatorLen == 0)
if t.reading {
t.window.CPrint(tui.ColSpinner, str)
} else {
@@ -3366,7 +3435,7 @@ func (t *Terminal) printInfoImpl() {
pos += width
}
printSeparator := func(fillLength int, pad bool) {
if t.separatorLen > 0 {
if separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
} else if pad {
@@ -3375,7 +3444,7 @@ func (t *Terminal) printInfoImpl() {
}
if t.infoStyle == infoHidden {
if t.separatorLen > 0 {
if separatorLen > 0 {
if !move(line+1, 0, false) {
return
}
@@ -3438,7 +3507,7 @@ func (t *Terminal) printInfoImpl() {
}
switch t.infoStyle {
case infoDefault:
if !move(line+1, 0, t.separatorLen == 0) {
if !move(line+1, 0, separatorLen == 0) {
return
}
printSpinner()
@@ -3522,7 +3591,7 @@ func (t *Terminal) printInfoImpl() {
}
if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 {
if separatorLen > 0 {
if !move(line+1, 0, false) {
return
}
+19 -19
View File
@@ -597,7 +597,7 @@ class TestLayout < TestInteractive
2
1
header
19/97
19/97
> 1
@@ -628,7 +628,7 @@ class TestLayout < TestInteractive
hello
100/100
100/100
>
BLOCK
tmux.until { assert_block(block2, it) }
@@ -648,7 +648,7 @@ class TestLayout < TestInteractive
2
1
98/98
98/98
>
BLOCK
tmux.until { assert_block(block1, it) }
@@ -663,7 +663,7 @@ class TestLayout < TestInteractive
1
hello
98/98
98/98
>
BLOCK
tmux.until { assert_block(block2, it) }
@@ -679,7 +679,7 @@ class TestLayout < TestInteractive
4
> 3
98/98
98/98
>
2
@@ -718,7 +718,7 @@ class TestLayout < TestInteractive
2
1
98/98
98/98
>
BLOCK
tmux.until { assert_block(block1, it) }
@@ -734,7 +734,7 @@ class TestLayout < TestInteractive
hello
98/98
98/98
>
BLOCK
tmux.until { assert_block(block2, it) }
@@ -750,7 +750,7 @@ class TestLayout < TestInteractive
4
> 3
98/98
98/98
>
2
@@ -767,7 +767,7 @@ class TestLayout < TestInteractive
2
1
98/98
98/98
>
hello
@@ -810,7 +810,7 @@ class TestLayout < TestInteractive
2
1
header
19/97
19/97
> 1
@@ -825,7 +825,7 @@ class TestLayout < TestInteractive
11
> 10
list
19/97
19/97
> 1
3
@@ -1039,9 +1039,9 @@ class TestLayout < TestInteractive
103 101 103 101 102 102 2/2 103
5/5 102 2/2 102 103 101 103 101 > 2/2 101 101 >
> 103 > 103 102 102 > 102 5/5 102
5/5 103 2/2 103 101 103 > 103
5/5 103 2/2 103 101 103 > 103
> > 102 101
2/2 103 101 102
2/2 103 101 102
> 102 > 103
103
@@ -1099,9 +1099,9 @@ class TestLayout < TestInteractive
> 102 > 102 > 101 > 101 101 101 101 > 1
3/3 101 1/1 101 102 102 102 102 102
> 102 > 102 HEAD 101 HEAD HEAD HEAD 101 1/1 101
3/3 102 1/1 1/1 1/1 102 > 102 3/3 >
3/3 102 1/1 1/1 1/1 102 > 102 3/3 >
> HEAD > > > HEAD HEAD >
1/1
1/1
> 101 101
102 102
HEAD HEAD
@@ -1159,9 +1159,9 @@ class TestLayout < TestInteractive
> 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 >
3/3 102 1/1 102 > 102 3/3 >
> HEAD > HEAD HEAD >
1/1
1/1
> 101 101
102 102
HEAD HEAD
@@ -1191,7 +1191,7 @@ class TestLayout < TestInteractive
label
header
10/10
10/10
>
BLOCK
@@ -1234,7 +1234,7 @@ class TestLayout < TestInteractive
1
9/9
9/9
>
BLOCK
tmux.until { assert_block(block, it) }