Files
TUIkit/Sources/TUIKit/Core/Theme.swift
T
phranck 982cfbc39e style: Add subtle theme tint to backgrounds
Each theme now has a darker background with a subtle color tint:
- Green: #0F1A0F (dark with green tint)
- Amber: #1A150F (dark with amber/orange tint)
- White: #121418 (dark with cool/blue tint)
- Red: #1A0F0F (dark with red tint)

This gives each theme a more distinctive character while keeping
the backgrounds dark and easy on the eyes.
2026-01-28 21:25:29 +01:00

656 lines
21 KiB
Swift

//
// Theme.swift
// TUIKit
//
// Theming system with full 16M color support and predefined terminal themes.
//
import Foundation
// MARK: - Theme Protocol
/// A theme defines the color palette for a TUIKit application.
///
/// Themes provide semantic colors that views use for consistent styling.
/// TUIKit includes several predefined themes inspired by classic terminals.
///
/// # Usage
///
/// ```swift
/// // Set the app theme
/// ThemeManager.shared.current = .amber
///
/// // Use theme colors in views
/// Text("Hello").foregroundColor(.theme.primary)
/// ```
public protocol Theme: Sendable {
/// The theme's unique identifier.
var id: String { get }
/// The theme's display name.
var name: String { get }
// MARK: - Background Colors
/// The primary background color.
var background: Color { get }
/// Secondary background for cards, panels, etc.
var backgroundSecondary: Color { get }
/// Tertiary background for nested elements.
var backgroundTertiary: Color { get }
// MARK: - Foreground Colors
/// Primary text/foreground color.
var foreground: Color { get }
/// Secondary text color (less prominent).
var foregroundSecondary: Color { get }
/// Tertiary text color (even less prominent).
var foregroundTertiary: Color { get }
// MARK: - Accent Colors
/// Primary accent color for interactive elements.
var accent: Color { get }
/// Secondary accent color.
var accentSecondary: Color { get }
// MARK: - Semantic Colors
/// Color for success states.
var success: Color { get }
/// Color for warning states.
var warning: Color { get }
/// Color for error states.
var error: Color { get }
/// Color for informational states.
var info: Color { get }
// MARK: - UI Element Colors
/// Border color for boxes, cards, etc.
var border: Color { get }
/// Border color for focused elements.
var borderFocused: Color { get }
/// Separator/divider color.
var separator: Color { get }
/// Selection highlight color.
var selection: Color { get }
/// Color for disabled elements.
var disabled: Color { get }
// MARK: - Status Bar Colors
/// Status bar background.
var statusBarBackground: Color { get }
/// Status bar text color.
var statusBarForeground: Color { get }
/// Status bar shortcut highlight color.
var statusBarHighlight: Color { get }
}
// MARK: - Default Theme Implementation
extension Theme {
// Default implementations using the primary colors
public var backgroundSecondary: Color { background }
public var backgroundTertiary: Color { background }
public var foregroundSecondary: Color { foreground }
public var foregroundTertiary: Color { foreground }
public var accentSecondary: Color { accent }
public var borderFocused: Color { accent }
public var separator: Color { border }
public var selection: Color { accent }
public var disabled: Color { foregroundTertiary }
public var statusBarBackground: Color { backgroundSecondary }
public var statusBarForeground: Color { foreground }
public var statusBarHighlight: Color { accent }
}
// MARK: - Theme Environment Key
/// Environment key for the current theme.
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = GreenPhosphorTheme()
}
extension EnvironmentValues {
/// The current theme.
///
/// Set a theme at the app level and it propagates to all child views:
///
/// ```swift
/// WindowGroup {
/// ContentView()
/// }
/// .environment(\.theme, GreenPhosphorTheme())
/// ```
///
/// Access the theme in views:
///
/// ```swift
/// struct MyView: View {
/// @Environment(\.theme) var theme
///
/// var body: some View {
/// Text("Hello")
/// .foregroundColor(theme.foreground)
/// }
/// }
/// ```
public var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// MARK: - Color Theme Extension
extension Color {
/// Access theme colors from the current environment.
///
/// These colors read from `EnvironmentStorage.shared` during rendering.
///
/// # Example
///
/// ```swift
/// Text("Hello").foregroundColor(.theme.foreground)
/// ```
public static var theme: ThemeColors.Type {
ThemeColors.self
}
}
/// Namespace for theme-aware colors.
///
/// These properties read the current theme from the environment storage
/// that is set during rendering.
public enum ThemeColors {
/// Gets the current theme from environment storage.
private static var current: Theme {
EnvironmentStorage.shared.environment.theme
}
/// Primary background color.
public static var background: Color { current.background }
/// Secondary background color.
public static var backgroundSecondary: Color { current.backgroundSecondary }
/// Tertiary background color.
public static var backgroundTertiary: Color { current.backgroundTertiary }
/// Primary foreground color.
public static var foreground: Color { current.foreground }
/// Secondary foreground color.
public static var foregroundSecondary: Color { current.foregroundSecondary }
/// Tertiary foreground color.
public static var foregroundTertiary: Color { current.foregroundTertiary }
/// Primary accent color.
public static var accent: Color { current.accent }
/// Secondary accent color.
public static var accentSecondary: Color { current.accentSecondary }
/// Success color.
public static var success: Color { current.success }
/// Warning color.
public static var warning: Color { current.warning }
/// Error color.
public static var error: Color { current.error }
/// Info color.
public static var info: Color { current.info }
/// Border color.
public static var border: Color { current.border }
/// Focused border color.
public static var borderFocused: Color { current.borderFocused }
/// Separator color.
public static var separator: Color { current.separator }
/// Selection color.
public static var selection: Color { current.selection }
/// Disabled color.
public static var disabled: Color { current.disabled }
/// Status bar background.
public static var statusBarBackground: Color { current.statusBarBackground }
/// Status bar foreground.
public static var statusBarForeground: Color { current.statusBarForeground }
/// Status bar highlight.
public static var statusBarHighlight: Color { current.statusBarHighlight }
}
// MARK: - Theme Modifier
extension View {
/// Sets the theme for this view and its descendants.
///
/// # Example
///
/// ```swift
/// ContentView()
/// .theme(GreenPhosphorTheme())
/// ```
///
/// - Parameter theme: The theme to apply.
/// - Returns: A view with the theme applied.
public func theme(_ theme: Theme) -> some View {
environment(\.theme, theme)
}
}
// MARK: - Predefined Themes
/// Classic green phosphor terminal theme (P1 phosphor).
///
/// Inspired by early CRT monitors like the IBM 5151 and Apple II.
/// Uses a dark background with subtle green tint.
public struct GreenPhosphorTheme: Theme {
public let id = "green-phosphor"
public let name = "Green"
// Dark background with subtle green tint
public let background = Color.hex(0x0F1A0F) // Very dark with green tint
public let backgroundSecondary = Color.hex(0x162016) // Slightly lighter
public let backgroundTertiary = Color.hex(0x0A120A) // Darkest
// Green phosphor text hierarchy
public let foreground = Color.hex(0x33FF33) // Bright green - primary text
public let foregroundSecondary = Color.hex(0x29CC29) // 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(0x1F4D1F) // Dark green for selection bg
// Status bar
public let statusBarBackground = Color.hex(0x162016)
public let statusBarForeground = Color.hex(0x33FF33)
public let statusBarHighlight = Color.hex(0x66FF66)
public init() {}
}
/// Classic amber phosphor terminal theme (P3 phosphor).
///
/// Inspired by terminals like the IBM 3278 and Wyse 50.
/// Uses a dark background with subtle amber/orange tint.
public struct AmberPhosphorTheme: Theme {
public let id = "amber-phosphor"
public let name = "Amber"
// Dark background with subtle amber tint
public let background = Color.hex(0x1A150F) // Very dark with amber tint
public let backgroundSecondary = Color.hex(0x201A14) // Slightly lighter
public let backgroundTertiary = Color.hex(0x120E0A) // Darkest
// 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(0x4D3A1F) // Dark amber for selection bg
// Status bar
public let statusBarBackground = Color.hex(0x201A14)
public let statusBarForeground = Color.hex(0xFFAA00)
public let statusBarHighlight = Color.hex(0xFFCC33)
public init() {}
}
/// Classic white phosphor terminal theme (P4 phosphor).
///
/// Inspired by terminals like the DEC VT100 and VT220.
/// Uses a dark background with subtle cool/blue tint.
public struct WhitePhosphorTheme: Theme {
public let id = "white-phosphor"
public let name = "White"
// Dark background with subtle cool/blue tint
public let background = Color.hex(0x121418) // Very dark with blue tint
public let backgroundSecondary = Color.hex(0x181C20) // Slightly lighter
public let backgroundTertiary = Color.hex(0x0C0E10) // Darkest
// 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(0x3A3A3A) // Dark gray for selection bg
// Status bar
public let statusBarBackground = Color.hex(0x181C20)
public let statusBarForeground = Color.hex(0xE8E8E8)
public let statusBarHighlight = Color.hex(0xFFFFFF)
public init() {}
}
/// Red phosphor terminal theme.
///
/// Less common but used in some military and specialized applications.
/// Night-vision friendly with reduced eye strain in dark environments.
public struct RedPhosphorTheme: Theme {
public let id = "red-phosphor"
public let name = "Red"
// Dark background with subtle red tint
public let background = Color.hex(0x1A0F0F) // Very dark with red tint
public let backgroundSecondary = Color.hex(0x201414) // Slightly lighter
public let backgroundTertiary = Color.hex(0x120A0A) // Darkest
// 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(0x4D1F1F) // Dark red for selection bg
// Status bar
public let statusBarBackground = Color.hex(0x201414)
public let statusBarForeground = Color.hex(0xFF4444)
public let statusBarHighlight = Color.hex(0xFF6666)
public init() {}
}
/// Classic ncurses-style theme.
///
/// Traditional terminal colors as used in ncurses applications
/// like htop, mc (Midnight Commander), and vim.
public struct NCursesTheme: Theme {
public let id = "ncurses"
public let name = "ncurses"
// Standard terminal black background
public let background = Color.black
public let backgroundSecondary = Color.blue
public let backgroundTertiary = Color.brightBlack
public let foreground = Color.white
public let foregroundSecondary = Color.brightWhite
public let foregroundTertiary = Color.brightBlack
public let accent = Color.cyan
public let accentSecondary = Color.brightCyan
public let success = Color.green
public let warning = Color.yellow
public let error = Color.red
public let info = Color.cyan
public let border = Color.white
public let borderFocused = Color.brightCyan
public let selection = Color.blue
public let disabled = Color.brightBlack
public let statusBarBackground = Color.blue
public let statusBarForeground = Color.white
public let statusBarHighlight = Color.yellow
public init() {}
}
// MARK: - Theme Registry
/// Registry of available themes.
public struct ThemeRegistry {
/// All available themes in cycling order.
///
/// Order: Green → Amber → White → Red → NCurses
public static let all: [Theme] = [
GreenPhosphorTheme(),
AmberPhosphorTheme(),
WhitePhosphorTheme(),
RedPhosphorTheme(),
NCursesTheme()
]
/// Finds a theme by ID.
public static func theme(withId id: String) -> Theme? {
all.first { $0.id == id }
}
/// Finds a theme by name.
public static func theme(withName name: String) -> Theme? {
all.first { $0.name == name }
}
}
// MARK: - Convenience Theme Accessors
extension Theme where Self == GreenPhosphorTheme {
/// The default theme (green phosphor).
public static var `default`: GreenPhosphorTheme { GreenPhosphorTheme() }
/// Green phosphor terminal theme.
public static var green: GreenPhosphorTheme { GreenPhosphorTheme() }
/// Green phosphor terminal theme (alias).
public static var greenPhosphor: GreenPhosphorTheme { GreenPhosphorTheme() }
}
extension Theme where Self == AmberPhosphorTheme {
/// Amber phosphor terminal theme.
public static var amber: AmberPhosphorTheme { AmberPhosphorTheme() }
/// Amber phosphor terminal theme (alias).
public static var amberPhosphor: AmberPhosphorTheme { AmberPhosphorTheme() }
}
extension Theme where Self == WhitePhosphorTheme {
/// White phosphor terminal theme.
public static var white: WhitePhosphorTheme { WhitePhosphorTheme() }
/// White phosphor terminal theme (alias).
public static var whitePhosphor: WhitePhosphorTheme { WhitePhosphorTheme() }
}
extension Theme where Self == RedPhosphorTheme {
/// Red phosphor terminal theme.
public static var red: RedPhosphorTheme { RedPhosphorTheme() }
/// Red phosphor terminal theme (alias).
public static var redPhosphor: RedPhosphorTheme { RedPhosphorTheme() }
}
extension Theme where Self == NCursesTheme {
/// Classic ncurses theme.
public static var ncurses: NCursesTheme { NCursesTheme() }
}
// MARK: - Theme Manager
/// Manages theme cycling for the application.
///
/// The `ThemeManager` provides methods to cycle through available themes
/// and set specific themes. It works with the environment system to update
/// the current theme and trigger re-renders.
///
/// # Usage
///
/// Access via environment:
///
/// ```swift
/// @Environment(\.themeManager) var themeManager
///
/// // Cycle to the next theme
/// themeManager.cycleTheme()
///
/// // Set a specific theme
/// themeManager.setTheme(.amber)
/// themeManager.setTheme(.greenPhosphor)
///
/// // Get the current theme
/// let theme = themeManager.currentTheme
/// ```
public final class ThemeManager: @unchecked Sendable {
/// The current theme index.
private var currentIndex: Int = 0
/// All available themes.
public let availableThemes: [Theme]
/// Creates a new theme manager with the default themes.
public init() {
self.availableThemes = ThemeRegistry.all
}
/// Creates a new theme manager with custom themes.
///
/// - Parameter themes: The themes to cycle through.
public init(themes: [Theme]) {
self.availableThemes = themes.isEmpty ? ThemeRegistry.all : themes
}
/// The current theme.
public var currentTheme: Theme {
availableThemes[currentIndex]
}
/// The name of the current theme.
public var currentThemeName: String {
currentTheme.name
}
/// Cycles to the next theme.
///
/// Updates the environment and triggers a re-render.
public func cycleTheme() {
currentIndex = (currentIndex + 1) % availableThemes.count
applyCurrentTheme()
}
/// Cycles to the previous theme.
///
/// Updates the environment and triggers a re-render.
public func cyclePreviousTheme() {
currentIndex = (currentIndex - 1 + availableThemes.count) % availableThemes.count
applyCurrentTheme()
}
/// Sets a specific theme.
///
/// - Parameter theme: The theme to set.
///
/// # Example
///
/// ```swift
/// themeManager.setTheme(.amber)
/// themeManager.setTheme(.greenPhosphor)
/// themeManager.setTheme(.dark)
/// ```
public func setTheme(_ theme: Theme) {
if let index = availableThemes.firstIndex(where: { $0.id == theme.id }) {
currentIndex = index
} else {
// Theme not in list, add temporarily at current position
currentIndex = 0
}
// Apply the theme directly (even if not in availableThemes)
var environment = EnvironmentStorage.shared.environment
environment.theme = theme
EnvironmentStorage.shared.environment = environment
AppState.shared.setNeedsRender()
}
/// Applies the current theme to the environment and triggers a re-render.
private func applyCurrentTheme() {
var environment = EnvironmentStorage.shared.environment
environment.theme = currentTheme
EnvironmentStorage.shared.environment = environment
AppState.shared.setNeedsRender()
}
}
// MARK: - ThemeManager Environment Key
/// Environment key for the theme manager.
private struct ThemeManagerKey: EnvironmentKey {
static let defaultValue: ThemeManager = ThemeManager()
}
extension EnvironmentValues {
/// The theme manager for cycling and setting themes.
///
/// ```swift
/// @Environment(\.themeManager) var themeManager
///
/// themeManager.cycleTheme()
/// themeManager.setTheme(.amber)
/// ```
public var themeManager: ThemeManager {
get { self[ThemeManagerKey.self] }
set { self[ThemeManagerKey.self] = newValue }
}
}