diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc1af55..4e92620c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ CHANGELOG # Separator shown; no border separates the input section from the list section fzf --style full --input-border none --header foo --no-header-border + + # Conversely, separator is now shown when the input border does not draw + # a line facing the list section + fzf --input-border bottom ``` - Rendering improvements - Each frame is now wrapped in synchronized update mode (mode 2026) to reduce flickering on supported terminals diff --git a/src/options.go b/src/options.go index f67583c9..94e3a7f9 100644 --- a/src/options.go +++ b/src/options.go @@ -3699,7 +3699,7 @@ func (opts *Options) noSeparatorLine() bool { // 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 + sep := opts.Separator == nil && !inputBorderFacesList(opts.Layout, opts.InputBorderShape) || opts.Separator != nil && len(*opts.Separator) > 0 return noSeparatorLine(opts.InfoStyle, sep) } diff --git a/src/terminal.go b/src/terminal.go index a4815472..dcac3cd9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1249,13 +1249,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor } // Determine input border shape - if t.inputBorderShape == tui.BorderLine { - if t.layout == layoutReverse { - t.inputBorderShape = tui.BorderBottom - } else { - t.inputBorderShape = tui.BorderTop - } - } + t.inputBorderShape = resolveInputBorderShape(t.layout, t.inputBorderShape) // Inline borders are embedded between the list's top and bottom horizontals. // Shapes missing either one (none/phantom/line/single-sided) fall back to a plain @@ -1711,10 +1705,32 @@ func (t *Terminal) separatorLength() int { return t.separatorLen } +// BorderLine for the input border resolves to a single line on the side +// facing the list section +func resolveInputBorderShape(layout layoutType, shape tui.BorderShape) tui.BorderShape { + if shape != tui.BorderLine { + return shape + } + if layout == layoutReverse { + return tui.BorderBottom + } + return tui.BorderTop +} + +// Whether the input border draws a line on the side facing the list section +func inputBorderFacesList(layout layoutType, shape tui.BorderShape) bool { + shape = resolveInputBorderShape(layout, shape) + if layout == layoutReverse { + return shape.HasBottom() + } + return shape.HasTop() +} + // 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() { + // The input border itself draws a line on the side facing the list section + if inputBorderFacesList(t.layout, t.inputBorderShape) { return true } for po := &t.previewOpts; po != nil; po = po.alternative { @@ -1722,12 +1738,18 @@ func (t *Terminal) separatedByBorder() bool { // 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. + // + // The whole chain is checked because which spec is active depends + // on the terminal size, and the resolution (computePreviewSize in + // resizeWindows) itself consults noSeparatorLine, so the decision + // here cannot depend on its outcome. When a spec other than 'next' + // is active, this errs on the side of showing the separator. return false } } hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape() hasHeaderWindow := t.hasHeaderWindow() - if !hasHeaderWindow && !hasHeaderLinesWindow { + if !t.inputBorderShape.Visible() && !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 @@ -1736,7 +1758,11 @@ func (t *Terminal) separatedByBorder() bool { // 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 { + // NOTE: hasHeaderWindow can be true with no header content when the input + // border is visible (see Terminal.hasHeaderWindow). An empty header window + // takes no space, so it cannot be the facing section. + hasHeaderSection := hasHeaderWindow && len(t.header0)+t.headerLines > 0 + if hasHeaderSection && !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 diff --git a/test/test_layout.rb b/test/test_layout.rb index 98de2218..600f0444 100644 --- a/test/test_layout.rb +++ b/test/test_layout.rb @@ -1228,6 +1228,115 @@ class TestLayout < TestInteractive end end + def test_separator_with_input_border + # Border line below input does not face the list; separator is shown + tmux.send_keys %(seq 100 | #{FZF} --input-border bottom), :Enter + block = <<~BLOCK + > 1 + 100/100 ── + > + ──────────── + BLOCK + tmux.until { assert_block(block, it) } + teardown + setup + + # List border faces the input instead; separator is hidden + tmux.send_keys %(seq 100 | #{FZF} --input-border bottom --list-border rounded), :Enter + block = <<~BLOCK + │ > 1 + ╰──────────── + 100/100 + > + ───────────── + BLOCK + tmux.until { assert_block(block, it) } + teardown + setup + + # Border line above input faces the list; separator is hidden + tmux.send_keys %(seq 100 | #{FZF} --input-border top), :Enter + block = <<~BLOCK + > 1 + ──────────── + 100/100 + > + BLOCK + tmux.until { assert_block(block, it) } + end + + def test_separator_with_input_border_reverse + # Border line above input does not face the list; separator is shown + tmux.send_keys %(seq 100 | #{FZF} --layout reverse --input-border top), :Enter + block = <<~BLOCK + ──────────── + > + 100/100 ── + > 1 + BLOCK + tmux.until { assert_block(block, it) } + teardown + setup + + # Border line below input faces the list; separator is hidden + tmux.send_keys %(seq 100 | #{FZF} --layout reverse --input-border bottom), :Enter + block = <<~BLOCK + > + 100/100 + ──────────── + > 1 + BLOCK + tmux.until { assert_block(block, it) } + end + + def test_separator_with_preview_next + # Preview window at 'next' position sits next to the input section, so the + # header border does not hide the separator + tmux.send_keys %(seq 100 | #{FZF} --header foo --header-border rounded --no-list-border --preview 'echo hi' --preview-window next,1,border-none), :Enter + block = <<~BLOCK + ╰──────────── + hi + 100/100 ─── + > + BLOCK + tmux.until { assert_block(block, it) } + teardown + setup + + # Same when 'next' position comes from a threshold alternative + tmux.send_keys %(seq 100 | #{FZF} --header foo --header-border rounded --no-list-border --preview 'echo hi' --preview-window 'up,40%,<9999(next,1,border-none)'), :Enter + tmux.until { assert_block(block, it) } + end + + def test_separator_with_inline_header_border + # Inline header is drawn inside the list border; bare header lines sit + # next to the input section, so the separator is shown + tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header foo --header-lines 2 --header-border inline --header-lines-border none), :Enter + block = <<~BLOCK + ╰──────────── + 2 + 1 + 98/98 ─── + > + BLOCK + tmux.until { assert_block(block, it) } + teardown + setup + + # Header lines are embedded in the list border along with the inline + # header; the list border faces the input section and hides the separator + tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header foo --header-lines 2 --header-border inline), :Enter + block = <<~BLOCK + │ 2 + │ 1 + │ foo + ╰──────────── + 98/98 + > + BLOCK + tmux.until { assert_block(block, it) } + end + def test_header_border_no_pointer_and_marker tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter block = <<~BLOCK