refactor: Extract BorderRenderer utility for shared border rendering (A.3, A.4, B.1)

- Create BorderRenderer enum with reusable line-level border primitives
- Standard style: standardTopBorder, standardBottomBorder, standardDivider,
  standardContentLine (with optional persistent background)
- Block style: blockTopBorder, blockBottomBorder, blockContentLine,
  blockSeparator (with FG/BG transition support)
- standardTopBorder has title overload for ContainerView inline titles
- Refactor 5 files to use BorderRenderer: BorderModifier, Button, Menu,
  ContainerView, StatusBar
- Eliminate ~170 lines of duplicated border-assembly code

Net reduction: 167 lines across 5 files
This commit is contained in:
phranck
2026-01-30 11:04:31 +01:00
parent fdd22ac0de
commit d5c3d2c784
6 changed files with 312 additions and 260 deletions
+14 -69
View File
@@ -53,87 +53,32 @@ extension BorderedView: Renderable {
/// Renders with standard box-drawing characters.
private func renderStandardStyle(buffer: FrameBuffer, innerWidth: Int, style: BorderStyle) -> FrameBuffer {
let borderForeground = color ?? Color.theme.border
// Build the top border line
let topLine = buildBorderLine(
left: style.topLeft,
fill: style.horizontal,
right: style.topRight,
width: innerWidth
)
// Build the bottom border line
let bottomLine = buildBorderLine(
left: style.bottomLeft,
fill: style.horizontal,
right: style.bottomRight,
width: innerWidth
)
// Build the result
let borderColor = color ?? Color.theme.border
var lines: [String] = []
// Top border
lines.append(ANSIRenderer.colorize(topLine, foreground: borderForeground))
// Content lines with side borders
lines.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: borderColor))
for line in buffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let borderedLine = ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground)
+ paddedLine
+ ANSIRenderer.reset
+ ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground)
lines.append(borderedLine)
lines.append(BorderRenderer.standardContentLine(
content: line, innerWidth: innerWidth, style: style, color: borderColor
))
}
// Bottom border
lines.append(ANSIRenderer.colorize(bottomLine, foreground: borderForeground))
lines.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: borderColor))
return FrameBuffer(lines: lines)
}
/// Renders with half-block characters for block appearance.
///
/// Block style design:
/// ```
/// ▄▄▄▄▄▄▄▄▄▄ ← Top: ▄, FG = container BG, BG = transparent
/// █ Content █ ← Sides: █, FG = container BG, content has container BG
/// ▀▀▀▀▀▀▀▀▀▀ ← Bottom: ▀, FG = container BG, BG = transparent
/// ```
private func renderBlockStyle(buffer: FrameBuffer, innerWidth: Int) -> FrameBuffer {
var lines: [String] = []
// For block style, use container background color for borders
let containerBg = Color.theme.containerBackground
let sideBorder = ANSIRenderer.colorize("█", foreground: containerBg)
// Top border: ▄▄▄ with FG = container BG
let topLine = String(repeating: "▄", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(topLine, foreground: containerBg))
// Content lines with █ side borders and container background
for line in buffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: containerBg)
lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
}
// Bottom border: ▀▀▀ with FG = container BG
let bottomLine = String(repeating: "▀", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(bottomLine, foreground: containerBg))
return FrameBuffer(lines: lines)
}
var lines: [String] = []
/// Builds a horizontal border line.
private func buildBorderLine(
left: Character,
fill: Character,
right: Character,
width: Int
) -> String {
String(left) + String(repeating: fill, count: width) + String(right)
lines.append(BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: containerBg))
for line in buffer.lines {
lines.append(BorderRenderer.blockContentLine(content: line, innerWidth: innerWidth, sectionColor: containerBg))
}
lines.append(BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: containerBg))
return FrameBuffer(lines: lines)
}
}
@@ -0,0 +1,219 @@
//
// BorderRenderer.swift
// TUIKit
//
// Centralized border rendering utilities for standard and block styles.
//
/// Reusable building blocks for border rendering.
///
/// Each method produces a single rendered line (`String`) that callers
/// append to a `[String]` array for `FrameBuffer` construction.
/// This eliminates duplicated border-assembly code across Views and Modifiers.
///
/// Two style families are supported:
/// - **Standard**: box-drawing characters (┌─┐│└─┘├─┤)
/// - **Block**: half-block characters (▄ █ ▀) for smooth visual edges
public enum BorderRenderer {
// MARK: - Standard Style (Box-Drawing Characters)
/// Renders a plain top border line.
///
/// ┌──────────────┐
///
/// - Parameters:
/// - style: The border style providing corner and edge characters.
/// - innerWidth: The width of the content area (excluding borders).
/// - color: The foreground color for the border.
/// - Returns: A colorized top border string.
public static func standardTopBorder(
style: BorderStyle,
innerWidth: Int,
color: Color
) -> String {
let line = String(style.topLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.topRight)
return ANSIRenderer.colorize(line, foreground: color)
}
/// Renders a top border line with an inline title.
///
/// ┌─ Title ──────┐
///
/// - Parameters:
/// - style: The border style.
/// - innerWidth: The content width.
/// - color: The border color.
/// - title: The title text.
/// - titleColor: The title foreground color.
/// - Returns: A colorized top border string with embedded title.
public static func standardTopBorder(
style: BorderStyle,
innerWidth: Int,
color: Color,
title: String,
titleColor: Color
) -> String {
let titleStyled = ANSIRenderer.colorize(" \(title) ", foreground: titleColor, bold: true)
let leftPart = ANSIRenderer.colorize(
String(style.topLeft) + String(style.horizontal),
foreground: color
)
let rightPartLength = max(0, innerWidth - 1 - title.count - 2)
let rightPart = ANSIRenderer.colorize(
String(repeating: style.horizontal, count: rightPartLength) + String(style.topRight),
foreground: color
)
return leftPart + titleStyled + rightPart
}
/// Renders a plain bottom border line.
///
/// └──────────────┘
///
/// - Parameters:
/// - style: The border style.
/// - innerWidth: The content width.
/// - color: The border color.
/// - Returns: A colorized bottom border string.
public static func standardBottomBorder(
style: BorderStyle,
innerWidth: Int,
color: Color
) -> String {
let line = String(style.bottomLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.bottomRight)
return ANSIRenderer.colorize(line, foreground: color)
}
/// Renders a horizontal divider with T-junctions.
///
/// ├──────────────┤
///
/// - Parameters:
/// - style: The border style (uses leftT, horizontal, rightT).
/// - innerWidth: The content width.
/// - color: The border color.
/// - Returns: A colorized divider string.
public static func standardDivider(
style: BorderStyle,
innerWidth: Int,
color: Color
) -> String {
let line = String(style.leftT)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.rightT)
return ANSIRenderer.colorize(line, foreground: color)
}
/// Wraps a single content line with vertical side borders.
///
/// │ padded content │
///
/// If `backgroundColor` is provided, `applyPersistentBackground` is used
/// so the background survives inner ANSI resets.
///
/// - Parameters:
/// - content: The content string (will be padded to `innerWidth`).
/// - innerWidth: The target content width.
/// - style: The border style (for the vertical character).
/// - color: The border color.
/// - backgroundColor: Optional background applied to the content area.
/// - Returns: The bordered content line.
public static func standardContentLine(
content: String,
innerWidth: Int,
style: BorderStyle,
color: Color,
backgroundColor: Color? = nil
) -> String {
let paddedLine = content.padToVisibleWidth(innerWidth)
let styledContent: String
if let bgColor = backgroundColor {
styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bgColor)
} else {
styledContent = paddedLine
}
let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: color)
return vertical + styledContent + ANSIRenderer.reset + vertical
}
// MARK: - Block Style (Half-Block Characters)
/// Renders a block-style top border.
///
/// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
///
/// - Parameters:
/// - innerWidth: The content width (border width = innerWidth + 2).
/// - color: The foreground color (typically the section's background color).
/// - Returns: The top border string.
public static func blockTopBorder(
innerWidth: Int,
color: Color
) -> String {
let line = String(repeating: "▄", count: innerWidth + 2)
return ANSIRenderer.colorize(line, foreground: color)
}
/// Renders a block-style bottom border.
///
/// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀
///
/// - Parameters:
/// - innerWidth: The content width (border width = innerWidth + 2).
/// - color: The foreground color (typically the section's background color).
/// - Returns: The bottom border string.
public static func blockBottomBorder(
innerWidth: Int,
color: Color
) -> String {
let line = String(repeating: "▀", count: innerWidth + 2)
return ANSIRenderer.colorize(line, foreground: color)
}
/// Wraps a single content line with full-block side borders
/// and applies a persistent background.
///
/// █ content █
///
/// - Parameters:
/// - content: The content string (will be padded to `innerWidth`).
/// - innerWidth: The target content width.
/// - sectionColor: The color for both `█` borders and content background.
/// - Returns: The bordered content line.
public static func blockContentLine(
content: String,
innerWidth: Int,
sectionColor: Color
) -> String {
let paddedLine = content.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: sectionColor)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: sectionColor)
return sideBorder + styledContent + ANSIRenderer.reset + sideBorder
}
/// Renders a block-style section separator (transition between two background colors).
///
/// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (header→body: FG=headerBg, BG=bodyBg)
/// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ (body→footer: FG=footerBg, BG=bodyBg)
///
/// - Parameters:
/// - innerWidth: The content width (separator width = innerWidth + 2).
/// - character: The separator character (`"▀"` for header→body, `"▄"` for body→footer).
/// - foregroundColor: The FG color (the section being transitioned from or to).
/// - backgroundColor: The BG color (the adjacent section).
/// - Returns: The separator line.
public static func blockSeparator(
innerWidth: Int,
character: Character = "▀",
foregroundColor: Color,
backgroundColor: Color
) -> String {
let line = String(repeating: character, count: innerWidth + 2)
return ANSIRenderer.colorize(line, foreground: foregroundColor, background: backgroundColor)
}
}
+5 -19
View File
@@ -300,27 +300,13 @@ extension Button: Renderable {
let innerWidth = buffer.width
var result: [String] = []
// Border characters (optionally colored)
let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: color)
// Top border
let topLine = String(style.topLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.topRight)
result.append(ANSIRenderer.colorize(topLine, foreground: color))
// Content lines with side borders
result.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: color))
for line in buffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let borderedLine = vertical + paddedLine + ANSIRenderer.reset + vertical
result.append(borderedLine)
result.append(BorderRenderer.standardContentLine(
content: line, innerWidth: innerWidth, style: style, color: color
))
}
// Bottom border
let bottomLine = String(style.bottomLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.bottomRight)
result.append(ANSIRenderer.colorize(bottomLine, foreground: color))
result.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: color))
return FrameBuffer(lines: result)
}
+42 -80
View File
@@ -222,69 +222,46 @@ extension ContainerView: Renderable {
var lines: [String] = []
// Top border (with title if present)
let topLine: String
if let titleText = title {
let titleStyled = ANSIRenderer.colorize(" \(titleText) ", foreground: titleColor ?? Color.theme.accent, bold: true)
let leftPart = ANSIRenderer.colorize(
String(borderStyle.topLeft) + String(borderStyle.horizontal),
foreground: borderColor
)
let rightPartLength = max(0, innerWidth - 1 - titleText.count - 2)
let rightPart = ANSIRenderer.colorize(
String(repeating: borderStyle.horizontal, count: rightPartLength) + String(borderStyle.topRight),
foreground: borderColor
)
topLine = leftPart + titleStyled + rightPart
lines.append(BorderRenderer.standardTopBorder(
style: borderStyle, innerWidth: innerWidth, color: borderColor,
title: titleText, titleColor: titleColor ?? Color.theme.accent
))
} else {
topLine = ANSIRenderer.colorize(
String(borderStyle.topLeft)
+ String(repeating: borderStyle.horizontal, count: innerWidth)
+ String(borderStyle.topRight),
foreground: borderColor
)
lines.append(BorderRenderer.standardTopBorder(
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
}
lines.append(topLine)
// Vertical border characters
let leftBorder = ANSIRenderer.colorize(String(borderStyle.vertical), foreground: borderColor)
let rightBorder = ANSIRenderer.colorize(String(borderStyle.vertical), foreground: borderColor)
// Body lines with theme background
let bodyBg = context.environment.theme.containerBackground
for line in bodyBuffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg)
lines.append(leftBorder + styledContent + ANSIRenderer.reset + rightBorder)
lines.append(BorderRenderer.standardContentLine(
content: line, innerWidth: innerWidth, style: borderStyle,
color: borderColor, backgroundColor: bodyBg
))
}
// Footer section (if present)
if let footerBuf = footerBuffer, !footerBuf.isEmpty {
// Footer separator
if style.showFooterSeparator {
let separatorLine = ANSIRenderer.colorize(
String(borderStyle.leftT)
+ String(repeating: borderStyle.horizontal, count: innerWidth)
+ String(borderStyle.rightT),
foreground: borderColor
)
lines.append(separatorLine)
lines.append(BorderRenderer.standardDivider(
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
}
// Footer lines (no background - footer has its own styling)
for line in footerBuf.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
lines.append(leftBorder + paddedLine + ANSIRenderer.reset + rightBorder)
lines.append(BorderRenderer.standardContentLine(
content: line, innerWidth: innerWidth, style: borderStyle, color: borderColor
))
}
}
// Bottom border
let bottomLine = ANSIRenderer.colorize(
String(borderStyle.bottomLeft)
+ String(repeating: borderStyle.horizontal, count: innerWidth)
+ String(borderStyle.bottomRight),
foreground: borderColor
)
lines.append(bottomLine)
lines.append(BorderRenderer.standardBottomBorder(
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
return FrameBuffer(lines: lines)
}
@@ -324,66 +301,51 @@ extension ContainerView: Renderable {
let hasFooter = footerBuffer != nil && !(footerBuffer?.isEmpty ?? true)
// === TOP BORDER ===
// ▄▄▄: FG = header/body BG, BG = transparent (App BG shows through)
let topLine = String(repeating: "▄", count: innerWidth + 2)
if hasHeader {
lines.append(ANSIRenderer.colorize(topLine, foreground: headerFooterBg))
} else {
lines.append(ANSIRenderer.colorize(topLine, foreground: bodyBg))
}
lines.append(BorderRenderer.blockTopBorder(
innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg
))
// === HEADER SECTION (if title present) ===
if let titleText = title {
// █ TITLE █: FG = header BG for █, content has header BG
let titleStyled = ANSIRenderer.colorize(" \(titleText) ", foreground: titleColor ?? Color.theme.accent, bold: true)
let paddedTitle = titleStyled.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedTitle, color: headerFooterBg)
lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
lines.append(BorderRenderer.blockContentLine(
content: titleStyled, innerWidth: innerWidth, sectionColor: headerFooterBg
))
// Header/Body separator: ▀▀▀
// FG = header BG, BG = body BG (creates smooth transition)
if style.showHeaderSeparator {
let sepLine = String(repeating: "▀", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(sepLine, foreground: headerFooterBg, background: bodyBg))
lines.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
}
}
// === BODY LINES ===
// █ Content █: FG = body BG for █, content has body BG
for line in bodyBuffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: bodyBg)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg)
lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
lines.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: bodyBg
))
}
// === FOOTER SECTION (if present) ===
if let footerBuf = footerBuffer, !footerBuf.isEmpty {
// Body/Footer separator: ▄▄▄
// FG = footer BG, BG = body BG (creates smooth transition)
if style.showFooterSeparator {
let sepLine = String(repeating: "▄", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(sepLine, foreground: headerFooterBg, background: bodyBg))
lines.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, character: "▄",
foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
}
// █ Footer █: FG = footer BG for █, content has footer BG
for line in footerBuf.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: headerFooterBg)
lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
lines.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: headerFooterBg
))
}
}
// === BOTTOM BORDER ===
// ▀▀▀: FG = footer/body BG, BG = transparent (App BG shows through)
let bottomLine = String(repeating: "▀", count: innerWidth + 2)
if hasFooter {
lines.append(ANSIRenderer.colorize(bottomLine, foreground: headerFooterBg))
} else {
lines.append(ANSIRenderer.colorize(bottomLine, foreground: bodyBg))
}
lines.append(BorderRenderer.blockBottomBorder(
innerWidth: innerWidth, color: hasFooter ? headerFooterBg : bodyBg
))
return FrameBuffer(lines: lines)
}
+22 -54
View File
@@ -340,85 +340,53 @@ extension Menu: Renderable {
var result: [String] = []
if isBlockStyle {
// Block style: use half-blocks with special coloring
// Header/Footer BG = darker (containerHeaderBackground)
// Body BG = lighter (containerBackground)
// App BG = transparent (no background set)
let headerFooterBg = Color.theme.containerHeaderBackground
let bodyBg = Color.theme.containerBackground
// Determine if we have a header (divider present)
let hasHeader = dividerLineIndex != nil
// Top border: ▄▄▄
// FG = header background, BG = App background (transparent/none)
let topLine = String(repeating: "▄", count: innerWidth + 2)
if hasHeader {
result.append(ANSIRenderer.colorize(topLine, foreground: headerFooterBg))
} else {
// No header: use body background for top
result.append(ANSIRenderer.colorize(topLine, foreground: bodyBg))
}
// Top border
result.append(BorderRenderer.blockTopBorder(
innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg
))
// Content lines with side borders
// Content lines with section-aware coloring
for (index, line) in buffer.lines.enumerated() {
let isHeaderLine = hasHeader && dividerLineIndex.map({ index < $0 }) ?? false
let isDividerLine = hasHeader && dividerLineIndex.map({ index == $0 }) ?? false
if isDividerLine {
// Header/Body separator: ▀▀▀
// FG = header BG, BG = body BG (creates smooth transition)
let dividerLine = String(repeating: "▀", count: innerWidth + 2)
result.append(ANSIRenderer.colorize(dividerLine, foreground: headerFooterBg, background: bodyBg))
result.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
} else if isHeaderLine {
// Header line: █ borders and content with header background
let paddedLine = line.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: headerFooterBg)
result.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
result.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: headerFooterBg
))
} else {
// Body line: █ borders with body background
let paddedLine = line.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: bodyBg)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg)
result.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder)
result.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: bodyBg
))
}
}
// Bottom border: ▀▀▀
// FG = body background, BG = App background (transparent)
let bottomLine = String(repeating: "▀", count: innerWidth + 2)
result.append(ANSIRenderer.colorize(bottomLine, foreground: bodyBg))
// Bottom border
result.append(BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: bodyBg))
} else {
// Standard style: regular box-drawing characters
let borderForeground = color ?? Color.theme.border
let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground)
// Top border
let topLine = String(style.topLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.topRight)
result.append(ANSIRenderer.colorize(topLine, foreground: borderForeground))
result.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: borderForeground))
// Content lines with side borders
for (index, line) in buffer.lines.enumerated() {
if let dividerIndex = dividerLineIndex, index == dividerIndex {
// Render horizontal divider with T-junctions
let dividerLine = String(style.leftT)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.rightT)
result.append(ANSIRenderer.colorize(dividerLine, foreground: borderForeground))
result.append(BorderRenderer.standardDivider(style: style, innerWidth: innerWidth, color: borderForeground))
} else {
let paddedLine = line.padToVisibleWidth(innerWidth)
result.append(vertical + paddedLine + ANSIRenderer.reset + vertical)
result.append(BorderRenderer.standardContentLine(
content: line, innerWidth: innerWidth, style: style, color: borderForeground
))
}
}
// Bottom border
let bottomLine = String(style.bottomLeft)
+ String(repeating: style.horizontal, count: innerWidth)
+ String(style.bottomRight)
result.append(ANSIRenderer.colorize(bottomLine, foreground: borderForeground))
result.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: borderForeground))
}
return FrameBuffer(lines: result)
+10 -38
View File
@@ -909,50 +909,22 @@ extension StatusBar: Renderable {
let border = context.environment.appearance.borderStyle
let borderColor = context.environment.theme.border
// Build the three lines with theme border color
let topBorder = ANSIRenderer.colorize(String(border.topLeft)
+ String(repeating: border.horizontal, count: innerWidth)
+ String(border.topRight), foreground: borderColor)
let contentLine = ANSIRenderer.colorize(String(border.vertical), foreground: borderColor)
+ content
+ ANSIRenderer.reset // Prevent color bleeding
+ ANSIRenderer.colorize(String(border.vertical), foreground: borderColor)
let bottomBorder = ANSIRenderer.colorize(String(border.bottomLeft)
+ String(repeating: border.horizontal, count: innerWidth)
+ String(border.bottomRight), foreground: borderColor)
return FrameBuffer(lines: [topBorder, contentLine, bottomBorder])
return FrameBuffer(lines: [
BorderRenderer.standardTopBorder(style: border, innerWidth: innerWidth, color: borderColor),
BorderRenderer.standardContentLine(content: content, innerWidth: innerWidth, style: border, color: borderColor),
BorderRenderer.standardBottomBorder(style: border, innerWidth: innerWidth, color: borderColor),
])
}
/// Renders block-style status bar with half-block characters.
/// ```
/// ▄▄▄▄▄▄▄▄▄▄ ← Top: ▄, statusBar BG color
/// █ Content █ ← Sides: █, statusBar BG color; content with statusBar BG
/// ▀▀▀▀▀▀▀▀▀▀ ← Bottom: ▀, statusBar BG color
/// ```
private func renderBlockBordered(content: String, innerWidth: Int, context: RenderContext) -> FrameBuffer {
var lines: [String] = []
// Get colors from theme
let statusBarBg = context.environment.theme.statusBarBackground
// Top border: ▄▄▄ with statusBar background color
let topLine = String(repeating: "▄", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(topLine, foreground: statusBarBg))
// Content line with █ side borders and statusBar BG applied to content
let styledContent = ANSIRenderer.applyPersistentBackground(content, color: statusBarBg)
let sideBorder = ANSIRenderer.colorize("█", foreground: statusBarBg)
let contentLine = sideBorder + styledContent + ANSIRenderer.reset + sideBorder
lines.append(contentLine)
// Bottom border: ▀▀▀ (upper half block) with statusBar background color
let bottomLine = String(repeating: "▀", count: innerWidth + 2)
lines.append(ANSIRenderer.colorize(bottomLine, foreground: statusBarBg))
return FrameBuffer(lines: lines)
return FrameBuffer(lines: [
BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: statusBarBg),
BorderRenderer.blockContentLine(content: content, innerWidth: innerWidth, sectionColor: statusBarBg),
BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: statusBarBg),
])
}
}