mirror of
https://github.com/junegunn/fzf.git
synced 2026-04-23 08:04:34 +08:00
Add --{header,header-lines,footer}-border=inline
New BorderShape that embeds the section inside the --list-border frame, joined to the list content by a horizontal separator with T-junctions where the list shape has side borders. Requires a list border with both top and bottom segments; falls back to 'line' otherwise. Stacks when multiple sections are inline. Sections inherit --color list-border by default and are colored as a uniform block via their own --color *-border and *-bg. Incompatible with --header-first. --header-border=inline requires --header-lines-border to be inline or unset.
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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/_
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
368
src/terminal.go
368
src/terminal.go
@@ -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:
|
||||
|
||||
233
src/tui/light.go
233
src/tui/light.go
@@ -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 {
|
||||
|
||||
187
src/tui/tcell.go
187
src/tui/tcell.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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