Files
TUIkit/Sources/TUIKit/Views/StatusBar.swift
T
phranck b9a6cc29ab fix: Show all system items (quit, help, theme) and fix deduplication
- StatusBarState now initializes with all 3 system items
- Added deduplication logic to StatusBar.renderToBuffer
- User items with same shortcut override system items
- Updated Example App to not duplicate 'q quit'
- Updated tests for 3 system items instead of 1
2026-01-28 20:40:52 +01:00

934 lines
29 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// StatusBar.swift
// TUIKit
//
// A status bar that displays keyboard shortcuts and context-sensitive actions.
// Always rendered at the bottom of the terminal, never dimmed by overlays.
//
import Foundation
// MARK: - Status Bar Style
/// The visual style of the status bar.
public enum StatusBarStyle: Sendable {
/// A single line with horizontal padding.
case compact
/// Block-style border (like `BorderStyle.block`).
case bordered
}
// MARK: - Status Bar Alignment
/// The horizontal alignment of items within the status bar.
public enum StatusBarAlignment: Sendable {
/// Items are aligned to the left (leading edge).
case leading
/// Items are aligned to the right (trailing edge).
case trailing
/// Items are centered horizontally.
case center
/// Items are evenly distributed across the full width.
case justified
}
// MARK: - Shortcut Symbols
/// A collection of Unicode symbols commonly used for keyboard shortcuts.
///
/// Use these constants instead of typing Unicode characters directly.
/// They provide a consistent look and are easier to read in code.
///
/// # Example
///
/// ```swift
/// StatusBarItem(shortcut: .escape, label: "close") { dismiss() }
/// StatusBarItem(shortcut: .arrowsUpDown, label: "nav")
/// StatusBarItem(shortcut: .enter, label: "select", key: .enter)
/// ```
public enum Shortcut {
// MARK: - Special Keys
/// Escape key symbol: ⎋
public static let escape = "⎋"
/// Return/Enter key symbol: ↵
public static let enter = "↵"
/// Alternative return symbol: ⏎
public static let returnKey = "⏎"
/// Tab key symbol: ⇥
public static let tab = "⇥"
/// Shift+Tab (backtab) symbol: ⇤
public static let shiftTab = "⇤"
/// Backspace/Delete symbol: ⌫
public static let backspace = "⌫"
/// Forward delete symbol: ⌦
public static let delete = "⌦"
/// Space bar symbol: ␣
public static let space = "␣"
// MARK: - Arrow Keys (Single)
/// Up arrow: ↑
public static let arrowUp = "↑"
/// Down arrow: ↓
public static let arrowDown = "↓"
/// Left arrow: ←
public static let arrowLeft = "←"
/// Right arrow: →
public static let arrowRight = "→"
// MARK: - Arrow Key Combinations
/// Up and down arrows: ↑↓
public static let arrowsUpDown = "↑↓"
/// Left and right arrows: ←→
public static let arrowsLeftRight = "←→"
/// All four arrows: ↑↓←→
public static let arrowsAll = "↑↓←→"
/// Vertical arrows (alternative): ⇅
public static let arrowsVertical = "⇅"
/// Horizontal arrows (alternative): ⇆
public static let arrowsHorizontal = "⇆"
// MARK: - Modifier Keys
/// Command key (Mac): ⌘
public static let command = "⌘"
/// Option/Alt key (Mac): ⌥
public static let option = "⌥"
/// Control key: ⌃
public static let control = "⌃"
/// Shift key: ⇧
public static let shift = "⇧"
/// Caps Lock: ⇪
public static let capsLock = "⇪"
// MARK: - Function Keys
/// Function key prefix: Fn
public static let fn = "Fn"
// MARK: - Navigation
/// Home key symbol: ⤒
public static let home = "⤒"
/// End key symbol: ⤓
public static let end = "⤓"
/// Page Up symbol: ⇞
public static let pageUp = "⇞"
/// Page Down symbol: ⇟
public static let pageDown = "⇟"
// MARK: - Actions
/// Plus/Add symbol: +
public static let plus = "+"
/// Minus/Remove symbol:
public static let minus = ""
/// Checkmark/Confirm: ✓
public static let checkmark = "✓"
/// Cross/Cancel: ✗
public static let cross = "✗"
/// Search/Find: 🔍 (or use "?" for simpler display)
public static let search = "?"
/// Help symbol: ?
public static let help = "?"
/// Save symbol: 💾 (or use "S" for simpler display)
public static let save = "S"
// MARK: - Common Shortcuts
/// Quit shortcut: q
public static let quit = "q"
/// Yes shortcut: y
public static let yes = "y"
/// No shortcut: n
public static let no = "n"
/// Cancel shortcut: c
public static let cancel = "c"
/// OK shortcut: o
public static let ok = "o"
// MARK: - Brackets and Selection
/// Selection indicator: ▸
public static let selectionRight = "▸"
/// Selection indicator left: ◂
public static let selectionLeft = "◂"
/// Bullet point: •
public static let bullet = "•"
/// Square selection: ▪
public static let squareBullet = "▪"
// MARK: - Combining Helpers
/// Combines multiple shortcuts with a separator.
///
/// - Parameters:
/// - shortcuts: The shortcuts to combine.
/// - separator: The separator (default: empty string).
/// - Returns: The combined shortcut string.
///
/// # Example
///
/// ```swift
/// Shortcut.combine(.control, "c") // "⌃c"
/// Shortcut.combine(.shift, .tab) // "⇧⇥"
/// ```
public static func combine(_ shortcuts: String..., separator: String = "") -> String {
shortcuts.joined(separator: separator)
}
/// Creates a Ctrl+key shortcut display.
///
/// - Parameter key: The key character.
/// - Returns: The formatted shortcut (e.g., "^c").
public static func ctrl(_ key: Character) -> String {
"^\(key)"
}
/// Creates a range shortcut display (e.g., "1-9").
///
/// - Parameters:
/// - start: The start of the range.
/// - end: The end of the range.
/// - Returns: The formatted range (e.g., "1-9").
public static func range(_ start: String, _ end: String) -> String {
"\(start)-\(end)"
}
}
// MARK: - Status Bar Item Order
/// Defines the display order of status bar items.
///
/// Items are sorted by their order value (ascending). Lower values appear first (left).
/// System items appear on the right side with high order values.
///
/// # Order Ranges
///
/// - `0-99`: Reserved for leading items
/// - `100-899`: User-defined items (default: 500)
/// - `900-999`: Reserved for system items (quit, help, theme) on the right
///
/// # System Item Layout (from right edge)
///
/// ```
/// [user items...] [q quit] [? help] [t theme]
/// ```
///
/// # Example
///
/// ```swift
/// // Custom item appears on the left (before system items)
/// StatusBarItem(shortcut: "s", label: "save", order: .default)
/// ```
public struct StatusBarItemOrder: Comparable, Sendable {
public let value: Int
public init(_ value: Int) {
self.value = value
}
public static func < (lhs: StatusBarItemOrder, rhs: StatusBarItemOrder) -> Bool {
lhs.value < rhs.value
}
// MARK: - User Item Orders
/// Default order for user-defined items (appears on the left).
public static let `default` = StatusBarItemOrder(500)
/// Order for items that should appear early (leftmost user items).
public static let early = StatusBarItemOrder(100)
/// Order for items that should appear late (rightmost user items, before system items).
public static let late = StatusBarItemOrder(800)
// MARK: - System Item Orders (right side)
/// Order for the quit item (leftmost of system items).
/// Appears as: `[...user items] [q quit] [? help] [t theme]`
public static let quit = StatusBarItemOrder(900)
/// Order for the help item (middle system item).
public static let help = StatusBarItemOrder(910)
/// Order for the theme item (rightmost).
public static let theme = StatusBarItemOrder(920)
}
// MARK: - Status Bar Item Protocol
/// A protocol for items that can be displayed in a status bar.
///
/// Implement this protocol to create custom status bar items.
/// The default `StatusBarItem` already conforms to this protocol.
public protocol StatusBarItemProtocol: Sendable {
/// The unique identifier for this item.
var id: String { get }
/// The shortcut key(s) to display (e.g., "q", "↑↓", "⎋").
var shortcut: String { get }
/// A short description (one word, e.g., "quit", "nav", "close").
var label: String { get }
/// The key event that triggers this item's action.
///
/// Return nil if the item is purely informational (no action).
var triggerKey: Key? { get }
/// The display order of this item.
///
/// Items are sorted by order (ascending). Lower values appear first.
var order: StatusBarItemOrder { get }
/// Whether this item matches a given key event.
///
/// Override this for complex matching (e.g., arrow keys).
func matches(_ event: KeyEvent) -> Bool
}
// Default implementations
public extension StatusBarItemProtocol {
/// Default order for user-defined items.
var order: StatusBarItemOrder { .default }
func matches(_ event: KeyEvent) -> Bool {
guard let trigger = triggerKey else { return false }
return event.key == trigger
}
}
// MARK: - Status Bar Item
/// A status bar item displaying a shortcut and its description.
///
/// # Example
///
/// ```swift
/// StatusBarItem(shortcut: "q", label: "quit") {
/// app.quit()
/// }
///
/// StatusBarItem(shortcut: "↑↓", label: "nav", key: .up) // Info only, no action
///
/// // With custom order
/// StatusBarItem(shortcut: "s", label: "save", order: .early) {
/// save()
/// }
/// ```
public struct StatusBarItem: StatusBarItemProtocol, Identifiable {
public let id: String
public let shortcut: String
public let label: String
public let triggerKey: Key?
public let order: StatusBarItemOrder
/// The action to perform when the shortcut is triggered.
private let action: (@Sendable () -> Void)?
/// Creates a status bar item with an action.
///
/// - Parameters:
/// - shortcut: The shortcut key(s) to display.
/// - label: A short description (one word).
/// - key: The key that triggers the action (derived from shortcut if not provided).
/// - order: The display order (default: `.default`).
/// - action: The action to perform.
public init(
shortcut: String,
label: String,
key: Key? = nil,
order: StatusBarItemOrder = .default,
action: (@Sendable () -> Void)? = nil
) {
self.id = "\(shortcut)-\(label)"
self.shortcut = shortcut
self.label = label
self.order = order
self.action = action
// Derive trigger key from shortcut if not explicitly provided
if let explicitKey = key {
self.triggerKey = explicitKey
} else if let mappedKey = Self.keyFromShortcut(shortcut) {
// First try to map special symbols to keys
self.triggerKey = mappedKey
} else if shortcut.count == 1, let char = shortcut.first {
// Single character becomes a character key
self.triggerKey = .character(char)
} else {
self.triggerKey = nil
}
}
/// Creates an informational status bar item (no action).
///
/// - Parameters:
/// - shortcut: The shortcut key(s) to display.
/// - label: A short description.
/// - order: The display order (default: `.default`).
public init(shortcut: String, label: String, order: StatusBarItemOrder = .default) {
self.init(shortcut: shortcut, label: label, key: nil, order: order, action: nil)
}
/// Whether this item has an action to execute.
public var hasAction: Bool {
action != nil
}
/// Executes the item's action.
public func execute() {
action?()
}
/// Maps common shortcut symbols to Key values.
private static func keyFromShortcut(_ shortcut: String) -> Key? {
switch shortcut.lowercased() {
case "⎋", "esc", "escape":
return .escape
case "↵", "⏎", "enter", "return":
return .enter
case "⇥", "tab":
return .tab
case "⌫", "backspace", "del":
return .backspace
case "↑":
return .up
case "↓":
return .down
case "←":
return .left
case "→":
return .right
default:
return nil
}
}
/// Override matching for special cases.
public func matches(_ event: KeyEvent) -> Bool {
// Handle arrow key combinations like "↑↓"
if shortcut.contains("↑") && event.key == .up { return true }
if shortcut.contains("↓") && event.key == .down { return true }
if shortcut.contains("←") && event.key == .left { return true }
if shortcut.contains("→") && event.key == .right { return true }
// Standard matching
guard let trigger = triggerKey else { return false }
// For character keys, do case-sensitive matching
// "n" only matches 'n', "N" only matches 'N' (Shift+n)
if case .character(let triggerChar) = trigger,
case .character(let eventChar) = event.key {
return triggerChar == eventChar
}
return event.key == trigger
}
}
// MARK: - System Status Bar Items
/// System status bar items that are always present.
///
/// These items are automatically added to the status bar by the framework.
/// They appear in a fixed order and provide essential app-wide functionality.
///
/// System items include:
/// - **quit** (`q`): Exits the application
/// - **help** (`?`): Shows help (not yet implemented)
/// - **theme** (`t`): Cycles through themes (not yet implemented)
public enum SystemStatusBarItem {
/// The quit item (`q quit`).
///
/// This item is always present and exits the application.
public static let quit = StatusBarItem(
shortcut: "q",
label: "quit",
order: .quit
)
/// The help item (`? help`).
///
/// Shows application help. Action must be set by the framework.
public static let help = StatusBarItem(
shortcut: "?",
label: "help",
order: .help
)
/// The theme item (`t theme`).
///
/// Cycles through available themes. Action must be set by the framework.
public static let theme = StatusBarItem(
shortcut: "t",
label: "theme",
order: .theme
)
/// All system items in their default order.
public static var all: [StatusBarItem] {
[quit, help, theme]
}
/// Creates system items with custom actions.
///
/// - Parameters:
/// - onQuit: Action for quit (default: exits app).
/// - onHelp: Action for help (optional).
/// - onTheme: Action for theme cycling (optional).
/// - Returns: Array of configured system items.
public static func items(
onQuit: (@Sendable () -> Void)? = nil,
onHelp: (@Sendable () -> Void)? = nil,
onTheme: (@Sendable () -> Void)? = nil
) -> [StatusBarItem] {
var result: [StatusBarItem] = []
// Quit is always present
result.append(StatusBarItem(
shortcut: "q",
label: "quit",
order: .quit,
action: onQuit
))
// Help is present if action is provided
if let onHelp = onHelp {
result.append(StatusBarItem(
shortcut: "?",
label: "help",
order: .help,
action: onHelp
))
}
// Theme is present if action is provided
if let onTheme = onTheme {
result.append(StatusBarItem(
shortcut: "t",
label: "theme",
order: .theme,
action: onTheme
))
}
return result
}
}
// MARK: - Status Bar Item Builder
/// Result builder for creating status bar items.
@resultBuilder
public struct StatusBarItemBuilder {
public static func buildBlock(_ components: [any StatusBarItemProtocol]...) -> [any StatusBarItemProtocol] {
components.flatMap { $0 }
}
public static func buildArray(_ components: [[any StatusBarItemProtocol]]) -> [any StatusBarItemProtocol] {
components.flatMap { $0 }
}
public static func buildOptional(_ component: [any StatusBarItemProtocol]?) -> [any StatusBarItemProtocol] {
component ?? []
}
public static func buildEither(first component: [any StatusBarItemProtocol]) -> [any StatusBarItemProtocol] {
component
}
public static func buildEither(second component: [any StatusBarItemProtocol]) -> [any StatusBarItemProtocol] {
component
}
public static func buildExpression(_ expression: any StatusBarItemProtocol) -> [any StatusBarItemProtocol] {
[expression]
}
}
// MARK: - StatusBar View
/// A status bar that displays at the bottom of the terminal.
///
/// The status bar shows keyboard shortcuts and their descriptions.
/// It's rendered separately from the main view tree and is never
/// affected by overlays or dimming.
///
/// # Layout
///
/// The status bar consists of two containers:
/// - **User Container** (left): User-defined items, sorted by order
/// - **System Container** (right): System items (quit, help, theme), fixed order
///
/// ```
/// ┌────────────────────────────────────────┬───────────────────────────────┐
/// │ s save x action ↑↓ nav │ q quit ? help t theme │
/// └────────────────────────────────────────┴───────────────────────────────┘
/// ```
///
/// # Usage
///
/// To set status bar items, use the environment:
///
/// ```swift
/// struct MyView: View {
/// @Environment(\.statusBar) var statusBar
///
/// var body: some View {
/// VStack {
/// Text("Hello")
/// }
/// .onAppear {
/// statusBar.setItems([
/// StatusBarItem(shortcut: "s", label: "save"),
/// StatusBarItem(shortcut: "↑↓", label: "nav"),
/// ])
/// }
/// }
/// }
/// ```
public struct StatusBar: View {
/// User items (left container).
public let userItems: [any StatusBarItemProtocol]
/// System items (right container).
public let systemItems: [any StatusBarItemProtocol]
/// The visual style.
public let style: StatusBarStyle
/// The horizontal alignment of user items within the left container.
public let alignment: StatusBarAlignment
/// The highlight color for shortcut keys.
public let highlightColor: Color
/// The label color.
public let labelColor: Color?
/// Creates a status bar with separate user and system items.
///
/// - Parameters:
/// - userItems: User-defined items (left container).
/// - systemItems: System items (right container).
/// - style: The visual style (default: `.compact`).
/// - alignment: The alignment of user items (default: `.leading`).
/// - highlightColor: The color for shortcut keys (default: `.cyan`).
/// - labelColor: The color for labels (default: nil, terminal default).
public init(
userItems: [any StatusBarItemProtocol] = [],
systemItems: [any StatusBarItemProtocol] = [],
style: StatusBarStyle = .compact,
alignment: StatusBarAlignment = .leading,
highlightColor: Color = .cyan,
labelColor: Color? = nil
) {
self.userItems = userItems
self.systemItems = systemItems
self.style = style
self.alignment = alignment
self.highlightColor = highlightColor
self.labelColor = labelColor
}
/// Creates a status bar with all items combined (legacy compatibility).
///
/// - Parameters:
/// - items: All items to display (will be treated as user items).
/// - style: The visual style (default: `.compact`).
/// - alignment: The horizontal alignment (default: `.justified`).
/// - highlightColor: The color for shortcut keys (default: `.cyan`).
/// - labelColor: The color for labels (default: nil, terminal default).
public init(
items: [any StatusBarItemProtocol],
style: StatusBarStyle = .compact,
alignment: StatusBarAlignment = .justified,
highlightColor: Color = .cyan,
labelColor: Color? = nil
) {
self.userItems = items
self.systemItems = []
self.style = style
self.alignment = alignment
self.highlightColor = highlightColor
self.labelColor = labelColor
}
/// Creates a status bar using a builder.
///
/// - Parameters:
/// - style: The visual style.
/// - alignment: The horizontal alignment.
/// - highlightColor: The color for shortcut keys.
/// - labelColor: The color for labels.
/// - builder: A closure that returns items.
public init(
style: StatusBarStyle = .compact,
alignment: StatusBarAlignment = .justified,
highlightColor: Color = .cyan,
labelColor: Color? = nil,
@StatusBarItemBuilder _ builder: () -> [any StatusBarItemProtocol]
) {
self.userItems = builder()
self.systemItems = []
self.style = style
self.alignment = alignment
self.highlightColor = highlightColor
self.labelColor = labelColor
}
/// All items combined (sorted user items, then filtered system items).
///
/// User items are sorted by their `order` property.
/// System items maintain their fixed order (quit, help, theme).
/// User items override system items with the same shortcut.
/// Use this for event handling to check all items.
public var allItems: [any StatusBarItemProtocol] {
let userShortcuts = Set(userItems.map { $0.shortcut })
let filteredSystemItems = systemItems.filter { !userShortcuts.contains($0.shortcut) }
return userItems.sorted { $0.order < $1.order } + filteredSystemItems
}
/// Whether the status bar has any items to display.
public var hasItems: Bool {
!userItems.isEmpty || !systemItems.isEmpty
}
public var body: Never {
fatalError("StatusBar renders via Renderable")
}
}
// MARK: - StatusBar Rendering
extension StatusBar: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
// Get shortcuts used by user items (for deduplication)
let userShortcuts = Set(userItems.map { $0.shortcut })
// Filter out system items that are overridden by user items
let filteredSystemItems = systemItems.filter { !userShortcuts.contains($0.shortcut) }
// Combine: sorted user items + filtered system items (fixed order)
let sortedUserItems = userItems.sorted { $0.order < $1.order }
let combinedItems = sortedUserItems + filteredSystemItems
guard !combinedItems.isEmpty else {
return FrameBuffer()
}
// Build item strings
let itemStrings = combinedItems.map { item -> String in
let shortcutStyled = ANSIRenderer.render(item.shortcut, with: {
var style = TextStyle()
style.foregroundColor = highlightColor
style.isBold = true
return style
}())
let labelStyled: String
if let color = labelColor {
labelStyled = ANSIRenderer.render(" " + item.label, with: {
var style = TextStyle()
style.foregroundColor = color
return style
}())
} else {
labelStyled = " " + item.label
}
return shortcutStyled + labelStyled
}
switch style {
case .compact:
return renderCompact(itemStrings: itemStrings, width: context.availableWidth)
case .bordered:
return renderBordered(itemStrings: itemStrings, width: context.availableWidth)
}
}
/// Aligns content within the given width based on alignment setting.
///
/// - Parameters:
/// - itemStrings: The styled item strings to align.
/// - width: The total available width.
/// - Returns: The aligned content string.
private func alignContent(itemStrings: [String], width: Int) -> String {
let separator = " " // Two spaces between items for non-justified
switch alignment {
case .leading:
let content = " " + itemStrings.joined(separator: separator)
return content.padToVisibleWidth(width)
case .trailing:
let content = itemStrings.joined(separator: separator) + " "
let contentWidth = content.strippedLength
let padding = max(0, width - contentWidth)
return String(repeating: " ", count: padding) + content
case .center:
let content = itemStrings.joined(separator: separator)
let contentWidth = content.strippedLength
let totalPadding = max(0, width - contentWidth)
let leftPadding = totalPadding / 2
let rightPadding = totalPadding - leftPadding
return String(repeating: " ", count: leftPadding) + content + String(repeating: " ", count: rightPadding)
case .justified:
return justifyContent(itemStrings: itemStrings, width: width)
}
}
/// Distributes items evenly across the width (justified alignment).
///
/// Items are distributed so that the space on the left edge, between items,
/// and on the right edge are all equal.
///
/// - Parameters:
/// - itemStrings: The styled item strings to distribute.
/// - width: The total available width.
/// - Returns: The justified content string.
private func justifyContent(itemStrings: [String], width: Int) -> String {
guard !itemStrings.isEmpty else {
return String(repeating: " ", count: width)
}
guard itemStrings.count > 1 else {
// Single item: center it
let content = itemStrings.first ?? ""
let contentWidth = content.strippedLength
let totalPadding = max(0, width - contentWidth)
let leftPadding = totalPadding / 2
let rightPadding = totalPadding - leftPadding
return String(repeating: " ", count: leftPadding) + content + String(repeating: " ", count: rightPadding)
}
// Calculate total content width (without gaps)
let totalContentWidth = itemStrings.reduce(0) { sum, item in
sum + item.strippedLength
}
// For n items, we have n+1 gaps (left edge, between each item, right edge)
let gapCount = itemStrings.count + 1
let availableForGaps = max(0, width - totalContentWidth)
let gapWidth = availableForGaps / gapCount
let extraSpace = availableForGaps % gapCount
// Build justified string with equal gaps
var result = ""
// Left edge gap (gets extra space if available)
let leftGapExtra = extraSpace > 0 ? 1 : 0
result += String(repeating: " ", count: gapWidth + leftGapExtra)
for (index, item) in itemStrings.enumerated() {
result += item
if index < itemStrings.count - 1 {
// Gap between items
// Distribute extra space to middle gaps (after left edge took one if available)
let gapIndex = index + 1 // 0 = left edge, 1..n-1 = between items, n = right edge
let extra = gapIndex < extraSpace ? 1 : 0
result += String(repeating: " ", count: gapWidth + extra)
}
}
// Right edge gap
let rightGapIndex = itemStrings.count
let rightGapExtra = rightGapIndex < extraSpace ? 1 : 0
result += String(repeating: " ", count: gapWidth + rightGapExtra)
// Ensure the result fills the width exactly
return result.padToVisibleWidth(width)
}
/// Renders the compact style (single line with alignment).
private func renderCompact(itemStrings: [String], width: Int) -> FrameBuffer {
let line = alignContent(itemStrings: itemStrings, width: width)
return FrameBuffer(lines: [line])
}
/// Renders the bordered style (block border with alignment).
private func renderBordered(itemStrings: [String], width: Int) -> FrameBuffer {
let border = BorderStyle.block
let innerWidth = width - 2 // Account for left and right border
let content = alignContent(itemStrings: itemStrings, width: innerWidth)
// Build the three lines
let topBorder = String(border.topLeft)
+ String(repeating: border.horizontal, count: innerWidth)
+ String(border.topRight)
let contentLine = String(border.vertical)
+ content
+ ANSIRenderer.reset // Prevent color bleeding
+ String(border.vertical)
let bottomBorder = String(border.bottomLeft)
+ String(repeating: border.horizontal, count: innerWidth)
+ String(border.bottomRight)
return FrameBuffer(lines: [topBorder, contentLine, bottomBorder])
}
}
// MARK: - Status Bar Height Helper
extension StatusBar {
/// The height of the status bar in lines.
public var height: Int {
switch style {
case .compact:
return 1
case .bordered:
return 3
}
}
}