mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Merge pull request #8 from phranck/feature/swiftlint-integration
feat: Integrate SwiftLint as build plugin
This commit is contained in:
+107
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: "+",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -71,5 +71,3 @@ public struct ViewArray<Element: View>: View {
|
||||
fatalError("ViewArray renders its children directly")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user