mirror of
https://github.com/junegunn/fzf.git
synced 2026-04-26 09:26:00 +08:00
Add --header-border=inline / --header-lines-border=inline / --footer-border=inline
Adds a new BorderShape, BorderInline, accepted as a value for
--header-border, --header-lines-border, and --footer-border. When the
surrounding --list-border has both top and bottom horizontals (rounded,
sharp, bold, double, thinblock, block, horizontal), the corresponding
section is rendered inside the list frame separated from the list
content by a horizontal line whose endpoints join the list border as
T-junctions. Without a compatible list border, the shape falls back to
BorderLine.
Supports:
- All three layouts (default, reverse, reverse-list).
- Any combination of the three inline sections, producing stacked
separators.
- --header-label and --footer-label rendered on their separator row.
- Section colors: the portion of the list frame adjacent to an inline
section (left/right verticals on the section's content rows plus the
outer top/bottom edge + corners when the section is at the edge)
inherits the section's --color *-border and *-bg, giving each section
a uniform color block. The separator itself carries the section's
colors since it acts as the section's inner edge.
- When --color header-border / --color footer-border is not set, the
inline section inherits --color list-border so the default palette
stays coherent.
- thinblock / block styles pick the horizontal char (top vs bottom)
based on which side of the list content the separator sits on, so
the thin line visually hugs the list content.
Rejects combinations that do not make sense:
- --input-border=inline / --list-border=inline / --preview-border=inline
- --header-first + (--header-border=inline | --header-lines-border=inline)
- --header-border=inline with a non-inline --header-lines-border
(inline has to propagate inward toward the list content).
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.72.0
|
||||
------
|
||||
- `--header-border`, `--header-lines-border`, and `--footer-border` now accept a new `inline` style that embeds the section inside the list frame, separated from the list content by a horizontal line whose endpoints join the surrounding list border as T-junctions.
|
||||
- Requires `--list-border` with a line-drawing shape (rounded / sharp / bold / double / horizontal); falls back to `line` otherwise.
|
||||
- Sections stack. Example combining all three:
|
||||
```sh
|
||||
ps -ef | fzf --reverse --style full:double \
|
||||
--header 'Select a process' --header-lines 1 \
|
||||
--bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \
|
||||
--header-border=inline --header-lines-border=inline \
|
||||
--footer-border=inline
|
||||
```
|
||||
- `--header-label` and `--footer-label` render on their respective separator row.
|
||||
- The separator inherits `--color list-border` when the section's own border color is not explicitly set.
|
||||
- `--header-first` is not compatible with `--header-border=inline` or `--header-lines-border=inline`; `--header-border=inline` requires `--header-lines-border` to be `inline` or unset.
|
||||
|
||||
0.71.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
|
||||
|
||||
+15
-2
@@ -1100,7 +1100,13 @@ Print header before the prompt line. When both normal header and header lines
|
||||
.TP
|
||||
.BI "\-\-header\-border" [=STYLE]
|
||||
Draw border around the header section. \fBline\fR style draws a single
|
||||
separator line between the header window and the list section.
|
||||
separator line between the header window and the list section. \fBinline\fR
|
||||
style embeds the header inside the list border frame, joined to the list
|
||||
section by a horizontal separator with T-junctions; it requires a
|
||||
line-drawing \fB\-\-list\-border\fR (rounded / sharp / bold / double /
|
||||
horizontal) and falls back to \fBline\fR otherwise. Not compatible with
|
||||
\fB\-\-header\-first\fR, and when \fB\-\-header\-lines\fR is also set
|
||||
\fB\-\-header\-lines\-border\fR must also be \fBinline\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-header\-label" [=LABEL]
|
||||
@@ -1116,6 +1122,10 @@ Display header from \fB--header\-lines\fR with a separate border. Pass
|
||||
\fBnone\fR to still separate the header lines but without a border. To combine
|
||||
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
|
||||
a single separator line between the header lines and the list section.
|
||||
\fBinline\fR style embeds the header lines inside the list border frame
|
||||
with a T-junction separator; it requires a line-drawing
|
||||
\fB\-\-list\-border\fR, falls back to \fBline\fR otherwise, and is not
|
||||
compatible with \fB\-\-header\-first\fR.
|
||||
|
||||
.SS FOOTER
|
||||
|
||||
@@ -1129,7 +1139,10 @@ are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even whe
|
||||
.TP
|
||||
.BI "\-\-footer\-border" [=STYLE]
|
||||
Draw border around the footer section. \fBline\fR style draws a single
|
||||
separator line between the footer and the list section.
|
||||
separator line between the footer and the list section. \fBinline\fR style
|
||||
embeds the footer inside the list border frame with a T-junction separator;
|
||||
it requires a line-drawing \fB\-\-list\-border\fR and falls back to
|
||||
\fBline\fR otherwise.
|
||||
|
||||
.TP
|
||||
.BI "\-\-footer\-label" [=LABEL]
|
||||
|
||||
+22
-3
@@ -178,10 +178,11 @@ Usage: fzf [options]
|
||||
--header-first Print header before the prompt line
|
||||
--header-border[=STYLE] Draw border around the header section
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|line|none] (default: rounded)
|
||||
top|bottom|left|right|line|inline|none] (default: rounded)
|
||||
--header-lines-border[=STYLE]
|
||||
Display header from --header-lines with a separate border.
|
||||
Pass 'none' to still separate it but without a border.
|
||||
Pass 'inline' to embed it inside the list frame.
|
||||
--header-label=LABEL Label to print on the header border
|
||||
--header-label-pos=COL Position of the header label
|
||||
[POSITIVE_INTEGER: columns from left|
|
||||
@@ -192,7 +193,7 @@ Usage: fzf [options]
|
||||
--footer=STR String to print as footer
|
||||
--footer-border[=STYLE] Draw border around the footer section
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|line|none] (default: line)
|
||||
top|bottom|left|right|line|inline|none] (default: line)
|
||||
--footer-label=LABEL Label to print on the footer border
|
||||
--footer-label-pos=COL Position of the footer label
|
||||
[POSITIVE_INTEGER: columns from left|
|
||||
@@ -953,6 +954,8 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
|
||||
switch str {
|
||||
case "line":
|
||||
return tui.BorderLine, nil
|
||||
case "inline":
|
||||
return tui.BorderInline, nil
|
||||
case "rounded":
|
||||
return tui.BorderRounded, nil
|
||||
case "sharp":
|
||||
@@ -983,7 +986,7 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
|
||||
if optional && str == "" {
|
||||
return defaultBorderShape, nil
|
||||
}
|
||||
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|line|inline|none)")
|
||||
}
|
||||
|
||||
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
|
||||
@@ -3610,6 +3613,22 @@ func validateOptions(opts *Options) error {
|
||||
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
|
||||
}
|
||||
|
||||
if opts.BorderShape == tui.BorderInline ||
|
||||
opts.ListBorderShape == tui.BorderInline ||
|
||||
opts.InputBorderShape == tui.BorderInline ||
|
||||
opts.Preview.border == tui.BorderInline {
|
||||
return errors.New("inline border is only supported for --header-border, --header-lines-border, and --footer-border")
|
||||
}
|
||||
if opts.HeaderFirst && (opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline) {
|
||||
return errors.New("--header-first is not compatible with --header-border=inline or --header-lines-border=inline")
|
||||
}
|
||||
if opts.HeaderBorderShape == tui.BorderInline &&
|
||||
opts.HeaderLinesShape != tui.BorderInline &&
|
||||
opts.HeaderLinesShape != tui.BorderUndefined &&
|
||||
opts.HeaderLinesShape != tui.BorderNone {
|
||||
return errors.New("--header-border=inline requires --header-lines-border to be inline or unset")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+302
-66
@@ -1168,7 +1168,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
baseTheme = renderer.DefaultTheme()
|
||||
}
|
||||
// This should be called before accessing tui.Color*
|
||||
tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
|
||||
headerInline := opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline
|
||||
footerInline := opts.FooterBorderShape == tui.BorderInline
|
||||
hasHeader := opts.HeaderBorderShape.Visible() || opts.HeaderLinesShape.Visible()
|
||||
tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), hasHeader, headerInline, footerInline)
|
||||
|
||||
// Gutter character
|
||||
var gutterChar, gutterRawChar string
|
||||
@@ -1227,6 +1230,22 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// horizontal separator (same as BorderLine).
|
||||
inlineSupported := t.listBorderShape.HasTop() && t.listBorderShape.HasBottom()
|
||||
if !inlineSupported {
|
||||
if t.headerBorderShape == tui.BorderInline {
|
||||
t.headerBorderShape = tui.BorderLine
|
||||
}
|
||||
if t.headerLinesShape == tui.BorderInline {
|
||||
t.headerLinesShape = tui.BorderLine
|
||||
}
|
||||
if t.footerBorderShape == tui.BorderInline {
|
||||
t.footerBorderShape = tui.BorderLine
|
||||
}
|
||||
}
|
||||
|
||||
// Determine header border shape
|
||||
if t.headerBorderShape == tui.BorderLine {
|
||||
if t.layout == layoutReverse {
|
||||
@@ -2237,6 +2256,98 @@ func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
|
||||
return false, tui.BorderNone
|
||||
}
|
||||
|
||||
// Inline sections live inside wborder rather than consuming shift/shrink/availableLines.
|
||||
type inlineRole int
|
||||
|
||||
const (
|
||||
inlineRoleHeader inlineRole = iota
|
||||
inlineRoleHeaderLines
|
||||
inlineRoleFooter
|
||||
)
|
||||
|
||||
type inlineSlot struct {
|
||||
role inlineRole
|
||||
windowType tui.WindowType
|
||||
contentLines int // 0 when out of budget: a ghost placeholder is created so
|
||||
// t.headerWindow / t.footerWindow stay non-nil but no frame is painted.
|
||||
label labelPrinter
|
||||
labelOpts labelOpts
|
||||
labelLen int
|
||||
}
|
||||
|
||||
// inlineMetaFor returns (onTop: top stack vs bottom; windowType; isInner:
|
||||
// adjacent to list content) for the given inline role, derived from the layout.
|
||||
// Header is outer when paired with header-lines; header-lines is always inner;
|
||||
// footer is inner except in reverseList where it sits outside header-lines.
|
||||
func (t *Terminal) inlineMetaFor(role inlineRole) (onTop bool, windowType tui.WindowType, isInner bool) {
|
||||
switch role {
|
||||
case inlineRoleHeader:
|
||||
return t.layout == layoutReverse, tui.WindowHeader, false
|
||||
case inlineRoleHeaderLines:
|
||||
return t.layout != layoutDefault, tui.WindowHeader, true
|
||||
case inlineRoleFooter:
|
||||
return t.layout != layoutReverse, tui.WindowFooter, t.layout != layoutReverseList
|
||||
}
|
||||
return false, tui.WindowBase, false
|
||||
}
|
||||
|
||||
func (t *Terminal) placeInlineSection(win tui.Window, role inlineRole) {
|
||||
switch role {
|
||||
case inlineRoleHeader:
|
||||
t.headerWindow = win
|
||||
case inlineRoleHeaderLines:
|
||||
t.headerLinesWindow = win
|
||||
case inlineRoleFooter:
|
||||
t.footerWindow = win
|
||||
}
|
||||
}
|
||||
|
||||
// placeInlineStack walks `slots` in outer-to-inner order. Only the outermost
|
||||
// slot (index 0) claims the adjacent wborder edge; later slots paint side-only
|
||||
// frames so the T-junction separator lands between sections.
|
||||
func (t *Terminal) placeInlineStack(slots []inlineSlot, startRow int, onTop bool) {
|
||||
firstEdge := tui.SectionEdgeTop
|
||||
if !onTop {
|
||||
firstEdge = tui.SectionEdgeBottom
|
||||
}
|
||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||
subLeft := t.window.Left()
|
||||
subWidth := t.window.Width()
|
||||
cursor := startRow
|
||||
for i, s := range slots {
|
||||
var windowTop, sepRow int
|
||||
if onTop {
|
||||
windowTop = cursor
|
||||
sepRow = cursor + s.contentLines - t.wborder.Top()
|
||||
} else {
|
||||
windowTop = cursor - s.contentLines + 1
|
||||
sepRow = windowTop - 1 - t.wborder.Top()
|
||||
}
|
||||
// 0-height placeholder keeps t.headerWindow / t.footerWindow non-nil.
|
||||
win := t.tui.NewWindow(windowTop, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
|
||||
t.placeInlineSection(win, s.role)
|
||||
if s.contentLines == 0 {
|
||||
continue
|
||||
}
|
||||
secTop := windowTop - t.wborder.Top()
|
||||
secBottom := secTop + s.contentLines - 1
|
||||
edge := tui.SectionEdgeNone
|
||||
if i == 0 {
|
||||
edge = firstEdge
|
||||
}
|
||||
t.wborder.PaintSectionFrame(secTop, secBottom, s.windowType, edge)
|
||||
// useBottom=onTop so the separator always hugs the list (inner side).
|
||||
// Matters for thinblock/block where top != bottom char.
|
||||
t.wborder.DrawHSeparator(sepRow, s.windowType, onTop)
|
||||
t.printLabelAt(t.wborder, s.label, s.labelOpts, s.labelLen, sepRow)
|
||||
if onTop {
|
||||
cursor += s.contentLines + 1
|
||||
} else {
|
||||
cursor -= s.contentLines + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
t.clearNumLinesCache()
|
||||
t.forcePreview = forcePreview
|
||||
@@ -2353,6 +2464,50 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Slices are ordered outer-to-inner: index 0 touches wborder's edge.
|
||||
var inlineTop []inlineSlot
|
||||
var inlineBottom []inlineSlot
|
||||
|
||||
// Caps contentLines against remaining wborder space. Oversized requests (e.g. a
|
||||
// huge --header-lines) become 0-height placeholders rather than pushing the list
|
||||
// window to negative height.
|
||||
addInline := func(contentLines int, role inlineRole) {
|
||||
onTop, windowType, isInner := t.inlineMetaFor(role)
|
||||
used := 0
|
||||
for _, s := range inlineTop {
|
||||
if s.contentLines > 0 {
|
||||
used += s.contentLines + 1
|
||||
}
|
||||
}
|
||||
for _, s := range inlineBottom {
|
||||
if s.contentLines > 0 {
|
||||
used += s.contentLines + 1
|
||||
}
|
||||
}
|
||||
remaining := availableLines - borderLines(t.listBorderShape) - used - 1
|
||||
if remaining < 2 {
|
||||
contentLines = 0
|
||||
} else {
|
||||
contentLines = util.Constrain(contentLines, 1, remaining-1)
|
||||
}
|
||||
slot := inlineSlot{role: role, windowType: windowType, contentLines: contentLines}
|
||||
switch role {
|
||||
case inlineRoleHeader:
|
||||
slot.label, slot.labelOpts, slot.labelLen = t.headerLabel, t.headerLabelOpts, t.headerLabelLen
|
||||
case inlineRoleFooter:
|
||||
slot.label, slot.labelOpts, slot.labelLen = t.footerLabel, t.footerLabelOpts, t.footerLabelLen
|
||||
}
|
||||
target := &inlineTop
|
||||
if !onTop {
|
||||
target = &inlineBottom
|
||||
}
|
||||
if isInner {
|
||||
*target = append(*target, slot)
|
||||
} else {
|
||||
*target = append([]inlineSlot{slot}, *target...)
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust position and size of the list window if header border is set
|
||||
headerBorderHeight := 0
|
||||
if hasHeaderWindow {
|
||||
@@ -2360,37 +2515,62 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
if hasHeaderLinesWindow {
|
||||
headerWindowHeight -= t.headerLines
|
||||
}
|
||||
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
|
||||
if t.layout == layoutReverse {
|
||||
shift += headerBorderHeight
|
||||
shrink += headerBorderHeight
|
||||
if t.headerBorderShape == tui.BorderInline {
|
||||
addInline(headerWindowHeight, inlineRoleHeader)
|
||||
} else {
|
||||
shrink += headerBorderHeight
|
||||
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
|
||||
if t.layout == layoutReverse {
|
||||
shift += headerBorderHeight
|
||||
shrink += headerBorderHeight
|
||||
} else {
|
||||
shrink += headerBorderHeight
|
||||
}
|
||||
availableLines -= headerBorderHeight
|
||||
}
|
||||
availableLines -= headerBorderHeight
|
||||
}
|
||||
|
||||
headerLinesHeight := 0
|
||||
if hasHeaderLinesWindow {
|
||||
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
|
||||
if t.layout != layoutDefault {
|
||||
shift += headerLinesHeight
|
||||
shrink += headerLinesHeight
|
||||
if headerLinesShape == tui.BorderInline {
|
||||
addInline(t.headerLines, inlineRoleHeaderLines)
|
||||
} else {
|
||||
shrink += headerLinesHeight
|
||||
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
|
||||
if t.layout != layoutDefault {
|
||||
shift += headerLinesHeight
|
||||
shrink += headerLinesHeight
|
||||
} else {
|
||||
shrink += headerLinesHeight
|
||||
}
|
||||
availableLines -= headerLinesHeight
|
||||
}
|
||||
availableLines -= headerLinesHeight
|
||||
}
|
||||
|
||||
footerBorderHeight := 0
|
||||
if hasFooterWindow {
|
||||
// Footer lines should not take all available lines
|
||||
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
|
||||
shrink += footerBorderHeight
|
||||
if t.layout != layoutReverse {
|
||||
shift += footerBorderHeight
|
||||
if t.footerBorderShape == tui.BorderInline {
|
||||
addInline(len(t.footer), inlineRoleFooter)
|
||||
} else {
|
||||
// Footer lines should not take all available lines
|
||||
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
|
||||
shrink += footerBorderHeight
|
||||
if t.layout != layoutReverse {
|
||||
shift += footerBorderHeight
|
||||
}
|
||||
availableLines -= footerBorderHeight
|
||||
}
|
||||
}
|
||||
|
||||
inlineTopLines := 0
|
||||
for _, s := range inlineTop {
|
||||
if s.contentLines > 0 {
|
||||
inlineTopLines += s.contentLines + 1
|
||||
}
|
||||
}
|
||||
inlineBottomLines := 0
|
||||
for _, s := range inlineBottom {
|
||||
if s.contentLines > 0 {
|
||||
inlineBottomLines += s.contentLines + 1
|
||||
}
|
||||
availableLines -= footerBorderHeight
|
||||
}
|
||||
|
||||
// Set up list border
|
||||
@@ -2501,12 +2681,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
if previewOpts.position == posUp {
|
||||
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
||||
t.window = t.tui.NewWindow(
|
||||
innerMarginInt[0]+pheight+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
|
||||
innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
|
||||
} else {
|
||||
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
|
||||
t.window = t.tui.NewWindow(
|
||||
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
|
||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
||||
}
|
||||
case posLeft, posRight:
|
||||
@@ -2545,7 +2725,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
m = 1
|
||||
}
|
||||
t.window = t.tui.NewWindow(
|
||||
innerMarginInt[0]+shift, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||
|
||||
// Clear characters on the margin
|
||||
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1
|
||||
@@ -2577,7 +2757,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
|
||||
t.window = t.tui.NewWindow(
|
||||
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||
x := marginInt[3] + width - pwidth
|
||||
createPreviewWindow(marginInt[0], x, pwidth, height)
|
||||
}
|
||||
@@ -2615,10 +2795,15 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
innerBorderFn(marginInt[0], marginInt[3], width, height)
|
||||
t.window = t.tui.NewWindow(
|
||||
innerMarginInt[0]+shift,
|
||||
innerMarginInt[0]+shift+inlineTopLines,
|
||||
innerMarginInt[3],
|
||||
innerWidth,
|
||||
innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||
innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
||||
}
|
||||
|
||||
if len(inlineTop)+len(inlineBottom) > 0 && t.wborder != nil {
|
||||
t.placeInlineStack(inlineTop, t.window.Top()-inlineTopLines, true)
|
||||
t.placeInlineStack(inlineBottom, t.window.Top()+t.window.Height()+inlineBottomLines-1, false)
|
||||
}
|
||||
|
||||
if len(t.scrollbar) == 0 {
|
||||
@@ -2700,7 +2885,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
|
||||
// Set up header border
|
||||
if hasHeaderWindow {
|
||||
if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
|
||||
var btop int
|
||||
if hasInputWindow && t.headerFirst {
|
||||
if t.layout == layoutReverse {
|
||||
@@ -2728,7 +2913,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
|
||||
// Set up header lines border
|
||||
if hasHeaderLinesWindow {
|
||||
if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
|
||||
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
|
||||
@@ -2761,7 +2946,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
}
|
||||
|
||||
// Set up footer
|
||||
if hasFooterWindow {
|
||||
if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
|
||||
var btop int
|
||||
if t.layout == layoutReverse {
|
||||
btop = w.Top() + w.Height()
|
||||
@@ -2778,8 +2963,21 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0)
|
||||
}
|
||||
|
||||
// Print border label
|
||||
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false)
|
||||
// When the list label lands on an edge owned by an inline section, swap its bg
|
||||
// so the label reads as part of that section's frame. Fg stays at list-label.
|
||||
listLabel, listLabelLen := t.listLabel, t.listLabelLen
|
||||
var adjacentSection *inlineSlot
|
||||
if t.listLabelOpts.bottom && len(inlineBottom) > 0 {
|
||||
adjacentSection = &inlineBottom[0]
|
||||
} else if !t.listLabelOpts.bottom && len(inlineTop) > 0 {
|
||||
adjacentSection = &inlineTop[0]
|
||||
}
|
||||
if adjacentSection != nil {
|
||||
bg := tui.BorderColor(adjacentSection.windowType).Bg()
|
||||
custom := tui.ColListLabel.WithBg(tui.ColorAttr{Color: bg, Attr: tui.AttrUndefined})
|
||||
listLabel, listLabelLen = t.ansiLabelPrinter(t.listLabelOpts.label, &custom, false)
|
||||
}
|
||||
t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
|
||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
|
||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false)
|
||||
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
|
||||
@@ -2787,37 +2985,39 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
|
||||
}
|
||||
|
||||
// printLabelAt positions and renders a label at the given row of `window`. Shared by
|
||||
// printLabel (which computes row from the border shape) and the inline-section label
|
||||
// code (which uses an explicit separator row).
|
||||
func (t *Terminal) printLabelAt(window tui.Window, render labelPrinter, opts labelOpts, length int, row int) {
|
||||
if window == nil || render == nil || window.Height() == 0 {
|
||||
return
|
||||
}
|
||||
var col int
|
||||
if opts.column == 0 {
|
||||
col = max(0, (window.Width()-length)/2)
|
||||
} else if opts.column < 0 {
|
||||
col = max(0, window.Width()+opts.column+1-length)
|
||||
} else {
|
||||
col = min(opts.column-1, window.Width()-length)
|
||||
}
|
||||
window.Move(row, col)
|
||||
render(window, window.Width())
|
||||
}
|
||||
|
||||
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
|
||||
if window == nil {
|
||||
if window == nil || window.Height() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if window.Height() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
switch borderShape {
|
||||
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
|
||||
if redrawBorder {
|
||||
window.DrawHBorder()
|
||||
}
|
||||
if render == nil {
|
||||
return
|
||||
}
|
||||
var col int
|
||||
if opts.column == 0 {
|
||||
col = max(0, (window.Width()-length)/2)
|
||||
} else if opts.column < 0 {
|
||||
col = max(0, window.Width()+opts.column+1-length)
|
||||
} else {
|
||||
col = min(opts.column-1, window.Width()-length)
|
||||
}
|
||||
row := 0
|
||||
if borderShape == tui.BorderBottom || opts.bottom {
|
||||
row = window.Height() - 1
|
||||
}
|
||||
window.Move(row, col)
|
||||
render(window, window.Width())
|
||||
t.printLabelAt(window, render, opts, length, row)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3185,8 +3385,19 @@ func (t *Terminal) resizeIfNeeded() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Inline sections are budget-capped inside wborder, so window.Height() may be
|
||||
// smaller than the requested content length. Treat "capped" (height < want) as
|
||||
// a no-op to avoid triggering a full redraw on every info/header/footer event
|
||||
// when the user has requested more content than fits.
|
||||
mismatch := func(shape tui.BorderShape, height, want int) bool {
|
||||
if shape == tui.BorderInline && height < want {
|
||||
return false
|
||||
}
|
||||
return height != want
|
||||
}
|
||||
|
||||
// Check footer window
|
||||
if len(t.footer) > 0 && (t.footerWindow == nil || t.footerWindow.Height() != len(t.footer)) ||
|
||||
if len(t.footer) > 0 && (t.footerWindow == nil || mismatch(t.footerBorderShape, t.footerWindow.Height(), len(t.footer))) ||
|
||||
len(t.footer) == 0 && t.footerWindow != nil {
|
||||
t.printAll()
|
||||
return true
|
||||
@@ -3200,14 +3411,12 @@ func (t *Terminal) resizeIfNeeded() bool {
|
||||
if needHeaderLinesWindow {
|
||||
primaryHeaderLines -= t.headerLines
|
||||
}
|
||||
// FIXME: Full redraw is triggered if there are too many lines in the header
|
||||
// so that the header window cannot display all of them.
|
||||
if (needHeaderWindow && t.headerWindow == nil) ||
|
||||
(!needHeaderWindow && t.headerWindow != nil) ||
|
||||
(needHeaderWindow && t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
|
||||
(needHeaderWindow && t.headerWindow != nil && mismatch(t.headerBorderShape, t.headerWindow.Height(), primaryHeaderLines)) ||
|
||||
(needHeaderLinesWindow && t.headerLinesWindow == nil) ||
|
||||
(!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
|
||||
(needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
|
||||
(needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) {
|
||||
t.printAll()
|
||||
return true
|
||||
}
|
||||
@@ -3219,14 +3428,21 @@ func (t *Terminal) printHeader() {
|
||||
return
|
||||
}
|
||||
|
||||
t.withWindow(t.headerWindow, func() {
|
||||
var headerItems []Item
|
||||
if !t.hasHeaderLinesWindow() {
|
||||
headerItems = t.header
|
||||
}
|
||||
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
|
||||
})
|
||||
if w, shape := t.determineHeaderLinesShape(); w {
|
||||
// When an inline section was requested but addInline had no budget, its window is
|
||||
// nil. Don't fall through to withWindow — that would leak header content into the
|
||||
// list window. A nil window is only legitimate when the shape is NOT inline (e.g.
|
||||
// header combined with the list when --no-list-border is in effect).
|
||||
if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) {
|
||||
t.withWindow(t.headerWindow, func() {
|
||||
var headerItems []Item
|
||||
if !t.hasHeaderLinesWindow() {
|
||||
headerItems = t.header
|
||||
}
|
||||
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
|
||||
})
|
||||
}
|
||||
if w, shape := t.determineHeaderLinesShape(); w &&
|
||||
!(shape == tui.BorderInline && t.headerLinesWindow == nil) {
|
||||
t.withWindow(t.headerLinesWindow, func() {
|
||||
t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
|
||||
})
|
||||
@@ -3277,7 +3493,10 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
|
||||
if t.listBorderShape.HasLeft() {
|
||||
indentSize += 1 + t.borderWidth
|
||||
}
|
||||
if borderShape.HasLeft() {
|
||||
// Section borders with their own left side skip past the list border's left column.
|
||||
// Inline sections also skip it, but only when the list border actually has a left,
|
||||
// since otherwise the inline window starts flush with the list window.
|
||||
if borderShape.HasLeft() || (borderShape == tui.BorderInline && t.listBorderShape.HasLeft()) {
|
||||
indentSize -= 1 + t.borderWidth
|
||||
if indentSize < 0 {
|
||||
indentSize = 0
|
||||
@@ -5952,11 +6171,28 @@ func (t *Terminal) Loop() error {
|
||||
case reqRedrawInputLabel:
|
||||
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
|
||||
case reqRedrawHeaderLabel:
|
||||
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
|
||||
if t.headerBorderShape == tui.BorderInline {
|
||||
// Inline labels sit on the separator inside wborder; re-run the
|
||||
// full layout to repaint the separator + label together.
|
||||
t.printAll()
|
||||
} else {
|
||||
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
|
||||
}
|
||||
case reqRedrawFooterLabel:
|
||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
|
||||
if t.footerBorderShape == tui.BorderInline {
|
||||
t.printAll()
|
||||
} else {
|
||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
|
||||
}
|
||||
case reqRedrawListLabel:
|
||||
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
|
||||
// When inline sections are active, the label's bg depends on which
|
||||
// section owns the adjacent edge. Rerun the layout to reuse that
|
||||
// logic rather than duplicating it here.
|
||||
if t.headerBorderShape == tui.BorderInline || t.headerLinesShape == tui.BorderInline || t.footerBorderShape == tui.BorderInline {
|
||||
t.printAll()
|
||||
} else {
|
||||
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
|
||||
}
|
||||
case reqRedrawBorderLabel:
|
||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
||||
case reqRedrawPreviewLabel:
|
||||
|
||||
+124
-109
@@ -1129,127 +1129,142 @@ func (w *LightWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
// drawHLine fills row `row` with `line` between optional left/right caps.
|
||||
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
|
||||
func (w *LightWindow) drawHLine(row int, line, leftCap, rightCap rune, color ColorPair) {
|
||||
w.Move(row, 0)
|
||||
hw := runeWidth(line)
|
||||
width := w.width
|
||||
if leftCap != 0 {
|
||||
w.CPrint(color, string(leftCap))
|
||||
width -= runeWidth(leftCap)
|
||||
}
|
||||
if rightCap != 0 {
|
||||
width -= runeWidth(rightCap)
|
||||
}
|
||||
if width < 0 {
|
||||
width = 0
|
||||
}
|
||||
inner := width / hw
|
||||
rem := width - inner*hw
|
||||
w.CPrint(color, repeat(line, inner)+repeat(' ', rem))
|
||||
if rightCap != 0 {
|
||||
w.CPrint(color, string(rightCap))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
}
|
||||
shape := w.border.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
}
|
||||
color := BorderColor(windowType)
|
||||
line := w.border.top
|
||||
if useBottom {
|
||||
line = w.border.bottom
|
||||
}
|
||||
var leftCap, rightCap rune
|
||||
if shape.HasLeft() || shape.HasRight() {
|
||||
leftCap = w.border.leftMid
|
||||
rightCap = w.border.rightMid
|
||||
}
|
||||
w.drawHLine(row, line, leftCap, rightCap, color)
|
||||
}
|
||||
|
||||
func (w *LightWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
|
||||
if w.height == 0 || w.border.shape == BorderNone {
|
||||
return
|
||||
}
|
||||
color := BorderColor(windowType)
|
||||
shape := w.border.shape
|
||||
hasLeft := shape.HasLeft()
|
||||
hasRight := shape.HasRight()
|
||||
rightW := runeWidth(w.border.right)
|
||||
// Content rows: overpaint left/right verticals + their 1-char margin.
|
||||
for row := topContent; row <= bottomContent; row++ {
|
||||
if hasLeft {
|
||||
w.Move(row, 0)
|
||||
w.CPrint(color, string(w.border.left)+" ")
|
||||
}
|
||||
if hasRight {
|
||||
w.Move(row, w.width-rightW-1)
|
||||
w.CPrint(color, " "+string(w.border.right))
|
||||
}
|
||||
}
|
||||
if edge == SectionEdgeTop && shape.HasTop() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.border.topLeft
|
||||
}
|
||||
if hasRight {
|
||||
rightCap = w.border.topRight
|
||||
}
|
||||
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
|
||||
}
|
||||
if edge == SectionEdgeBottom && shape.HasBottom() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.border.bottomLeft
|
||||
}
|
||||
if hasRight {
|
||||
rightCap = w.border.bottomRight
|
||||
}
|
||||
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
}
|
||||
switch w.border.shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||
w.drawBorderAround(onlyHorizontal)
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal(true, true)
|
||||
case BorderVertical:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, true)
|
||||
case BorderTop:
|
||||
w.drawBorderHorizontal(true, false)
|
||||
case BorderBottom:
|
||||
w.drawBorderHorizontal(false, true)
|
||||
case BorderLeft:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, false)
|
||||
case BorderRight:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(false, true)
|
||||
shape := w.border.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
}
|
||||
}
|
||||
color := BorderColor(w.windowType)
|
||||
hasLeft := shape.HasLeft()
|
||||
hasRight := shape.HasRight()
|
||||
|
||||
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
color := ColBorder
|
||||
switch w.windowType {
|
||||
case WindowList:
|
||||
color = ColListBorder
|
||||
case WindowInput:
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runeWidth(w.border.top)
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(color, repeat(w.border.top, w.width/hw))
|
||||
}
|
||||
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
vw := runeWidth(w.border.left)
|
||||
color := ColBorder
|
||||
switch w.windowType {
|
||||
case WindowList:
|
||||
color = ColListBorder
|
||||
case WindowInput:
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
for y := 0; y < w.height; y++ {
|
||||
if left {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.left))
|
||||
w.CPrint(color, " ") // Margin
|
||||
if shape.HasTop() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.border.topLeft
|
||||
}
|
||||
if right {
|
||||
w.Move(y, w.width-vw-1)
|
||||
w.CPrint(color, " ") // Margin
|
||||
w.CPrint(color, string(w.border.right))
|
||||
if hasRight {
|
||||
rightCap = w.border.topRight
|
||||
}
|
||||
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
w.Move(0, 0)
|
||||
color := ColBorder
|
||||
switch w.windowType {
|
||||
case WindowList:
|
||||
color = ColListBorder
|
||||
case WindowInput:
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runeWidth(w.border.top)
|
||||
tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
|
||||
bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
|
||||
rem := (w.width - tcw) % hw
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
if !onlyHorizontal {
|
||||
if !onlyHorizontal && (hasLeft || hasRight) {
|
||||
vw := runeWidth(w.border.left)
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.left))
|
||||
w.CPrint(color, " ") // Margin
|
||||
|
||||
w.Move(y, w.width-vw-1)
|
||||
w.CPrint(color, " ") // Margin
|
||||
w.CPrint(color, string(w.border.right))
|
||||
for y := 0; y < w.height; y++ {
|
||||
// Corner rows are already painted by drawHLine above / below.
|
||||
if (y == 0 && shape.HasTop()) || (y == w.height-1 && shape.HasBottom()) {
|
||||
continue
|
||||
}
|
||||
if hasLeft {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.left)+" ")
|
||||
}
|
||||
if hasRight {
|
||||
w.Move(y, w.width-vw-1)
|
||||
w.CPrint(color, " "+string(w.border.right))
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Move(w.height-1, 0)
|
||||
rem = (w.width - bcw) % hw
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||
if shape.HasBottom() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.border.bottomLeft
|
||||
}
|
||||
if hasRight {
|
||||
rightCap = w.border.bottomRight
|
||||
}
|
||||
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) string {
|
||||
|
||||
+133
-54
@@ -1017,6 +1017,113 @@ func (w *TcellWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
// borderStyleFor returns the tcell.Style used to draw borders for `wt`, honoring
|
||||
// whether the window is rendering with colors.
|
||||
func (w *TcellWindow) borderStyleFor(wt WindowType) tcell.Style {
|
||||
if !w.color {
|
||||
return w.normal.style()
|
||||
}
|
||||
return BorderColor(wt).style()
|
||||
}
|
||||
|
||||
// drawHLine fills row `y` with `line` between optional left/right caps.
|
||||
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
|
||||
// tcell has an issue displaying two overlapping wide runes, so the line
|
||||
// stops before the cap position rather than overpainting.
|
||||
func (w *TcellWindow) drawHLine(y int, line, leftCap, rightCap rune, style tcell.Style) {
|
||||
left := w.left
|
||||
right := left + w.width
|
||||
hw := runeWidth(line)
|
||||
lw := 0
|
||||
rw := 0
|
||||
if leftCap != 0 {
|
||||
lw = runeWidth(leftCap)
|
||||
}
|
||||
if rightCap != 0 {
|
||||
rw = runeWidth(rightCap)
|
||||
}
|
||||
for x := left + lw; x <= right-rw-hw; x += hw {
|
||||
_screen.SetContent(x, y, line, nil, style)
|
||||
}
|
||||
if leftCap != 0 {
|
||||
_screen.SetContent(left, y, leftCap, nil, style)
|
||||
}
|
||||
if rightCap != 0 {
|
||||
_screen.SetContent(right-rw, y, rightCap, nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
}
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
}
|
||||
style := w.borderStyleFor(windowType)
|
||||
line := w.borderStyle.top
|
||||
if useBottom {
|
||||
line = w.borderStyle.bottom
|
||||
}
|
||||
var leftCap, rightCap rune
|
||||
if shape.HasLeft() || shape.HasRight() {
|
||||
leftCap = w.borderStyle.leftMid
|
||||
rightCap = w.borderStyle.rightMid
|
||||
}
|
||||
w.drawHLine(w.top+row, line, leftCap, rightCap, style)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
}
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
}
|
||||
style := w.borderStyleFor(windowType)
|
||||
left := w.left
|
||||
right := left + w.width
|
||||
hasLeft := shape.HasLeft()
|
||||
hasRight := shape.HasRight()
|
||||
leftW := runeWidth(w.borderStyle.left)
|
||||
rightW := runeWidth(w.borderStyle.right)
|
||||
// Content rows: overpaint the left and right verticals (+ their 1-char margin) in
|
||||
// the section's color. Inner margin stays at whatever bg the sub-window set.
|
||||
for row := topContent; row <= bottomContent; row++ {
|
||||
y := w.top + row
|
||||
if hasLeft {
|
||||
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
||||
_screen.SetContent(left+leftW, y, ' ', nil, style)
|
||||
}
|
||||
if hasRight {
|
||||
_screen.SetContent(right-rightW-1, y, ' ', nil, style)
|
||||
_screen.SetContent(right-rightW, y, w.borderStyle.right, nil, style)
|
||||
}
|
||||
}
|
||||
if edge == SectionEdgeTop && shape.HasTop() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.borderStyle.topLeft
|
||||
}
|
||||
if hasRight {
|
||||
rightCap = w.borderStyle.topRight
|
||||
}
|
||||
w.drawHLine(w.top, w.borderStyle.top, leftCap, rightCap, style)
|
||||
}
|
||||
if edge == SectionEdgeBottom && shape.HasBottom() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.borderStyle.bottomLeft
|
||||
}
|
||||
if hasRight {
|
||||
rightCap = w.borderStyle.bottomRight
|
||||
}
|
||||
w.drawHLine(w.top+w.height-1, w.borderStyle.bottom, leftCap, rightCap, style)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
if w.height == 0 {
|
||||
return
|
||||
@@ -1031,72 +1138,44 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
top := w.top
|
||||
bot := top + w.height
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
switch w.windowType {
|
||||
case WindowBase:
|
||||
style = ColBorder.style()
|
||||
case WindowList:
|
||||
style = ColListBorder.style()
|
||||
case WindowHeader:
|
||||
style = ColHeaderBorder.style()
|
||||
case WindowFooter:
|
||||
style = ColFooterBorder.style()
|
||||
case WindowInput:
|
||||
style = ColInputBorder.style()
|
||||
case WindowPreview:
|
||||
style = ColPreviewBorder.style()
|
||||
}
|
||||
} else {
|
||||
style = w.normal.style()
|
||||
}
|
||||
style := w.borderStyleFor(w.windowType)
|
||||
|
||||
hw := runeWidth(w.borderStyle.top)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderTop {
|
||||
max = right - hw
|
||||
hasLeft := shape.HasLeft()
|
||||
hasRight := shape.HasRight()
|
||||
|
||||
if shape.HasTop() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.borderStyle.topLeft
|
||||
}
|
||||
// tcell has an issue displaying two overlapping wide runes
|
||||
// e.g. SetContent( HH )
|
||||
// SetContent( TR )
|
||||
// ==================
|
||||
// ( HH ) => TR is ignored
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
|
||||
if hasRight {
|
||||
rightCap = w.borderStyle.topRight
|
||||
}
|
||||
w.drawHLine(top, w.borderStyle.top, leftCap, rightCap, style)
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderBottom {
|
||||
max = right - hw
|
||||
if shape.HasBottom() {
|
||||
var leftCap, rightCap rune
|
||||
if hasLeft {
|
||||
leftCap = w.borderStyle.bottomLeft
|
||||
}
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
|
||||
if hasRight {
|
||||
rightCap = w.borderStyle.bottomRight
|
||||
}
|
||||
w.drawHLine(bot-1, w.borderStyle.bottom, leftCap, rightCap, style)
|
||||
}
|
||||
if !onlyHorizontal {
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
vw := runeWidth(w.borderStyle.right)
|
||||
for y := top; y < bot; y++ {
|
||||
// Corner rows are already painted by drawHLine above / below.
|
||||
if (y == top && shape.HasTop()) || (y == bot-1 && shape.HasBottom()) {
|
||||
continue
|
||||
}
|
||||
if hasLeft {
|
||||
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runeWidth(w.borderStyle.right)
|
||||
for y := top; y < bot; y++ {
|
||||
if hasRight {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
_screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
+80
-8
@@ -595,11 +595,12 @@ const (
|
||||
BorderBottom
|
||||
BorderLeft
|
||||
BorderRight
|
||||
BorderInline
|
||||
)
|
||||
|
||||
func (s BorderShape) HasLeft() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -607,7 +608,7 @@ func (s BorderShape) HasLeft() bool {
|
||||
|
||||
func (s BorderShape) HasRight() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -615,7 +616,7 @@ func (s BorderShape) HasRight() bool {
|
||||
|
||||
func (s BorderShape) HasTop() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -623,7 +624,7 @@ func (s BorderShape) HasTop() bool {
|
||||
|
||||
func (s BorderShape) HasBottom() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -643,6 +644,8 @@ type BorderStyle struct {
|
||||
topRight rune
|
||||
bottomLeft rune
|
||||
bottomRight rune
|
||||
leftMid rune
|
||||
rightMid rune
|
||||
}
|
||||
|
||||
type BorderCharacter int
|
||||
@@ -658,7 +661,9 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
bottomRight: ' '}
|
||||
bottomRight: ' ',
|
||||
leftMid: ' ',
|
||||
rightMid: ' '}
|
||||
}
|
||||
if !unicode {
|
||||
return BorderStyle{
|
||||
@@ -671,6 +676,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '+',
|
||||
bottomLeft: '+',
|
||||
bottomRight: '+',
|
||||
leftMid: '+',
|
||||
rightMid: '+',
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
@@ -685,6 +692,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '┐',
|
||||
bottomLeft: '└',
|
||||
bottomRight: '┘',
|
||||
leftMid: '├',
|
||||
rightMid: '┤',
|
||||
}
|
||||
case BorderBold:
|
||||
return BorderStyle{
|
||||
@@ -697,6 +706,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '┓',
|
||||
bottomLeft: '┗',
|
||||
bottomRight: '┛',
|
||||
leftMid: '┣',
|
||||
rightMid: '┫',
|
||||
}
|
||||
case BorderBlock:
|
||||
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
|
||||
@@ -712,6 +723,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '▜',
|
||||
bottomLeft: '▙',
|
||||
bottomRight: '▟',
|
||||
leftMid: '▌',
|
||||
rightMid: '▐',
|
||||
}
|
||||
|
||||
case BorderThinBlock:
|
||||
@@ -728,6 +741,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '🭾',
|
||||
bottomLeft: '🭼',
|
||||
bottomRight: '🭿',
|
||||
leftMid: '▏',
|
||||
rightMid: '▕',
|
||||
}
|
||||
|
||||
case BorderDouble:
|
||||
@@ -741,6 +756,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '╗',
|
||||
bottomLeft: '╚',
|
||||
bottomRight: '╝',
|
||||
leftMid: '╠',
|
||||
rightMid: '╣',
|
||||
}
|
||||
}
|
||||
return BorderStyle{
|
||||
@@ -753,6 +770,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
topRight: '╮',
|
||||
bottomLeft: '╰',
|
||||
bottomRight: '╯',
|
||||
leftMid: '├',
|
||||
rightMid: '┤',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +793,35 @@ const (
|
||||
WindowFooter
|
||||
)
|
||||
|
||||
// BorderColor returns the ColorPair used to draw borders for the given WindowType.
|
||||
func BorderColor(wt WindowType) ColorPair {
|
||||
switch wt {
|
||||
case WindowList:
|
||||
return ColListBorder
|
||||
case WindowInput:
|
||||
return ColInputBorder
|
||||
case WindowHeader:
|
||||
return ColHeaderBorder
|
||||
case WindowFooter:
|
||||
return ColFooterBorder
|
||||
case WindowPreview:
|
||||
return ColPreviewBorder
|
||||
}
|
||||
return ColBorder
|
||||
}
|
||||
|
||||
// SectionEdge selects which outer edge of the frame an inline section
|
||||
// should claim when PaintSectionFrame overpaints its adjacent border.
|
||||
// SectionEdgeNone paints only the inner verticals (for sections that
|
||||
// don't touch the outer top or bottom).
|
||||
type SectionEdge int
|
||||
|
||||
const (
|
||||
SectionEdgeNone SectionEdge = iota
|
||||
SectionEdgeTop
|
||||
SectionEdgeBottom
|
||||
)
|
||||
|
||||
type Renderer interface {
|
||||
DefaultTheme() *ColorTheme
|
||||
Init() error
|
||||
@@ -811,6 +859,19 @@ type Window interface {
|
||||
|
||||
DrawBorder()
|
||||
DrawHBorder()
|
||||
// DrawHSeparator draws an inline horizontal separator at `row` (relative to the
|
||||
// window's top) using the color for `windowType`. The separator is conceptually
|
||||
// the section's inner edge (e.g. the bottom border of an inline header), so the
|
||||
// whole row including junctions carries the section's fg + bg. When useBottom is
|
||||
// true the `bottom` horizontal char is used instead of `top`; for thinblock/block
|
||||
// styles this keeps the thin line bonded to the list content on the opposite side.
|
||||
DrawHSeparator(row int, windowType WindowType, useBottom bool)
|
||||
// PaintSectionFrame overpaints the border cells around the rows [topContent,
|
||||
// bottomContent] (inclusive, relative to the window's top) with the color for
|
||||
// `windowType`. When edge is SectionEdgeTop / SectionEdgeBottom, the
|
||||
// corresponding outer horizontal (+ corners) is also painted, letting the
|
||||
// inline section claim that edge of the outer frame.
|
||||
PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge)
|
||||
Refresh()
|
||||
FinishFill()
|
||||
|
||||
@@ -1166,7 +1227,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
|
||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool, headerInline bool, footerInline bool) {
|
||||
if forceBlack {
|
||||
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||
}
|
||||
@@ -1300,11 +1361,22 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
||||
} else {
|
||||
theme.HeaderBg = o(theme.Bg, theme.ListBg)
|
||||
}
|
||||
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
|
||||
// Inline header/footer borders sit inside the list frame, so default their color
|
||||
// to the list-border color when the user has not explicitly set it. The inline
|
||||
// separator then matches the surrounding frame.
|
||||
headerBorderFallback := theme.Border
|
||||
if headerInline {
|
||||
headerBorderFallback = theme.ListBorder
|
||||
}
|
||||
theme.HeaderBorder = o(headerBorderFallback, theme.HeaderBorder)
|
||||
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
|
||||
|
||||
theme.FooterBg = o(theme.Bg, theme.FooterBg)
|
||||
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
|
||||
footerBorderFallback := theme.Border
|
||||
if footerInline {
|
||||
footerBorderFallback = theme.ListBorder
|
||||
}
|
||||
theme.FooterBorder = o(footerBorderFallback, theme.FooterBorder)
|
||||
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
||||
|
||||
if theme.Nomatch.IsUndefined() {
|
||||
|
||||
@@ -130,6 +130,66 @@ class Tmux
|
||||
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
# 3-bit ANSI bg code (40..47) -> color name used in --color options.
|
||||
BG_NAMES = %w[black red green yellow blue magenta cyan white].freeze
|
||||
|
||||
# Parse `tmux capture-pane -e` output into per-row bg ranges. Each row is an
|
||||
# array of [col_start, col_end, bg] tuples where bg is one of:
|
||||
# 'default'
|
||||
# 'red' / 'green' / 'blue' / ... (3-bit names)
|
||||
# 'bright-red' / ... (bright variants)
|
||||
# '256:<n>' (256-color fallback)
|
||||
# ANSI state persists across rows, matching real terminal behavior.
|
||||
def bg_ranges
|
||||
raw = go(%W[capture-pane -p -J -e -t #{win}])
|
||||
bg = 'default'
|
||||
raw.map do |row|
|
||||
cells = []
|
||||
i = 0
|
||||
len = row.length
|
||||
while i < len
|
||||
c = row[i]
|
||||
if c == "\e" && row[i + 1] == '['
|
||||
j = i + 2
|
||||
j += 1 while j < len && row[j] != 'm'
|
||||
parts = row[i + 2...j].split(';')
|
||||
k = 0
|
||||
while k < parts.length
|
||||
p = parts[k].to_i
|
||||
case p
|
||||
when 0, 49 then bg = 'default'
|
||||
when 40..47 then bg = BG_NAMES[p - 40]
|
||||
when 100..107 then bg = "bright-#{BG_NAMES[p - 100]}"
|
||||
when 48
|
||||
if parts[k + 1] == '5'
|
||||
bg = "256:#{parts[k + 2]}"
|
||||
k += 2
|
||||
elsif parts[k + 1] == '2'
|
||||
bg = "rgb:#{parts[k + 2]}:#{parts[k + 3]}:#{parts[k + 4]}"
|
||||
k += 4
|
||||
end
|
||||
end
|
||||
k += 1
|
||||
end
|
||||
i = j + 1
|
||||
else
|
||||
cells << bg
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
ranges = []
|
||||
start = 0
|
||||
cells.each_with_index do |b, idx|
|
||||
if idx.positive? && b != cells[idx - 1]
|
||||
ranges << [start, idx - 1, cells[idx - 1]]
|
||||
start = idx
|
||||
end
|
||||
end
|
||||
ranges << [start, cells.length - 1, cells.last] unless cells.empty?
|
||||
ranges
|
||||
end
|
||||
end
|
||||
|
||||
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||
lines = nil
|
||||
begin
|
||||
|
||||
@@ -1392,5 +1392,159 @@ class TestLayout < TestInteractive
|
||||
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
|
||||
clicks: clicks)
|
||||
end
|
||||
|
||||
# Inline header inside a rounded list border.
|
||||
define_method(:"test_click_header_border_inline_#{slug}") do
|
||||
opts = %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
|
||||
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
|
||||
end
|
||||
|
||||
# Inline header inside a horizontal list border (top+bottom only, no T-junctions).
|
||||
define_method(:"test_click_header_border_inline_horizontal_list_#{slug}") do
|
||||
opts = %(--layout=#{layout} --style full --list-border=horizontal --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
|
||||
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
|
||||
end
|
||||
|
||||
# Inline header-lines inside a rounded list border.
|
||||
define_method(:"test_click_header_lines_border_inline_#{slug}") do
|
||||
clicks_hl = if layout == 'default'
|
||||
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
|
||||
else
|
||||
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
|
||||
end
|
||||
opts = %(--layout=#{layout} --style full --header-lines 3 --header-lines-border=inline)
|
||||
verify_clicks(kind: :header, opts: opts,
|
||||
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
|
||||
clicks: clicks_hl)
|
||||
end
|
||||
|
||||
# Inline footer inside a rounded list border.
|
||||
define_method(:"test_click_footer_border_inline_#{slug}") do
|
||||
opts = %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz' --footer-border=inline)
|
||||
verify_clicks(kind: :footer, opts: opts, input: 'seq 5',
|
||||
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
|
||||
end
|
||||
end
|
||||
|
||||
# An inline section requesting far more rows than the terminal can fit must not
|
||||
# break the layout. The list frame must still render inside the pane with both
|
||||
# corners visible and the prompt line present.
|
||||
def test_inline_header_lines_oversized
|
||||
tmux.send_keys %(seq 10000 | #{FZF} --style full --header-border inline --header-lines 9999), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
lines = tmux.capture
|
||||
# Rounded (light) and sharp (tcell) default border glyphs.
|
||||
top_corners = /[╭┌]/
|
||||
bottom_corners = /[╰└]/
|
||||
assert(lines.any? { |l| l.match?(top_corners) }, "list frame top missing: #{lines.inspect}")
|
||||
assert(lines.any? { |l| l.match?(bottom_corners) }, "list frame bottom missing: #{lines.inspect}")
|
||||
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# A non-inline section that consumes all available rows must still render without
|
||||
# crashing when another section is inline but has no budget. The inline section's
|
||||
# content is clipped to 0 but the layout proceeds.
|
||||
def test_inline_footer_starved_by_non_inline_header
|
||||
tmux.send_keys %(seq 10000 | #{FZF} --style full --footer-border inline --footer "$(seq 1000)" --header "$(seq 1000)"), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
lines = tmux.capture
|
||||
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# Without a line-drawing --list-border, --header-border=inline must silently
|
||||
# fall back to the `line` style (documented behavior).
|
||||
def test_inline_falls_back_without_list_border
|
||||
tmux.send_keys %(seq 5 | #{FZF} --list-border=none --header HEADER --header-border=inline), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
lines = tmux.capture
|
||||
assert(lines.any? { |l| l.include?('HEADER') }, "header missing: #{lines.inspect}")
|
||||
# Neither list frame corners (rounded/sharp) nor T-junction runes appear,
|
||||
# since we've fallen back to a plain line separator.
|
||||
assert(lines.none? { |l| l.match?(/[╭╮╰╯┌┐└┘├┤]/) }, "unexpected frame glyphs: #{lines.inspect}")
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# Invalid inline combinations must be rejected at startup.
|
||||
def test_inline_rejected_on_unsupported_options
|
||||
[
|
||||
['--border=inline', 'inline border is only supported'],
|
||||
['--list-border=inline', 'inline border is only supported'],
|
||||
['--input-border=inline', 'inline border is only supported'],
|
||||
['--preview-window=border-inline --preview :', 'invalid preview window option: border-inline'],
|
||||
['--header-first --header-border=inline', '--header-first is not compatible'],
|
||||
['--header-first --header-lines-border=inline --header-lines=1', '--header-first is not compatible'],
|
||||
['--header-border=inline --header-lines-border=sharp --header-lines=1',
|
||||
'--header-border=inline requires --header-lines-border to be inline or unset']
|
||||
].each do |args, expected|
|
||||
output = `#{FZF} #{args} < /dev/null 2>&1`
|
||||
refute_equal 0, $CHILD_STATUS.exitstatus, "expected non-zero exit for: #{args}"
|
||||
assert_includes output, expected, "wrong error for: #{args}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Count rows whose entire width is a single `color` range.
|
||||
def count_full_rows(ranges_by_row, color)
|
||||
ranges_by_row.count { |r| r.length == 1 && r[0][2] == color }
|
||||
end
|
||||
|
||||
# Wait until `tmux.bg_ranges` has at least `count` fully-`color` rows; return them.
|
||||
def wait_for_full_rows(color, count)
|
||||
ranges = nil
|
||||
tmux.until do |_|
|
||||
ranges = tmux.bg_ranges
|
||||
count_full_rows(ranges, color) >= count
|
||||
end
|
||||
ranges
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
# Inline header's entire section (outer edge + content-row verticals + separator)
|
||||
# carries the header-bg color; list rows below carry list-bg.
|
||||
def test_inline_header_bg_color
|
||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
# 3 fully-red rows: top edge, header content, separator.
|
||||
ranges = wait_for_full_rows('red', 3)
|
||||
assert_equal_org(3, count_full_rows(ranges, 'red'))
|
||||
# List rows below (>=5) are fully green.
|
||||
assert_operator count_full_rows(ranges, 'green'), :>=, 5
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# Regression: when --header-lines-border=inline is the only inline section
|
||||
# (no --header-border), the section must still use header-bg, not list-bg.
|
||||
def test_inline_header_lines_bg_without_main_header
|
||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header-lines 2 --header-lines-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
# Top edge + 2 content rows + separator = 4 fully-red rows.
|
||||
ranges = wait_for_full_rows('red', 4)
|
||||
assert_equal_org(4, count_full_rows(ranges, 'red'))
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# Inline footer's entire section carries footer-bg; list rows above carry list-bg.
|
||||
def test_inline_footer_bg_color
|
||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --footer FOOTER --footer-border=inline --color=bg:-1,footer-border:white,list-border:white,footer-bg:blue,list-bg:green), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
ranges = wait_for_full_rows('blue', 3)
|
||||
assert_equal_org(3, count_full_rows(ranges, 'blue'))
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
|
||||
# The list-label's bg is swapped to match the adjacent inline section so it reads as
|
||||
# part of the section frame rather than a list-colored island on a section-colored edge.
|
||||
def test_list_label_bg_on_inline_section_edge
|
||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --list-label=LL --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green,list-label:yellow:bold), :Enter
|
||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
||||
# The label sits on the header-owned top edge, so the entire row must be a
|
||||
# single red run (no green breaks where the label cells are).
|
||||
ranges = wait_for_full_rows('red', 3)
|
||||
assert_operator count_full_rows(ranges, 'red'), :>=, 3
|
||||
tmux.send_keys 'Escape'
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user