From 5e31fd25e70a40a5fecf791a412a0e738b842001 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 17 Jul 2026 23:18:00 +0900 Subject: [PATCH] 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 --- CHANGELOG.md | 8 +++++ man/man1/fzf.1 | 4 +++ src/options.go | 3 ++ src/terminal.go | 86 +++++++++++++++++++++++++++++++++++++-------- test/test_layout.rb | 38 ++++++++++---------- 5 files changed, 106 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b91465a..9cc1af55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index cc3b74a4..5d82030f 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 diff --git a/src/options.go b/src/options.go index 5322f3da..3e92c6b9 100644 --- a/src/options.go +++ b/src/options.go @@ -3696,6 +3696,9 @@ func (opts *Options) noSeparatorLine() bool { if opts.Inputless { return true } + // NOTE: This can overestimate by one line when the default separator is + // suppressed at runtime (see Terminal.separatorLength), making the height + // lower bounds derived from it one line more generous than necessary. sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 return noSeparatorLine(opts.InfoStyle, sep) } diff --git a/src/terminal.go b/src/terminal.go index d9da03f0..9cc1fdac 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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,62 @@ 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 + } + 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 { + // Header section + 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) { @@ -3357,7 +3415,7 @@ func (t *Terminal) printInfoImpl() { str = string(trimmed) width = maxWidth } - move(line, pos, t.separatorLen == 0) + move(line, pos, t.separatorLength() == 0) if t.reading { t.window.CPrint(tui.ColSpinner, str) } else { @@ -3366,7 +3424,7 @@ func (t *Terminal) printInfoImpl() { pos += width } printSeparator := func(fillLength int, pad bool) { - if t.separatorLen > 0 { + if t.separatorLength() > 0 { t.separator(t.window, fillLength) t.window.Print(" ") } else if pad { @@ -3375,7 +3433,7 @@ func (t *Terminal) printInfoImpl() { } if t.infoStyle == infoHidden { - if t.separatorLen > 0 { + if t.separatorLength() > 0 { if !move(line+1, 0, false) { return } @@ -3438,7 +3496,7 @@ func (t *Terminal) printInfoImpl() { } switch t.infoStyle { case infoDefault: - if !move(line+1, 0, t.separatorLen == 0) { + if !move(line+1, 0, t.separatorLength() == 0) { return } printSpinner() @@ -3522,7 +3580,7 @@ func (t *Terminal) printInfoImpl() { } if t.infoStyle == infoInlineRight { - if t.separatorLen > 0 { + if t.separatorLength() > 0 { if !move(line+1, 0, false) { return } diff --git a/test/test_layout.rb b/test/test_layout.rb index 3e4f3a7a..98de2218 100644 --- a/test/test_layout.rb +++ b/test/test_layout.rb @@ -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) }