Merge pull request #8 from phranck/feature/swiftlint-integration

feat: Integrate SwiftLint as build plugin
This commit is contained in:
phranck
2026-01-30 14:27:53 +01:00
44 changed files with 483 additions and 389 deletions
+107
View File
@@ -0,0 +1,107 @@
# SwiftLint Configuration for TUIKit
# Only lint project sources (not build artifacts or dependencies)
included:
- Sources
- Tests
excluded:
- .build
- .swiftpm
# Rules
disabled_rules:
# We use fatalError() intentionally in body properties for Renderable views
- fatal_error_message
# Nesting is acceptable for enums inside structs (e.g. Appearance.ID, GeneratedPalette.Hue)
- nesting
# BorderStyle.none follows SwiftUI convention (e.g. Font.Weight.none)
- discouraged_none_name
opt_in_rules:
- closure_end_indentation
- closure_spacing
- collection_alignment
- comma_inheritance
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- explicit_init
- first_where
- flatmap_over_map_reduce
- identical_operands
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- modifier_order
- operator_usage_whitespace
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- redundant_nil_coalescing
- redundant_type_annotation
- return_value_from_void_function
- shorthand_optional_binding
- sorted_first_last
- toggle_bool
- unneeded_parentheses_in_closure_argument
- vertical_whitespace_closing_braces
- yoda_condition
# Rule Configuration
line_length:
warning: 140
error: 200
ignores_urls: true
ignores_comments: true
file_length:
warning: 500
error: 1200
ignore_comment_only_lines: true
type_body_length:
warning: 400
error: 600
function_body_length:
warning: 80
error: 150
function_parameter_count:
warning: 8
error: 12
cyclomatic_complexity:
warning: 15
error: 25
identifier_name:
min_length:
warning: 2
error: 1
max_length:
warning: 60
error: 80
excluded:
- id
- to
large_tuple:
warning: 6
error: 12
type_name:
min_length:
warning: 3
error: 1
max_length:
warning: 50
error: 60
excluded:
- ID
+10 -1
View File
@@ -1,5 +1,5 @@
{
"originHash" : "d7399fa1b3074f6a5debef722dbf8a5621cd2171116ce5f4a91b6c3170b14ece",
"originHash" : "6ffad17119186b233c9c58b654f1e56acc6057ddea719a104c85e9ba6691a8f1",
"pins" : [
{
"identity" : "swift-docc-plugin",
@@ -18,6 +18,15 @@
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
},
{
"identity" : "swiftlintplugins",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
"state" : {
"revision" : "8a4640d14777685ba8f14e832373160498fbab92",
"version" : "0.63.2"
}
}
],
"version" : 3
+9 -2
View File
@@ -22,14 +22,21 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.3"),
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.58.2"),
],
targets: [
.target(
name: "TUIKit"
name: "TUIKit",
plugins: [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
),
.executableTarget(
name: "TUIKitExample",
dependencies: ["TUIKit"]
dependencies: ["TUIKit"],
plugins: [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
),
.testTarget(
name: "TUIKitTests",
+44 -46
View File
@@ -59,7 +59,7 @@ public enum QuitBehavior: Sendable {
/// Pressing `q` will always exit the application, regardless of
/// the current navigation state.
case always
/// Quit only works from the root/main screen.
///
/// Pressing `q` will only exit when no context is pushed onto the
@@ -92,33 +92,33 @@ public enum QuitBehavior: Sendable {
/// ```
public final class StatusBarState: @unchecked Sendable {
// MARK: - User Items
/// Stack of user contexts with their items.
private var userContextStack: [(context: String, items: [any StatusBarItemProtocol])] = []
/// Global user items that are always shown (lowest priority).
private var userGlobalItems: [any StatusBarItemProtocol] = []
// MARK: - System Items Configuration
/// Whether system items are shown at all.
///
/// Set to `false` to hide all system items (quit, help, theme).
/// Default is `true`.
public var showSystemItems: Bool = true
/// Whether the appearance item (`a`) is shown.
///
/// When `true`, pressing `a` cycles through available appearances (border styles).
/// Default is `true`.
public var showAppearanceItem: Bool = true
/// Whether the theme item (`t`) is shown.
///
/// When `true`, pressing `t` cycles through available themes.
/// Default is `true`.
public var showThemeItem: Bool = true
/// Controls when the quit shortcut (`q`) is active.
///
/// - `.always`: Quit works from any screen (default).
@@ -127,9 +127,9 @@ public final class StatusBarState: @unchecked Sendable {
/// When set to `.rootOnly`, pressing `q` on a subpage does nothing,
/// allowing the app to handle navigation (e.g., go back) instead.
public var quitBehavior: QuitBehavior = .always
// MARK: - Appearance
/// The current status bar style.
public var style: StatusBarStyle = .compact
@@ -140,20 +140,20 @@ public final class StatusBarState: @unchecked Sendable {
public var highlightColor: Color = .cyan
/// The label color.
public var labelColor: Color? = nil
public var labelColor: Color?
/// Creates a new status bar state.
public init() {
// System items are built dynamically based on flags
}
// MARK: - System Items Access
/// Whether we are at the root level (no context pushed).
public var isAtRoot: Bool {
userContextStack.isEmpty
}
/// Whether quit is currently allowed based on `quitBehavior`.
public var isQuitAllowed: Bool {
switch quitBehavior {
@@ -163,29 +163,29 @@ public final class StatusBarState: @unchecked Sendable {
return isAtRoot
}
}
/// The current system items based on configuration flags.
///
/// Returns items filtered by `showSystemItems`, `showAppearanceItem`, `showThemeItem`,
/// and `quitBehavior`. The quit item is only included when quit is allowed.
public var currentSystemItems: [StatusBarItem] {
guard showSystemItems else { return [] }
var items: [StatusBarItem] = []
// Quit item respects quitBehavior
if isQuitAllowed {
items.append(SystemStatusBarItem.quit)
}
if showAppearanceItem {
items.append(SystemStatusBarItem.appearance)
}
if showThemeItem {
items.append(SystemStatusBarItem.theme)
}
return items
}
@@ -221,7 +221,7 @@ public final class StatusBarState: @unchecked Sendable {
internal func setItemsSilently(_ items: [any StatusBarItemProtocol]) {
userGlobalItems = items
}
/// The current user items (topmost context or global user items).
///
/// Does not include system items.
@@ -319,13 +319,13 @@ public final class StatusBarState: @unchecked Sendable {
public var currentItems: [any StatusBarItemProtocol] {
// Get shortcuts used by user items (for deduplication)
let userShortcuts = Set(currentUserItems.map { $0.shortcut })
// Filter out system items that are overridden by user items
let filteredSystemItems = currentSystemItems.filter { !userShortcuts.contains($0.shortcut) }
// Sort user items by order, then append system items (fixed order)
let sortedUserItems = currentUserItems.sorted { $0.order < $1.order }
return sortedUserItems + filteredSystemItems
}
@@ -333,7 +333,7 @@ public final class StatusBarState: @unchecked Sendable {
public var hasItems: Bool {
!currentItems.isEmpty
}
/// Whether there are any user items (ignoring system items).
public var hasUserItems: Bool {
!currentUserItems.isEmpty
@@ -362,14 +362,12 @@ public final class StatusBarState: @unchecked Sendable {
/// - Returns: True if an item with an action handled the event.
@discardableResult
public func handleKeyEvent(_ event: KeyEvent) -> Bool {
for item in currentItems {
if item.matches(event) {
if let statusBarItem = item as? StatusBarItem {
// Only consume the event if the item has an action
if statusBarItem.hasAction {
statusBarItem.execute()
return true
}
for item in currentItems where item.matches(event) {
if let statusBarItem = item as? StatusBarItem {
// Only consume the event if the item has an action
if statusBarItem.hasAction {
statusBarItem.execute()
return true
}
}
}
@@ -381,7 +379,7 @@ public final class StatusBarState: @unchecked Sendable {
/// Environment key for accessing the status bar state.
private struct StatusBarKey: EnvironmentKey {
static let defaultValue: StatusBarState = StatusBarState()
static let defaultValue = StatusBarState()
}
extension EnvironmentValues {
@@ -410,14 +408,14 @@ extension EnvironmentValues {
/// and read from the main loop. A single-word Bool write/read is practically
/// atomic on arm64/x86_64. Using `Atomic<Bool>` from the `Synchronization`
/// module would be cleaner but requires macOS 15+.
private nonisolated(unsafe) var needsRerender = false
nonisolated(unsafe) private var needsRerender = false
/// Flag set by the SIGINT signal handler to request a graceful shutdown.
///
/// The actual cleanup (disabling raw mode, restoring cursor, exiting
/// alternate screen) happens in the main loop — signal handlers must
/// not call non-async-signal-safe functions like `write()` or `fflush()`.
private nonisolated(unsafe) var needsShutdown = false
nonisolated(unsafe) private var needsShutdown = false
// MARK: - App Runner
@@ -593,7 +591,7 @@ internal final class AppRunner<A: App> {
)
let buffer = renderToBuffer(statusBarView, context: context)
// Get background color from palette
let bgColor = paletteManager.currentPalette?.background ?? .black
let bgCode = ANSIRenderer.backgroundCode(for: bgColor)
@@ -603,10 +601,10 @@ internal final class AppRunner<A: App> {
// Write status bar with theme background
for (index, line) in buffer.lines.enumerated() {
terminal.moveCursor(toRow: row + index, column: 1)
let visibleWidth = line.strippedLength
let padding = max(0, terminalWidth - visibleWidth)
// Replace all reset codes with "reset + restore background"
let lineWithBg = line.replacingOccurrences(of: reset, with: reset + bgCode)
let paddedLine = bgCode + lineWithBg + String(repeating: " ", count: padding) + reset
@@ -632,17 +630,17 @@ internal final class AppRunner<A: App> {
if statusBar.isQuitAllowed {
isRunning = false
}
case .character(let character) where character == "t" || character == "T":
// 't' cycles palette (if theme item is enabled)
if statusBar.showThemeItem {
paletteManager.cycleNext()
}
case .character(let character) where character == "a" || character == "A":
// 'a' cycles appearance
appearanceManager.cycleNext()
default:
break
}
@@ -690,25 +688,25 @@ extension WindowGroup: SceneRenderable {
let terminal = Terminal.shared
let terminalWidth = terminal.width
let terminalHeight = context.availableHeight
// Get background color from palette
let bgColor = context.environment.palette.background
let bgCode = ANSIRenderer.backgroundCode(for: bgColor)
let reset = ANSIRenderer.reset
// Write buffer to terminal, ensuring consistent background color
for row in 0..<terminalHeight {
terminal.moveCursor(toRow: 1 + row, column: 1)
if row < buffer.lines.count {
let line = buffer.lines[row]
let visibleWidth = line.strippedLength
let padding = max(0, terminalWidth - visibleWidth)
// Replace all reset codes with "reset + restore background"
// This ensures background color persists after styled text
let lineWithBg = line.replacingOccurrences(of: reset, with: reset + bgCode)
// Wrap entire line with background
let paddedLine = bgCode + lineWithBg + String(repeating: " ", count: padding) + reset
terminal.write(paddedLine)
+22 -22
View File
@@ -55,10 +55,10 @@ public struct Appearance: Cyclable, Equatable {
/// The type-safe identifier.
public let rawId: ID
/// The border style used for all controls.
public let borderStyle: BorderStyle
/// Creates a custom appearance.
///
/// - Parameters:
@@ -68,14 +68,14 @@ public struct Appearance: Cyclable, Equatable {
self.rawId = id
self.borderStyle = borderStyle
}
/// Human-readable name derived from ID (conforms to ``Cyclable``).
public var name: String {
rawId.rawValue.capitalized
}
/// Equatable conformance based on the type-safe ID.
public static func == (lhs: Appearance, rhs: Appearance) -> Bool {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.rawId == rhs.rawId && lhs.borderStyle == rhs.borderStyle
}
}
@@ -100,25 +100,25 @@ extension Appearance {
/// ```
public struct ID: RawRepresentable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
/// Single line borders (┌─┐).
public static let line = ID(rawValue: "line")
public static let line = Self(rawValue: "line")
/// Rounded corners (╭─╮).
public static let rounded = ID(rawValue: "rounded")
public static let rounded = Self(rawValue: "rounded")
/// Double-line borders (╔═╗).
public static let doubleLine = ID(rawValue: "doubleLine")
public static let doubleLine = Self(rawValue: "doubleLine")
/// Heavy/bold borders (┏━┓).
public static let heavy = ID(rawValue: "heavy")
public static let heavy = Self(rawValue: "heavy")
/// Block/solid borders (███).
public static let block = ID(rawValue: "block")
public static let block = Self(rawValue: "block")
}
}
@@ -129,27 +129,27 @@ extension Appearance {
///
/// Uses `BorderStyle.line` with standard box-drawing characters.
public static let line = Appearance(id: .line, borderStyle: .line)
/// Rounded corners (default).
///
/// Uses `BorderStyle.rounded` with curved corner characters.
public static let rounded = Appearance(id: .rounded, borderStyle: .rounded)
/// Double-line borders.
///
/// Uses `BorderStyle.doubleLine` for a more prominent look.
public static let doubleLine = Appearance(id: .doubleLine, borderStyle: .doubleLine)
/// Heavy/bold borders.
///
/// Uses `BorderStyle.heavy` for bold, prominent borders.
public static let heavy = Appearance(id: .heavy, borderStyle: .heavy)
/// Block/solid borders.
///
/// Uses `BorderStyle.block` with solid block characters.
public static let block = Appearance(id: .block, borderStyle: .block)
/// The default appearance (rounded).
public static let `default`: Appearance = .rounded
}
@@ -168,7 +168,7 @@ public struct AppearanceRegistry {
.heavy,
.block
]
/// Finds an appearance by ID.
///
/// - Parameter id: The appearance ID to find.
@@ -213,7 +213,7 @@ extension EnvironmentValues {
/// Environment key for the appearance manager.
private struct AppearanceManagerKey: EnvironmentKey {
static let defaultValue: ThemeManager = ThemeManager(
static let defaultValue = ThemeManager(
items: AppearanceRegistry.all,
applyToEnvironment: { item in
if let appearance = item as? Appearance {
+12 -12
View File
@@ -27,10 +27,10 @@ public struct BorderStyle: Sendable, Equatable {
/// Vertical edge character.
public let vertical: Character
/// Left T-junction character (├).
public let leftT: Character
/// Right T-junction character (┤).
public let rightT: Character
@@ -67,7 +67,7 @@ public struct BorderStyle: Sendable, Equatable {
/// │ Content│
/// └────────┘
/// ```
public static let line = BorderStyle(
public static let line = Self(
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
@@ -87,7 +87,7 @@ public struct BorderStyle: Sendable, Equatable {
/// ║ Content║
/// ╚════════╝
/// ```
public static let doubleLine = BorderStyle(
public static let doubleLine = Self(
topLeft: "╔",
topRight: "╗",
bottomLeft: "╚",
@@ -107,7 +107,7 @@ public struct BorderStyle: Sendable, Equatable {
/// │ Content│
/// ╰────────╯
/// ```
public static let rounded = BorderStyle(
public static let rounded = Self(
topLeft: "╭",
topRight: "╮",
bottomLeft: "╰",
@@ -127,7 +127,7 @@ public struct BorderStyle: Sendable, Equatable {
/// ┃ Content┃
/// ┗━━━━━━━━┛
/// ```
public static let heavy = BorderStyle(
public static let heavy = Self(
topLeft: "┏",
topRight: "┓",
bottomLeft: "┗",
@@ -152,7 +152,7 @@ public struct BorderStyle: Sendable, Equatable {
/// █ Footer █
/// ▀▀▀▀▀▀▀▀▀▀ ← Bottom: upper half block
/// ```
public static let block = BorderStyle(
public static let block = Self(
topLeft: "▄",
topRight: "▄",
bottomLeft: "▀",
@@ -162,19 +162,19 @@ public struct BorderStyle: Sendable, Equatable {
leftT: "▀", // Header/Body separator (upper half)
rightT: "▀"
)
/// The character used for the bottom edge of block style.
///
/// Block style uses different characters for top (▄) and bottom (▀).
public static let blockBottomHorizontal: Character = "▀"
/// The character used for body/footer separator in block style.
///
/// Uses lower half block (▄) to create visual separation.
public static let blockFooterSeparator: Character = "▄"
/// No visible border (space characters).
public static let none = BorderStyle(
public static let none = Self(
topLeft: " ",
topRight: " ",
bottomLeft: " ",
@@ -184,7 +184,7 @@ public struct BorderStyle: Sendable, Equatable {
leftT: " ",
rightT: " "
)
/// ASCII-only border (+ - |).
///
/// Maximum compatibility with all terminals, including those that
@@ -197,7 +197,7 @@ public struct BorderStyle: Sendable, Equatable {
/// | Content|
/// +--------+
/// ```
public static let ascii = BorderStyle(
public static let ascii = Self(
topLeft: "+",
topRight: "+",
bottomLeft: "+",
+39 -39
View File
@@ -38,77 +38,77 @@ public struct Color: Sendable, Equatable {
// MARK: - Standard ANSI Colors
/// Black (ANSI 30/40)
public static let black = Color(value: .standard(.black))
public static let black = Self(value: .standard(.black))
/// Red (ANSI 31/41)
public static let red = Color(value: .standard(.red))
public static let red = Self(value: .standard(.red))
/// Green (ANSI 32/42)
public static let green = Color(value: .standard(.green))
public static let green = Self(value: .standard(.green))
/// Yellow (ANSI 33/43)
public static let yellow = Color(value: .standard(.yellow))
public static let yellow = Self(value: .standard(.yellow))
/// Blue (ANSI 34/44)
public static let blue = Color(value: .standard(.blue))
public static let blue = Self(value: .standard(.blue))
/// Magenta (ANSI 35/45)
public static let magenta = Color(value: .standard(.magenta))
public static let magenta = Self(value: .standard(.magenta))
/// Cyan (ANSI 36/46)
public static let cyan = Color(value: .standard(.cyan))
public static let cyan = Self(value: .standard(.cyan))
/// White (ANSI 37/47)
public static let white = Color(value: .standard(.white))
public static let white = Self(value: .standard(.white))
/// Default color (terminal default)
public static let `default` = Color(value: .standard(.`default`))
public static let `default` = Self(value: .standard(.`default`))
// MARK: - Bright ANSI Colors
/// Bright black (gray)
public static let brightBlack = Color(value: .bright(.black))
public static let brightBlack = Self(value: .bright(.black))
/// Bright red
public static let brightRed = Color(value: .bright(.red))
public static let brightRed = Self(value: .bright(.red))
/// Bright green
public static let brightGreen = Color(value: .bright(.green))
public static let brightGreen = Self(value: .bright(.green))
/// Bright yellow
public static let brightYellow = Color(value: .bright(.yellow))
public static let brightYellow = Self(value: .bright(.yellow))
/// Bright blue
public static let brightBlue = Color(value: .bright(.blue))
public static let brightBlue = Self(value: .bright(.blue))
/// Bright magenta
public static let brightMagenta = Color(value: .bright(.magenta))
public static let brightMagenta = Self(value: .bright(.magenta))
/// Bright cyan
public static let brightCyan = Color(value: .bright(.cyan))
public static let brightCyan = Self(value: .bright(.cyan))
/// Bright white
public static let brightWhite = Color(value: .bright(.white))
public static let brightWhite = Self(value: .bright(.white))
// MARK: - Semantic Colors
/// Primary color (default: blue)
public static let primary = Color.blue
public static let primary = Self.blue
/// Secondary color (default: gray)
public static let secondary = Color.brightBlack
public static let secondary = Self.brightBlack
/// Accent color (default: cyan)
public static let accent = Color.cyan
public static let accent = Self.cyan
/// Warning color
public static let warning = Color.yellow
public static let warning = Self.yellow
/// Error color
public static let error = Color.red
public static let error = Self.red
/// Success color
public static let success = Color.green
public static let success = Self.green
// MARK: - Custom Colors
@@ -116,8 +116,8 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter index: The palette index (0-255).
/// - Returns: The corresponding color.
public static func palette(_ index: UInt8) -> Color {
Color(value: .palette256(index))
public static func palette(_ index: UInt8) -> Self {
Self(value: .palette256(index))
}
/// Creates a True Color RGB color.
@@ -127,15 +127,15 @@ public struct Color: Sendable, Equatable {
/// - green: The green component (0-255).
/// - blue: The blue component (0-255).
/// - Returns: The RGB color.
public static func rgb(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> Color {
Color(value: .rgb(red: red, green: green, blue: blue))
public static func rgb(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> Self {
Self(value: .rgb(red: red, green: green, blue: blue))
}
/// Creates a color from a hex value.
///
/// - Parameter hex: The hex value (e.g., 0xFF5500).
/// - Returns: The corresponding RGB color.
public static func hex(_ hex: UInt32) -> Color {
public static func hex(_ hex: UInt32) -> Self {
let red = UInt8((hex >> 16) & 0xFF)
let green = UInt8((hex >> 8) & 0xFF)
let blue = UInt8(hex & 0xFF)
@@ -148,7 +148,7 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter hex: The hex string (e.g., "#FF5500", "F50", "#abc").
/// - Returns: The corresponding RGB color, or nil if invalid.
public static func hex(_ hex: String) -> Color? {
public static func hex(_ hex: String) -> Self? {
var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove # prefix if present
@@ -178,7 +178,7 @@ public struct Color: Sendable, Equatable {
/// - saturation: The saturation component (0-100).
/// - lightness: The lightness component (0-100).
/// - Returns: The corresponding RGB color.
public static func hsl(_ hue: Double, _ saturation: Double, _ lightness: Double) -> Color {
public static func hsl(_ hue: Double, _ saturation: Double, _ lightness: Double) -> Self {
let normalizedHue = hue / 360.0
let normalizedSaturation = saturation / 100.0
let normalizedLightness = lightness / 100.0
@@ -198,15 +198,15 @@ public struct Color: Sendable, Equatable {
var adjustedHue = hueComponent
if adjustedHue < 0 { adjustedHue += 1 }
if adjustedHue > 1 { adjustedHue -= 1 }
if adjustedHue < 1/6 { return luminance + (chroma - luminance) * 6 * adjustedHue }
if adjustedHue < 1/2 { return chroma }
if adjustedHue < 2/3 { return luminance + (chroma - luminance) * (2/3 - adjustedHue) * 6 }
if adjustedHue < 1 / 6 { return luminance + (chroma - luminance) * 6 * adjustedHue }
if adjustedHue < 1 / 2 { return chroma }
if adjustedHue < 2 / 3 { return luminance + (chroma - luminance) * (2 / 3 - adjustedHue) * 6 }
return luminance
}
let red = UInt8(hueToRGB(luminanceFactor, chromaFactor, normalizedHue + 1/3) * 255)
let red = UInt8(hueToRGB(luminanceFactor, chromaFactor, normalizedHue + 1 / 3) * 255)
let green = UInt8(hueToRGB(luminanceFactor, chromaFactor, normalizedHue) * 255)
let blue = UInt8(hueToRGB(luminanceFactor, chromaFactor, normalizedHue - 1/3) * 255)
let blue = UInt8(hueToRGB(luminanceFactor, chromaFactor, normalizedHue - 1 / 3) * 255)
return .rgb(red, green, blue)
}
@@ -215,7 +215,7 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter amount: The amount to lighten (0-1, default 0.2).
/// - Returns: A lighter color.
public func lighter(by amount: Double = 0.2) -> Color {
public func lighter(by amount: Double = 0.2) -> Self {
adjusted(by: amount)
}
@@ -223,7 +223,7 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter amount: The amount to darken (0-1, default 0.2).
/// - Returns: A darker color.
public func darker(by amount: Double = 0.2) -> Color {
public func darker(by amount: Double = 0.2) -> Self {
adjusted(by: -amount)
}
@@ -233,7 +233,7 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter amount: The adjustment amount (-1 to 1).
/// - Returns: The adjusted color, or self if not an RGB color.
private func adjusted(by amount: Double) -> Color {
private func adjusted(by amount: Double) -> Self {
guard case .rgb(let red, let green, let blue) = value else {
return self
}
@@ -253,7 +253,7 @@ public struct Color: Sendable, Equatable {
///
/// - Parameter opacity: The opacity (0-1).
/// - Returns: A color simulating the given opacity.
public func opacity(_ opacity: Double) -> Color {
public func opacity(_ opacity: Double) -> Self {
guard case .rgb(let red, let green, let blue) = value else {
return self
}
+2 -2
View File
@@ -72,7 +72,7 @@ public struct EnvironmentValues: @unchecked Sendable {
/// - keyPath: The key path to the value to modify.
/// - value: The new value.
/// - Returns: A new EnvironmentValues with the modified value.
public func setting<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>, to value: V) -> EnvironmentValues {
public func setting<V>(_ keyPath: WritableKeyPath<Self, V>, to value: V) -> Self {
var copy = self
copy[keyPath: keyPath] = value
return copy
@@ -90,7 +90,7 @@ public final class EnvironmentStorage: @unchecked Sendable {
public static let shared = EnvironmentStorage()
/// The current environment values.
private var current: EnvironmentValues = EnvironmentValues()
private var current = EnvironmentValues()
/// Stack of environments for nested rendering.
private var stack: [EnvironmentValues] = []
+1 -1
View File
@@ -250,7 +250,7 @@ public final class FocusManager: @unchecked Sendable {
/// Environment key for the focus manager.
private struct FocusManagerKey: EnvironmentKey {
static let defaultValue: FocusManager = FocusManager()
static let defaultValue = FocusManager()
}
extension EnvironmentValues {
+3 -5
View File
@@ -67,7 +67,7 @@ public enum Key: Hashable, Sendable {
case character(Character)
/// Creates a Key from a character if it's a simple character.
public static func from(_ char: Character) -> Key {
public static func from(_ char: Character) -> Self {
.character(char)
}
}
@@ -274,10 +274,8 @@ public final class KeyEventDispatcher: @unchecked Sendable {
@discardableResult
public func dispatch(_ event: KeyEvent) -> Bool {
// Process in reverse order (most recent handlers first)
for handler in handlers.reversed() {
if handler(event) {
return true
}
for handler in handlers.reversed() where handler(event) {
return true
}
return false
}
+1 -5
View File
@@ -84,7 +84,7 @@ public struct PreferenceValues: @unchecked Sendable {
/// Merges another set of preference values into this one.
///
/// - Parameter other: The other preference values to merge.
public mutating func merge(_ other: PreferenceValues) {
public mutating func merge(_ other: Self) {
for (key, value) in other.storage {
storage[key] = value
}
@@ -246,8 +246,6 @@ extension OnPreferenceChangeModifier: Renderable {
}
}
// MARK: - Common Preference Keys
/// A preference key for the navigation title.
@@ -272,5 +270,3 @@ public struct AnchorPreferenceKey: PreferenceKey {
value.merge(nextValue()) { _, new in new }
}
}
-2
View File
@@ -71,5 +71,3 @@ public struct ViewArray<Element: View>: View {
fatalError("ViewArray renders its children directly")
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ public struct Binding<Value> {
/// - Parameter value: The constant value.
/// - Returns: A binding that always returns the given value.
public static func constant(_ value: Value) -> Binding<Value> {
Binding(get: { value }, set: { _ in })
Self(get: { value }, set: { _ in })
}
}
+36 -36
View File
@@ -83,7 +83,7 @@ public protocol Palette: Cyclable {
/// Selection highlight color (foreground).
var selection: Color { get }
/// Selection background color (dimmed accent).
var selectionBackground: Color { get }
@@ -100,7 +100,7 @@ public protocol Palette: Cyclable {
/// Status bar shortcut highlight color.
var statusBarHighlight: Color { get }
// MARK: - Container Colors (for block appearance)
/// Container body background (used in block appearance).
@@ -254,7 +254,7 @@ public enum PaletteColors {
/// Selection color.
public static var selection: Color { current.selection }
/// Selection background color.
public static var selectionBackground: Color { current.selectionBackground }
@@ -269,10 +269,10 @@ public enum PaletteColors {
/// Status bar highlight.
public static var statusBarHighlight: Color { current.statusBarHighlight }
/// Container body background (for block appearance).
public static var containerBackground: Color { current.containerBackground }
/// Container header/footer background (for block appearance).
public static var containerHeaderBackground: Color { current.containerHeaderBackground }
}
@@ -291,33 +291,33 @@ public struct GreenPhosphorPalette: Palette {
public let background = Color.hex(0x060A07) // App background (darkest)
public let backgroundSecondary = Color.hex(0x0E271C) // Container body background (brighter)
public let backgroundTertiary = Color.hex(0x0A1B13) // Header/footer background
// Green phosphor text hierarchy
public let foreground = Color.hex(0x33FF33) // Bright green - primary text
public let foregroundSecondary = Color.hex(0x27C227) // Medium green - secondary text
public let foregroundTertiary = Color.hex(0x1F8F1F) // Dim green - tertiary/muted text
// Accent colors
public let accent = Color.hex(0x66FF66) // Lighter green for highlights
public let accentSecondary = Color.hex(0x00CC00) // Darker accent
// Semantic colors (stay in green family)
public let success = Color.hex(0x33FF33)
public let warning = Color.hex(0xCCFF33) // Yellow-green
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
public let info = Color.hex(0x33FFCC) // Cyan-green
// UI elements
public let border = Color.hex(0x2D5A2D) // Subtle green border
public let borderFocused = Color.hex(0x33FF33) // Bright when focused
public let selection = Color.hex(0x66FF66) // Bright green for selection text
public let selectionBackground = Color.hex(0x1A4D1A) // Dark green for selection bar bg
// Status bar
public let statusBarBackground = Color.hex(0x0F2215) // Dark green for status bar
public let statusBarForeground = Color.hex(0x2FDD2F) // Slightly dimmer than primary foreground
public let statusBarHighlight = Color.hex(0x66FF66)
// Container colors for block appearance
public var containerBackground: Color { backgroundSecondary } // #0E271C - body
public var containerHeaderBackground: Color { backgroundTertiary } // #0A1B13 - header/footer
@@ -338,33 +338,33 @@ public struct AmberPhosphorPalette: Palette {
public let background = Color.hex(0x0A0706) // App background (darkest)
public let backgroundSecondary = Color.hex(0x251710) // Container body background (brighter)
public let backgroundTertiary = Color.hex(0x1E110E) // Header/footer background
// Amber phosphor text hierarchy (matching Spotnik)
public let foreground = Color.hex(0xFFAA00) // Bright amber - primary text
public let foregroundSecondary = Color.hex(0xCC8800) // Medium amber - secondary text
public let foregroundTertiary = Color.hex(0x8F6600) // Dim amber - tertiary/muted text
// Accent colors
public let accent = Color.hex(0xFFCC33) // Lighter amber for highlights
public let accentSecondary = Color.hex(0xCC9900) // Darker accent
// Semantic colors (stay in amber family)
public let success = Color.hex(0xFFCC00)
public let warning = Color.hex(0xFFE066) // Light amber
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
public let info = Color.hex(0xFFD966) // Light amber
// UI elements
public let border = Color.hex(0x5A4A2D) // Subtle amber border
public let borderFocused = Color.hex(0xFFAA00) // Bright when focused
public let selection = Color.hex(0xFFCC33) // Bright amber for selection text
public let selectionBackground = Color.hex(0x4D3A1F) // Dark amber for selection bar bg
// Status bar
public let statusBarBackground = Color.hex(0x191613) // Same as header/footer
public let statusBarForeground = Color.hex(0xFFAA00)
public let statusBarHighlight = Color.hex(0xFFCC33)
// Container colors for block appearance
public var containerBackground: Color { backgroundSecondary } // Body
public var containerHeaderBackground: Color { backgroundTertiary } // Header/footer
@@ -385,33 +385,33 @@ public struct WhitePhosphorPalette: Palette {
public let background = Color.hex(0x06070A) // App background (darkest)
public let backgroundSecondary = Color.hex(0x111A2A) // Container body background (brighter)
public let backgroundTertiary = Color.hex(0x0D131D) // Header/footer background
// White/gray phosphor text hierarchy
public let foreground = Color.hex(0xE8E8E8) // Bright white - primary text
public let foregroundSecondary = Color.hex(0xB0B0B0) // Medium gray - secondary text
public let foregroundTertiary = Color.hex(0x787878) // Dim gray - tertiary/muted text
// Accent colors
public let accent = Color.hex(0xFFFFFF) // Pure white for highlights
public let accentSecondary = Color.hex(0xC0C0C0) // Light gray accent
// Semantic colors (subtle tints)
public let success = Color.hex(0xC0FFC0) // Slight green tint
public let warning = Color.hex(0xFFE0A0) // Slight amber tint
public let error = Color.hex(0xFFA0A0) // Slight red tint
public let info = Color.hex(0xA0D0FF) // Slight blue tint
// UI elements
public let border = Color.hex(0x484848) // Subtle gray border
public let borderFocused = Color.hex(0xE8E8E8) // Bright when focused
public let selection = Color.hex(0xFFFFFF) // White for selection text
public let selectionBackground = Color.hex(0x3A3A3A) // Dark gray for selection bar bg
// Status bar
public let statusBarBackground = Color.hex(0x131619) // Same as header/footer
public let statusBarForeground = Color.hex(0xDCDCDC) // Slightly dimmer than primary foreground
public let statusBarHighlight = Color.hex(0xFFFFFF)
// Container colors for block appearance
public var containerBackground: Color { backgroundSecondary } // Body
public var containerHeaderBackground: Color { backgroundTertiary } // Header/footer
@@ -432,33 +432,33 @@ public struct RedPhosphorPalette: Palette {
public let background = Color.hex(0x0A0606) // App background (darkest)
public let backgroundSecondary = Color.hex(0x281112) // Container body background (brighter)
public let backgroundTertiary = Color.hex(0x1E0F10) // Header/footer background
// Red phosphor text hierarchy
public let foreground = Color.hex(0xFF4444) // Bright red - primary text
public let foregroundSecondary = Color.hex(0xCC3333) // Medium red - secondary text
public let foregroundTertiary = Color.hex(0x8F2222) // Dim red - tertiary/muted text
// Accent colors
public let accent = Color.hex(0xFF6666) // Lighter red for highlights
public let accentSecondary = Color.hex(0xCC4444) // Darker accent
// Semantic colors (stay in red family)
public let success = Color.hex(0xFF8080) // Light red (success in red theme)
public let warning = Color.hex(0xFFAA66) // Orange
public let error = Color.hex(0xFFFFFF) // White (stands out as error)
public let info = Color.hex(0xFF9999) // Light red
// UI elements
public let border = Color.hex(0x5A2D2D) // Subtle red border
public let borderFocused = Color.hex(0xFF4444) // Bright when focused
public let selection = Color.hex(0xFF6666) // Bright red for selection text
public let selectionBackground = Color.hex(0x4D1F1F) // Dark red for selection bar bg
// Status bar
public let statusBarBackground = Color.hex(0x191313) // Same as header/footer
public let statusBarForeground = Color.hex(0xF23B3B) // Slightly dimmer than primary foreground
public let statusBarHighlight = Color.hex(0xFF6666)
// Container colors for block appearance
public var containerBackground: Color { backgroundSecondary } // Body
public var containerHeaderBackground: Color { backgroundTertiary } // Header/footer
@@ -600,19 +600,19 @@ public struct GeneratedPalette: Palette, Sendable {
// --- Semantic: hue-shifted from base ---
// success = base + 120° (toward green family)
let successHue = GeneratedPalette.wrapHue(hue + 120)
let successHue = Self.wrapHue(hue + 120)
self.success = Color.hsl(successHue, baseSaturation * 0.70, 65)
// warning = base + 60° (toward yellow family)
let warningHue = GeneratedPalette.wrapHue(hue + 60)
let warningHue = Self.wrapHue(hue + 60)
self.warning = Color.hsl(warningHue, baseSaturation * 0.80, 70)
// error = base + 180° (complementary)
let errorHue = GeneratedPalette.wrapHue(hue + 180)
let errorHue = Self.wrapHue(hue + 180)
self.error = Color.hsl(errorHue, baseSaturation * 0.85, 65)
// info = base − 60° (analogous cool side)
let infoHue = GeneratedPalette.wrapHue(hue - 60)
let infoHue = Self.wrapHue(hue - 60)
self.info = Color.hsl(infoHue, baseSaturation * 0.70, 70)
// --- UI elements ---
@@ -652,9 +652,9 @@ public struct GeneratedPalette: Palette, Sendable {
// MARK: - Presets
/// A green generated palette — for direct comparison with GreenPhosphorPalette.
public static let green = GeneratedPalette(name: "Gen. Green", hue: Hue.green)
public static let green = Self(name: "Gen. Green", hue: Hue.green)
/// A violet generated palette.
public static let violet = GeneratedPalette(name: "Violet", hue: Hue.violet)
public static let violet = Self(name: "Violet", hue: Hue.violet)
}
// MARK: - Palette Registry
@@ -731,7 +731,7 @@ extension Palette where Self == GeneratedPalette {
/// Environment key for the palette manager.
private struct PaletteManagerKey: EnvironmentKey {
static let defaultValue: ThemeManager = ThemeManager(
static let defaultValue = ThemeManager(
items: PaletteRegistry.all,
applyToEnvironment: { item in
if let palette = item as? any Palette {
+2
View File
@@ -1,3 +1,4 @@
// swiftlint:disable large_tuple
//
// TupleViews.swift
// TUIKit
@@ -139,3 +140,4 @@ public struct TupleView10<V0: View, V1: View, V2: View, V3: View, V4: View, V5:
fatalError("TupleView10 renders its children directly")
}
}
// swiftlint:enable large_tuple
@@ -174,7 +174,7 @@ public final class UserDefaultsStorage: StorageBackend, @unchecked Sendable {
/// Sets a string value for the given key.
public func set(_ value: String?, forKey key: String) {
if let value = value {
if let value {
setValue(value, forKey: key)
} else {
removeValue(forKey: key)
@@ -198,7 +198,7 @@ public final class UserDefaultsStorage: StorageBackend, @unchecked Sendable {
/// Sets a data value for the given key.
public func set(_ value: Data?, forKey key: String) {
if let value = value {
if let value {
setValue(value, forKey: key)
} else {
removeValue(forKey: key)
+2
View File
@@ -1,3 +1,4 @@
// swiftlint:disable function_parameter_count
//
// ViewBuilder.swift
// TUIKit
@@ -190,3 +191,4 @@ public struct ViewBuilder {
expression
}
}
// swiftlint:enable function_parameter_count
-2
View File
@@ -57,5 +57,3 @@ extension ModifiedView: Renderable {
return modifier.modify(buffer: childBuffer, context: context)
}
}
@@ -19,11 +19,11 @@ public struct BackgroundModifier: ViewModifier {
for line in buffer.lines {
// Pad the line to full width so background covers everything
let paddedLine = line.padToVisibleWidth(width)
// Apply background color to the entire line
var style = TextStyle()
style.backgroundColor = color
// We need to handle existing ANSI codes in the line
// For simplicity, we wrap the whole line with background
let colored = applyBackground(to: paddedLine, color: color)
@@ -38,5 +38,3 @@ public struct BackgroundModifier: ViewModifier {
ANSIRenderer.backgroundCode(for: color) + string + ANSIRenderer.reset
}
}
@@ -31,7 +31,7 @@ extension BorderedView: Renderable {
// Resolve border style - use explicit or fall back to appearance default
let effectiveStyle = style ?? context.environment.appearance.borderStyle
let isBlockAppearance = context.environment.appearance.rawId == .block
// Reduce available width for content by 2 (left + right border)
var contentContext = context
contentContext.availableWidth = max(1, context.availableWidth - BorderRenderer.borderWidthOverhead)
@@ -43,14 +43,14 @@ extension BorderedView: Renderable {
let contentWidth = buffer.width
let innerWidth = max(contentWidth, 1)
if isBlockAppearance {
return renderBlockStyle(buffer: buffer, innerWidth: innerWidth)
} else {
return renderStandardStyle(buffer: buffer, innerWidth: innerWidth, style: effectiveStyle)
}
}
/// Renders with standard box-drawing characters.
private func renderStandardStyle(buffer: FrameBuffer, innerWidth: Int, style: BorderStyle) -> FrameBuffer {
let borderColor = color ?? Color.theme.border
@@ -66,7 +66,7 @@ extension BorderedView: Renderable {
return FrameBuffer(lines: lines)
}
/// Renders with half-block characters for block appearance.
private func renderBlockStyle(buffer: FrameBuffer, innerWidth: Int) -> FrameBuffer {
let containerBg = Color.theme.containerBackground
@@ -81,4 +81,3 @@ extension BorderedView: Renderable {
return FrameBuffer(lines: lines)
}
}
@@ -57,5 +57,3 @@ extension DimmedModifier: Renderable {
return ANSIRenderer.dim + text + ANSIRenderer.reset
}
}
@@ -165,5 +165,3 @@ extension FlexibleFrameView: Renderable {
}
}
}
@@ -46,5 +46,3 @@ extension KeyPressModifier: Renderable {
return TUIKit.renderToBuffer(content, context: context)
}
}
@@ -286,5 +286,3 @@ final class TokenGenerator: @unchecked Sendable {
return "lifecycle-\(counter)"
}
}
@@ -73,5 +73,3 @@ extension OverlayModifier: Renderable {
return baseBuffer.composited(with: overlayBuffer, at: (x: horizontalOffset, y: verticalOffset))
}
}
@@ -59,16 +59,16 @@ public struct Edge: OptionSet, Sendable {
}
/// The top edge.
public static let top = Edge(rawValue: 1 << 0)
public static let top = Self(rawValue: 1 << 0)
/// The leading (left) edge.
public static let leading = Edge(rawValue: 1 << 1)
public static let leading = Self(rawValue: 1 << 1)
/// The bottom edge.
public static let bottom = Edge(rawValue: 1 << 2)
public static let bottom = Self(rawValue: 1 << 2)
/// The trailing (right) edge.
public static let trailing = Edge(rawValue: 1 << 3)
public static let trailing = Self(rawValue: 1 << 3)
/// All edges.
public static let all: Edge = [.top, .leading, .bottom, .trailing]
@@ -84,7 +84,7 @@ public struct Edge: OptionSet, Sendable {
public struct PaddingModifier: ViewModifier {
/// The padding insets.
public let insets: EdgeInsets
/// Creates a padding modifier.
///
/// - Parameter insets: The padding insets.
@@ -101,7 +101,7 @@ public struct PaddingModifier: ViewModifier {
// Calculate line width
let lineWidth = buffer.width + insets.leading + insets.trailing
let emptyLine = String(repeating: " ", count: lineWidth)
// Top padding (full lines)
for _ in 0..<insets.top {
result.append(emptyLine)
@@ -120,5 +120,3 @@ public struct PaddingModifier: ViewModifier {
return FrameBuffer(lines: result)
}
}
@@ -67,5 +67,3 @@ extension StatusBarItemsModifier: Renderable {
return TUIKit.renderToBuffer(content, context: renderContext)
}
}
+1 -1
View File
@@ -137,7 +137,7 @@ public enum ANSIRenderer {
return ["48", "2", "\(red)", "\(green)", "\(blue)"]
}
}
/// Generates the ANSI escape sequence for a background color.
///
/// Use this to set only the background color without other styles.
+6 -8
View File
@@ -65,7 +65,7 @@ public struct FrameBuffer {
/// - Parameters:
/// - other: The buffer to append below.
/// - spacing: Number of empty lines between the two buffers.
public mutating func appendVertically(_ other: FrameBuffer, spacing: Int = 0) {
public mutating func appendVertically(_ other: Self, spacing: Int = 0) {
if !lines.isEmpty && !other.isEmpty && spacing > 0 {
lines.append(contentsOf: Array(repeating: "", count: spacing))
}
@@ -77,7 +77,7 @@ public struct FrameBuffer {
/// - Parameters:
/// - other: The buffer to append to the right.
/// - spacing: Number of space characters between the two buffers.
public mutating func appendHorizontally(_ other: FrameBuffer, spacing: Int = 0) {
public mutating func appendHorizontally(_ other: Self, spacing: Int = 0) {
let maxHeight = max(height, other.height)
let myWidth = width
let spacer = String(repeating: " ", count: spacing)
@@ -100,7 +100,7 @@ public struct FrameBuffer {
/// For simplicity, this just overlays line by line.
///
/// - Parameter overlay: The buffer to overlay on top.
public mutating func overlay(_ overlay: FrameBuffer) {
public mutating func overlay(_ overlay: Self) {
let maxHeight = max(height, overlay.height)
var result: [String] = []
for row in 0..<maxHeight {
@@ -124,7 +124,7 @@ public struct FrameBuffer {
/// - overlay: The buffer to composite on top.
/// - position: The (x, y) offset where the overlay should be placed.
/// - Returns: A new buffer with the overlay composited.
public func composited(with overlay: FrameBuffer, at position: (x: Int, y: Int)) -> FrameBuffer {
public func composited(with overlay: Self, at position: (x: Int, y: Int)) -> Self {
guard !overlay.isEmpty else { return self }
let resultWidth = max(width, position.x + overlay.width)
@@ -158,7 +158,7 @@ public struct FrameBuffer {
result.append(baseLine)
}
return FrameBuffer(lines: result)
return Self(lines: result)
}
/// Inserts overlay text into base text at the specified column position.
@@ -206,12 +206,10 @@ public struct FrameBuffer {
/// (the parent stack then decides the actual layout direction).
///
/// - Parameter buffers: The buffers to stack vertically.
public init(verticallyStacking buffers: [FrameBuffer]) {
public init(verticallyStacking buffers: [Self]) {
self.init()
for buffer in buffers {
appendVertically(buffer)
}
}
}
+2 -2
View File
@@ -58,8 +58,8 @@ public struct RenderContext {
///
/// - Parameter environment: The new environment values.
/// - Returns: A new RenderContext with the updated environment.
public func withEnvironment(_ environment: EnvironmentValues) -> RenderContext {
RenderContext(
public func withEnvironment(_ environment: EnvironmentValues) -> Self {
Self(
terminal: terminal,
availableWidth: availableWidth,
availableHeight: availableHeight,
+4 -4
View File
@@ -152,7 +152,7 @@ public final class Terminal: @unchecked Sendable {
public func clear() {
write(ANSIRenderer.clearScreen + ANSIRenderer.moveCursor(toRow: 1, column: 1))
}
/// Fills the entire screen with a background color.
///
/// This clears the screen and fills every cell with the specified color.
@@ -162,15 +162,15 @@ public final class Terminal: @unchecked Sendable {
public func fillBackground(_ color: Color) {
let size = getSize()
let bgCode = ANSIRenderer.backgroundCode(for: color)
// Move to top-left and fill each line
var output = ANSIRenderer.moveCursor(toRow: 1, column: 1)
let emptyLine = bgCode + String(repeating: " ", count: size.width) + ANSIRenderer.reset
for _ in 0..<size.height {
output += emptyLine
}
// Move cursor back to top-left
output += ANSIRenderer.moveCursor(toRow: 1, column: 1)
write(output)
+14 -14
View File
@@ -145,17 +145,17 @@ extension VStack: Renderable {
}
return result
}
/// Aligns a buffer horizontally within the given width.
private func alignBuffer(_ buffer: FrameBuffer, toWidth width: Int, alignment: HorizontalAlignment) -> FrameBuffer {
guard buffer.width < width else { return buffer }
var alignedLines: [String] = []
for line in buffer.lines {
let lineWidth = line.strippedLength
let linePadding = width - lineWidth
switch alignment {
case .leading:
// Pad on right
@@ -170,7 +170,7 @@ extension VStack: Renderable {
alignedLines.append(String(repeating: " ", count: linePadding) + line)
}
}
return FrameBuffer(lines: alignedLines)
}
}
@@ -233,7 +233,7 @@ extension TupleView2: Renderable, ChildInfoProvider {
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.1, context: context)
]
}
}
@@ -247,7 +247,7 @@ extension TupleView3: Renderable, ChildInfoProvider {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.2, context: context)
]
}
}
@@ -262,7 +262,7 @@ extension TupleView4: Renderable, ChildInfoProvider {
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.3, context: context)
]
}
}
@@ -278,7 +278,7 @@ extension TupleView5: Renderable, ChildInfoProvider {
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.4, context: context)
]
}
}
@@ -295,7 +295,7 @@ extension TupleView6: Renderable, ChildInfoProvider {
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.5, context: context)
]
}
}
@@ -313,7 +313,7 @@ extension TupleView7: Renderable, ChildInfoProvider {
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.6, context: context)
]
}
}
@@ -332,7 +332,7 @@ extension TupleView8: Renderable, ChildInfoProvider {
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
makeChildInfo(for: value.7, context: context)
]
}
}
@@ -352,7 +352,7 @@ extension TupleView9: Renderable, ChildInfoProvider {
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
makeChildInfo(for: value.8, context: context),
makeChildInfo(for: value.8, context: context)
]
}
}
@@ -373,7 +373,7 @@ extension TupleView10: Renderable, ChildInfoProvider {
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
makeChildInfo(for: value.8, context: context),
makeChildInfo(for: value.9, context: context),
makeChildInfo(for: value.9, context: context)
]
}
}
+5 -5
View File
@@ -57,29 +57,29 @@ public struct ButtonStyle: Sendable {
// MARK: - Preset Styles
/// Default button style with border.
public static let `default` = ButtonStyle()
public static let `default` = Self()
/// Primary button style (cyan, bold).
public static let primary = ButtonStyle(
public static let primary = Self(
foregroundColor: .cyan,
borderColor: .cyan,
isBold: true
)
/// Destructive button style (red).
public static let destructive = ButtonStyle(
public static let destructive = Self(
foregroundColor: .red,
borderColor: .red
)
/// Success button style (green).
public static let success = ButtonStyle(
public static let success = Self(
foregroundColor: .green,
borderColor: .green
)
/// Plain button style (no border).
public static let plain = ButtonStyle(
public static let plain = Self(
borderStyle: BorderStyle.none,
horizontalPadding: 0
)
+2 -2
View File
@@ -45,10 +45,10 @@
public struct Card<Content: View, Footer: View>: View {
/// The card title (optional).
public let title: String?
/// The content of the card.
public let content: Content
/// The footer content (optional).
public let footer: Footer?
+37 -37
View File
@@ -61,7 +61,7 @@ public struct ContainerConfig: Sendable {
}
/// Default configuration.
public static let `default` = ContainerConfig()
public static let `default` = Self()
}
// MARK: - Container Style
@@ -75,16 +75,16 @@ public struct ContainerStyle: Sendable {
/// Note: Only applies to `Appearance.block`. For other appearances,
/// the title is rendered in the top border.
public var showHeaderSeparator: Bool
/// Whether to show a separator line between body and footer.
public var showFooterSeparator: Bool
/// The border style (nil uses appearance default).
public var borderStyle: BorderStyle?
/// The border color (nil uses theme default).
public var borderColor: Color?
/// Creates a container style with the specified options.
///
/// - Parameters:
@@ -115,7 +115,7 @@ public struct ContainerStyle: Sendable {
}
/// Default container style.
public static let `default` = ContainerStyle()
public static let `default` = Self()
}
// MARK: - Render Helper
@@ -208,22 +208,22 @@ internal func renderContainer<Content: View, Footer: View>(
public struct ContainerView<Content: View, Footer: View>: View {
/// The container title (rendered in border or header section).
public let title: String?
/// The title color.
public let titleColor: Color?
/// The main content.
public let content: Content
/// The footer content (typically buttons).
public let footer: Footer?
/// The container style configuration.
public let style: ContainerStyle
/// The inner padding for the body.
public let padding: EdgeInsets
/// Creates a container with all options.
///
/// - Parameters:
@@ -248,7 +248,7 @@ public struct ContainerView<Content: View, Footer: View>: View {
self.content = content()
self.footer = footer()
}
public var body: Never {
fatalError("ContainerView renders via Renderable")
}
@@ -289,11 +289,11 @@ extension ContainerView: Renderable {
let isBlockAppearance = appearance.rawId == .block
let effectiveBorderStyle = style.borderStyle ?? appearance.borderStyle
let borderColor = style.borderColor ?? Color.theme.border
// Render body content
let paddedContent = content.padding(padding)
let bodyBuffer = TUIKit.renderToBuffer(paddedContent, context: context)
// Render footer if present
let footerBuffer: FrameBuffer?
if let footerView = footer {
@@ -302,13 +302,13 @@ extension ContainerView: Renderable {
} else {
footerBuffer = nil
}
// Calculate inner width
let titleWidth = title.map { $0.count + 4 } ?? 0 // " Title " + borders
let bodyWidth = bodyBuffer.width
let footerWidth = footerBuffer?.width ?? 0
let innerWidth = max(titleWidth, bodyWidth, footerWidth)
if isBlockAppearance {
return renderBlockStyle(
bodyBuffer: bodyBuffer,
@@ -329,9 +329,9 @@ extension ContainerView: Renderable {
)
}
}
// MARK: - Standard Style Rendering
/// Renders with title in top border (line, rounded, doubleLine, heavy).
private func renderStandardStyle(
bodyBuffer: FrameBuffer,
@@ -342,7 +342,7 @@ extension ContainerView: Renderable {
context: RenderContext
) -> FrameBuffer {
var lines: [String] = []
// Top border (with title if present)
if let titleText = title {
lines.append(BorderRenderer.standardTopBorder(
@@ -354,7 +354,7 @@ extension ContainerView: Renderable {
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
}
// Body lines with theme background
let bodyBg = context.environment.palette.containerBackground
for line in bodyBuffer.lines {
@@ -363,7 +363,7 @@ extension ContainerView: Renderable {
color: borderColor, backgroundColor: bodyBg
))
}
// Footer section (if present)
if let footerBuf = footerBuffer, !footerBuf.isEmpty {
if style.showFooterSeparator {
@@ -371,7 +371,7 @@ extension ContainerView: Renderable {
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
}
// Footer lines (no background - footer has its own styling)
for line in footerBuf.lines {
lines.append(BorderRenderer.standardContentLine(
@@ -379,17 +379,17 @@ extension ContainerView: Renderable {
))
}
}
// Bottom border
lines.append(BorderRenderer.standardBottomBorder(
style: borderStyle, innerWidth: innerWidth, color: borderColor
))
return FrameBuffer(lines: lines)
}
// MARK: - Block Style Rendering
/// Renders with half-block characters for smooth visual edges.
///
/// Block style design:
@@ -412,42 +412,42 @@ extension ContainerView: Renderable {
context: RenderContext
) -> FrameBuffer {
var lines: [String] = []
// Get theme colors for block appearance
// Header/Footer = darker background
// Body = lighter background (containerBackground)
let headerFooterBg = Color.theme.containerHeaderBackground
let bodyBg = Color.theme.containerBackground
let hasHeader = title != nil
let hasFooter = footerBuffer != nil && !(footerBuffer?.isEmpty ?? true)
// === TOP BORDER ===
lines.append(BorderRenderer.blockTopBorder(
innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg
))
// === HEADER SECTION (if title present) ===
if let titleText = title {
let titleStyled = ANSIRenderer.colorize(" \(titleText) ", foreground: titleColor ?? Color.theme.accent, bold: true)
lines.append(BorderRenderer.blockContentLine(
content: titleStyled, innerWidth: innerWidth, sectionColor: headerFooterBg
))
if style.showHeaderSeparator {
lines.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
}
}
// === BODY LINES ===
for line in bodyBuffer.lines {
lines.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: bodyBg
))
}
// === FOOTER SECTION (if present) ===
if let footerBuf = footerBuffer, !footerBuf.isEmpty {
if style.showFooterSeparator {
@@ -456,19 +456,19 @@ extension ContainerView: Renderable {
foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
}
for line in footerBuf.lines {
lines.append(BorderRenderer.blockContentLine(
content: line, innerWidth: innerWidth, sectionColor: headerFooterBg
))
}
}
// === BOTTOM BORDER ===
lines.append(BorderRenderer.blockBottomBorder(
innerWidth: innerWidth, color: hasFooter ? headerFooterBg : bodyBg
))
return FrameBuffer(lines: lines)
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ public struct Dialog<Content: View, Footer: View>: View {
/// The dialog content.
public let content: Content
/// The footer content (typically buttons).
public let footer: Footer?
+12 -14
View File
@@ -180,12 +180,12 @@ extension Menu: Renderable {
}
var lines: [String] = []
// Calculate the content width for full-width selection bar
let contentWidth = maxItemWidth + 2 // +2 for padding
// Track the divider line index (for T-junction rendering)
var dividerLineIndex: Int? = nil
var dividerLineIndex: Int?
// Title if present
if let menuTitle = title {
@@ -204,10 +204,10 @@ extension Menu: Renderable {
// Menu items
let currentSelection = selectionBinding?.wrappedValue ?? selectedIndex
for (index, item) in items.enumerated() {
let isSelected = index == currentSelection
// Build the label with optional shortcut
let labelText: String
if let shortcut = item.shortcut {
@@ -218,7 +218,7 @@ extension Menu: Renderable {
// Build the full text with padding
let fullText = " " + labelText
// Pad to full width for selection bar
let visibleLength = fullText.count
let padding = max(0, contentWidth - visibleLength)
@@ -248,7 +248,7 @@ extension Menu: Renderable {
let appearance = context.environment.appearance
let effectiveBorderStyle = borderStyle ?? appearance.borderStyle
let isBlockStyle = appearance.rawId == .block
contentBuffer = applyBorder(
to: contentBuffer,
style: effectiveBorderStyle,
@@ -338,22 +338,22 @@ extension Menu: Renderable {
let innerWidth = buffer.width
var result: [String] = []
if isBlockStyle {
let headerFooterBg = Color.theme.containerHeaderBackground
let bodyBg = Color.theme.containerBackground
let hasHeader = dividerLineIndex != nil
// Top border
result.append(BorderRenderer.blockTopBorder(
innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg
))
// 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 {
result.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg
@@ -368,7 +368,7 @@ extension Menu: Renderable {
))
}
}
// Bottom border
result.append(BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: bodyBg))
} else {
@@ -419,5 +419,3 @@ extension AnyView: Renderable {
_render(context)
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ public struct Panel<Content: View, Footer: View>: View {
/// The content of the panel.
public let content: Content
/// The footer content (typically buttons).
public let footer: Footer?
+9 -9
View File
@@ -202,29 +202,29 @@ public struct Alignment: Sendable {
// MARK: - Preset Alignments
/// Top leading.
public static let topLeading = Alignment(horizontal: .leading, vertical: .top)
public static let topLeading = Self(horizontal: .leading, vertical: .top)
/// Top center.
public static let top = Alignment(horizontal: .center, vertical: .top)
public static let top = Self(horizontal: .center, vertical: .top)
/// Top trailing.
public static let topTrailing = Alignment(horizontal: .trailing, vertical: .top)
public static let topTrailing = Self(horizontal: .trailing, vertical: .top)
/// Center leading.
public static let leading = Alignment(horizontal: .leading, vertical: .center)
public static let leading = Self(horizontal: .leading, vertical: .center)
/// Center.
public static let center = Alignment(horizontal: .center, vertical: .center)
public static let center = Self(horizontal: .center, vertical: .center)
/// Center trailing.
public static let trailing = Alignment(horizontal: .trailing, vertical: .center)
public static let trailing = Self(horizontal: .trailing, vertical: .center)
/// Bottom leading.
public static let bottomLeading = Alignment(horizontal: .leading, vertical: .bottom)
public static let bottomLeading = Self(horizontal: .leading, vertical: .bottom)
/// Bottom center.
public static let bottom = Alignment(horizontal: .center, vertical: .bottom)
public static let bottom = Self(horizontal: .center, vertical: .bottom)
/// Bottom trailing.
public static let bottomTrailing = Alignment(horizontal: .trailing, vertical: .bottom)
public static let bottomTrailing = Self(horizontal: .trailing, vertical: .bottom)
}
+38 -39
View File
@@ -263,37 +263,37 @@ public enum Shortcut {
/// ```
public struct StatusBarItemOrder: Comparable, Sendable {
public let value: Int
public init(_ value: Int) {
self.value = value
}
public static func < (lhs: StatusBarItemOrder, rhs: StatusBarItemOrder) -> Bool {
public static func < (lhs: Self, rhs: Self) -> 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)
public static let `default` = Self(500)
/// Order for items that should appear early (leftmost user items).
public static let early = StatusBarItemOrder(100)
public static let early = Self(100)
/// Order for items that should appear late (rightmost user items, before system items).
public static let late = StatusBarItemOrder(800)
public static let late = Self(800)
// MARK: - System Item Orders (right side)
/// Order for the quit item (leftmost of system items).
/// Appears as: `[...user items] [q quit] [a appearance] [t theme]`
public static let quit = StatusBarItemOrder(900)
public static let quit = Self(900)
/// Order for the appearance item (middle system item).
public static let appearance = StatusBarItemOrder(910)
public static let appearance = Self(910)
/// Order for the theme item (rightmost).
public static let theme = StatusBarItemOrder(920)
public static let theme = Self(920)
}
// MARK: - Status Bar Item Protocol
@@ -316,7 +316,7 @@ public protocol StatusBarItemProtocol: Sendable {
///
/// 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.
@@ -332,7 +332,7 @@ public protocol StatusBarItemProtocol: Sendable {
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
@@ -488,7 +488,7 @@ public enum SystemStatusBarItem {
label: "quit",
order: .quit
)
/// The appearance item (`a appearance`).
///
/// Cycles through available appearances (border styles).
@@ -498,7 +498,7 @@ public enum SystemStatusBarItem {
label: "appearance",
order: .appearance
)
/// The theme item (`t theme`).
///
/// Cycles through available themes. Action must be set by the framework.
@@ -507,12 +507,12 @@ public enum SystemStatusBarItem {
label: "theme",
order: .theme
)
/// All system items in their default order.
public static var all: [StatusBarItem] {
[quit, appearance, theme]
}
/// Creates system items with custom actions.
///
/// - Parameters:
@@ -526,7 +526,7 @@ public enum SystemStatusBarItem {
onTheme: (@Sendable () -> Void)? = nil
) -> [StatusBarItem] {
var result: [StatusBarItem] = []
// Quit is always present
result.append(StatusBarItem(
shortcut: "q",
@@ -534,9 +534,9 @@ public enum SystemStatusBarItem {
order: .quit,
action: onQuit
))
// Appearance is present if action is provided
if let onAppearance = onAppearance {
if let onAppearance {
result.append(StatusBarItem(
shortcut: "a",
label: "appearance",
@@ -544,9 +544,9 @@ public enum SystemStatusBarItem {
action: onAppearance
))
}
// Theme is present if action is provided
if let onTheme = onTheme {
if let onTheme {
result.append(StatusBarItem(
shortcut: "t",
label: "theme",
@@ -554,7 +554,7 @@ public enum SystemStatusBarItem {
action: onTheme
))
}
return result
}
}
@@ -633,7 +633,7 @@ public struct StatusBarItemBuilder {
public struct StatusBar: View {
/// User items (left container).
public let userItems: [any StatusBarItemProtocol]
/// System items (right container).
public let systemItems: [any StatusBarItemProtocol]
@@ -673,7 +673,7 @@ public struct StatusBar: View {
self.highlightColor = highlightColor
self.labelColor = labelColor
}
/// Creates a status bar with all items combined (legacy compatibility).
///
/// - Parameters:
@@ -719,7 +719,7 @@ public struct StatusBar: View {
self.highlightColor = highlightColor
self.labelColor = labelColor
}
/// All items combined (sorted user items, then filtered system items).
///
/// User items are sorted by their `order` property.
@@ -731,7 +731,7 @@ public struct StatusBar: View {
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
@@ -748,14 +748,14 @@ 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()
}
@@ -912,7 +912,7 @@ extension StatusBar: Renderable {
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),
BorderRenderer.standardBottomBorder(style: border, innerWidth: innerWidth, color: borderColor)
])
}
@@ -923,10 +923,9 @@ extension StatusBar: Renderable {
return FrameBuffer(lines: [
BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: statusBarBg),
BorderRenderer.blockContentLine(content: content, innerWidth: innerWidth, sectionColor: statusBarBg),
BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: statusBarBg),
BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: statusBarBg)
])
}
}
// MARK: - Status Bar Height Helper
@@ -45,7 +45,7 @@ struct ContainersPage: View {
}
}
}
HStack(spacing: 2) {
// ContainerView with Header and Footer (best for block appearance)
DemoSection("ContainerView (Header + Footer)") {
@@ -57,7 +57,7 @@ struct ContainersPage: View {
Text("Footer: Press Enter to confirm").foregroundColor(.theme.foreground)
}
}
// Alignment examples - uses different text lengths to show alignment
DemoSection("Content Alignment") {
HStack(spacing: 1) {
@@ -99,7 +99,7 @@ struct ContainersPage: View {
}
}
}
DemoSection("Appearance & BorderStyle") {
Text("BorderStyle is determined by Appearance. Press 'a' to cycle.").foregroundColor(.theme.foregroundSecondary)
}
+23 -23
View File
@@ -12,15 +12,15 @@ import Testing
@Suite("Appearance Tests")
struct AppearanceTests {
// MARK: - Appearance ID Tests
@Test("Appearance ID can be created with rawValue")
func appearanceIdRawValue() {
let id = Appearance.ID(rawValue: "custom")
#expect(id.rawValue == "custom")
}
@Test("Predefined appearance IDs exist")
func predefinedAppearanceIds() {
#expect(Appearance.ID.line.rawValue == "line")
@@ -29,22 +29,22 @@ struct AppearanceTests {
#expect(Appearance.ID.heavy.rawValue == "heavy")
#expect(Appearance.ID.block.rawValue == "block")
}
@Test("Appearance IDs are hashable")
func appearanceIdHashable() {
let ids: Set<Appearance.ID> = [.line, .rounded, .doubleLine, .heavy, .block]
#expect(ids.count == 5)
}
// MARK: - Appearance Struct Tests
@Test("Appearance can be created with ID and borderStyle")
func appearanceCreation() {
let appearance = Appearance(id: .line, borderStyle: .line)
#expect(appearance.rawId == .line)
#expect(appearance.borderStyle == .line)
}
@Test("Appearance name is derived from ID")
func appearanceName() {
// .capitalized lowercases after first letter, so "doubleLine" becomes "Doubleline"
@@ -54,7 +54,7 @@ struct AppearanceTests {
#expect(Appearance.heavy.name == "Heavy")
#expect(Appearance.block.name == "Block")
}
@Test("Predefined appearances have correct border styles")
func predefinedAppearances() {
#expect(Appearance.line.borderStyle == .line)
@@ -63,22 +63,22 @@ struct AppearanceTests {
#expect(Appearance.heavy.borderStyle == .heavy)
#expect(Appearance.block.borderStyle == .block)
}
@Test("Default appearance is rounded")
func defaultAppearance() {
#expect(Appearance.default.rawId == .rounded)
#expect(Appearance.default.borderStyle == .rounded)
}
@Test("Appearances are equatable")
func appearanceEquatable() {
let appearance1 = Appearance.line
let appearance2 = Appearance(id: .line, borderStyle: .line)
#expect(appearance1 == appearance2)
}
// MARK: - Appearance Registry Tests
@Test("AppearanceRegistry contains all predefined appearances")
func registryContainsAll() {
let all = AppearanceRegistry.all
@@ -89,7 +89,7 @@ struct AppearanceTests {
#expect(all.contains { $0.rawId == .heavy })
#expect(all.contains { $0.rawId == .block })
}
@Test("AppearanceRegistry cycling order is correct")
func registryCyclingOrder() {
let all = AppearanceRegistry.all
@@ -100,7 +100,7 @@ struct AppearanceTests {
#expect(all[3].rawId == .heavy)
#expect(all[4].rawId == .block)
}
@Test("AppearanceRegistry can find appearance by ID")
func registryFindById() {
let found = AppearanceRegistry.appearance(withId: .heavy)
@@ -108,7 +108,7 @@ struct AppearanceTests {
#expect(found?.rawId == .heavy)
#expect(found?.borderStyle == .heavy)
}
@Test("AppearanceRegistry returns nil for unknown ID")
func registryUnknownId() {
let customId = Appearance.ID(rawValue: "unknown")
@@ -117,7 +117,7 @@ struct AppearanceTests {
}
// MARK: - Cyclable Conformance
@Test("Appearance conforms to Cyclable with string id")
func cyclableConformance() {
let appearance = Appearance.rounded
@@ -142,19 +142,19 @@ struct AppearanceManagerTests {
let manager = makeAppearanceManager()
#expect(manager.items.count == 5)
}
@Test("ThemeManager for appearances starts with first appearance (line)")
func managerStartsWithLine() {
let manager = makeAppearanceManager()
#expect(manager.currentAppearance?.rawId == .line)
}
@Test("ThemeManager for appearances currentName returns capitalized name")
func managerCurrentName() {
let manager = makeAppearanceManager()
#expect(manager.currentName == "Line")
}
@Test("ThemeManager for appearances can be created with custom items")
func managerCustomAppearances() {
let custom: [Appearance] = [.rounded, .heavy]
@@ -168,27 +168,27 @@ struct AppearanceManagerTests {
@Suite("Appearance Environment Tests")
struct AppearanceEnvironmentTests {
@Test("Appearance can be accessed via environment")
func environmentAccess() {
let env = EnvironmentValues()
#expect(env.appearance.rawId == .rounded) // Default
}
@Test("Appearance can be set via environment")
func environmentSet() {
var env = EnvironmentValues()
env.appearance = .heavy
#expect(env.appearance.rawId == .heavy)
}
@Test("AppearanceManager can be accessed via environment")
func managerEnvironmentAccess() {
let env = EnvironmentValues()
let manager = env.appearanceManager
#expect(manager.items.count == 5)
}
@Test("Custom AppearanceManager can be set via environment")
func managerEnvironmentSet() {
var env = EnvironmentValues()
+1 -1
View File
@@ -51,7 +51,7 @@ struct RenderingTests {
let buffer = renderToBuffer(stack, context: context)
#expect(buffer.height == 3)
#expect(buffer.lines[0] == "A")
#expect(buffer.lines[1] == "")
#expect(buffer.lines[1].isEmpty)
#expect(buffer.lines[2] == "B")
}
+19 -18
View File
@@ -1,3 +1,4 @@
// swiftlint:disable file_length
//
// StatusBarTests.swift
// TUIKit
@@ -250,7 +251,7 @@ struct StatusBarStateTests {
#expect(state.currentItems.count >= 1)
#expect(state.currentItems.contains { $0.shortcut == "q" })
}
@Test("StatusBarState without system items is empty")
func stateWithoutSystemItems() {
let state = StatusBarState()
@@ -508,7 +509,7 @@ struct StatusBarStateTests {
state.showSystemItems = false
#expect(state.height == 0)
}
@Test("Height is 1 when only system items")
func heightWithSystemItems() {
let state = StatusBarState()
@@ -1021,7 +1022,7 @@ struct StatusBarItemsModifierTests {
@Suite("System Status Bar Items Tests")
struct SystemStatusBarItemsTests {
@Test("System items are present by default")
func systemItemsPresentByDefault() {
let state = StatusBarState()
@@ -1029,84 +1030,84 @@ struct SystemStatusBarItemsTests {
#expect(state.currentSystemItems.count >= 1)
#expect(state.currentSystemItems.contains { $0.shortcut == "q" })
}
@Test("System items can be disabled")
func systemItemsCanBeDisabled() {
let state = StatusBarState()
state.showSystemItems = false
#expect(state.currentSystemItems.isEmpty)
}
@Test("System items appear on the right (high order values)")
func systemItemsAppearOnRight() {
let state = StatusBarState()
state.setItems([
StatusBarItem(shortcut: "s", label: "save")
])
// User items should come before system items (lower order)
let items = state.currentItems
let saveIndex = items.firstIndex { $0.shortcut == "s" }
let quitIndex = items.firstIndex { $0.shortcut == "q" }
#expect(saveIndex != nil)
#expect(quitIndex != nil)
#expect(saveIndex! < quitIndex!) // save appears before quit
}
@Test("User items can override system items with same shortcut")
func userItemsOverrideSystemItems() {
let state = StatusBarState()
// Set user item with same shortcut as system quit
state.setItems([
StatusBarItem(shortcut: "q", label: "custom-quit") {
// Custom action
}
])
// Should only have one "q" item, and it should be the user's
let qItems = state.currentItems.filter { $0.shortcut == "q" }
#expect(qItems.count == 1)
#expect(qItems[0].label == "custom-quit")
}
@Test("System item order constants are correct")
func systemItemOrderConstants() {
// System items should have high order values (900+)
#expect(StatusBarItemOrder.quit.value == 900)
#expect(StatusBarItemOrder.appearance.value == 910)
#expect(StatusBarItemOrder.theme.value == 920)
// User items should have lower order values
#expect(StatusBarItemOrder.default.value == 500)
#expect(StatusBarItemOrder.early.value == 100)
#expect(StatusBarItemOrder.late.value == 800)
// User items < system items
#expect(StatusBarItemOrder.late < StatusBarItemOrder.quit)
}
@Test("Items are sorted by order")
func itemsSortedByOrder() {
let state = StatusBarState()
// Add items in random order
state.setItems([
StatusBarItem(shortcut: "l", label: "late", order: .late),
StatusBarItem(shortcut: "e", label: "early", order: .early),
StatusBarItem(shortcut: "d", label: "default", order: .default)
])
let items = state.currentItems
// Should be sorted: early, default, late, quit (system)
let labels = items.map { $0.label }
let earlyIndex = labels.firstIndex(of: "early")!
let defaultIndex = labels.firstIndex(of: "default")!
let lateIndex = labels.firstIndex(of: "late")!
let quitIndex = labels.firstIndex(of: "quit")!
#expect(earlyIndex < defaultIndex)
#expect(defaultIndex < lateIndex)
#expect(lateIndex < quitIndex)