import SwiftUI import GhosttyTerminal import GhosttyTheme /// 统一主题:一套配色同时驱动 **app UI**(下面的颜色角色)和 **终端**(`ghosttyName` → GhosttyTheme)。 /// 在设置里选主题后 app 与终端一起变色。新增主题 = 再写一个 `static let`,视图零改动。 struct TXPalette: Equatable, Identifiable { let id: String let name: String let isDark: Bool /// 对应 GhosttyTheme 目录名,驱动终端配色(与 app 配色取自同一 Catppuccin flavor,视觉统一)。 let ghosttyName: String let bg: Color let surface: Color let surfaceElevated: Color let accent: Color let accentSoft: Color let textPrimary: Color let textSecondary: Color let textTertiary: Color let border: Color let borderStrong: Color let running: Color let mosh: Color let warning: Color let offline: Color /// 该主题的终端配色(GhosttyTheme → TerminalTheme)。找不到回退系统默认。 var terminalTheme: TerminalTheme { GhosttyThemeCatalog.theme(named: ghosttyName)?.toTerminalTheme() ?? .default } } extension TXPalette { /// 用一套 Catppuccin flavor 的核心色构造统一 palette(app 与终端同源)。 private static func catppuccin( id: String, name: String, ghostty: String, isDark: Bool, base: UInt32, surface0: UInt32, surface1: UInt32, text: UInt32, subtext0: UInt32, overlay0: UInt32, green: UInt32, teal: UInt32, yellow: UInt32 ) -> TXPalette { TXPalette( id: id, name: name, isDark: isDark, ghosttyName: ghostty, bg: Color(hex: base), surface: Color(hex: surface0), surfaceElevated: Color(hex: surface1), accent: Color(hex: green), accentSoft: Color(hex: green).opacity(0.16), textPrimary: Color(hex: text), textSecondary: Color(hex: subtext0), textTertiary: Color(hex: overlay0), border: (isDark ? Color.white : Color.black).opacity(0.08), borderStrong: (isDark ? Color.white : Color.black).opacity(0.14), running: Color(hex: green), mosh: Color(hex: teal), warning: Color(hex: yellow), offline: Color(hex: overlay0) ) } static let catppuccinMocha = catppuccin( id: "catppuccin-mocha", name: "Catppuccin Mocha", ghostty: "Catppuccin Mocha", isDark: true, base: 0x1E1E2E, surface0: 0x313244, surface1: 0x45475A, text: 0xCDD6F4, subtext0: 0xA6ADC8, overlay0: 0x6C7086, green: 0xA6E3A1, teal: 0x94E2D5, yellow: 0xF9E2AF) static let catppuccinMacchiato = catppuccin( id: "catppuccin-macchiato", name: "Catppuccin Macchiato", ghostty: "Catppuccin Macchiato", isDark: true, base: 0x24273A, surface0: 0x363A4F, surface1: 0x494D64, text: 0xCAD3F5, subtext0: 0xA5ADCB, overlay0: 0x6E738D, green: 0xA6DA95, teal: 0x8BD5CA, yellow: 0xEED49F) static let catppuccinFrappe = catppuccin( id: "catppuccin-frappe", name: "Catppuccin Frappé", ghostty: "Catppuccin Frappe", isDark: true, base: 0x303446, surface0: 0x414559, surface1: 0x51576D, text: 0xC6D0F5, subtext0: 0xA5ADCE, overlay0: 0x737994, green: 0xA6D189, teal: 0x81C8BE, yellow: 0xE5C890) static let catppuccinLatte = catppuccin( id: "catppuccin-latte", name: "Catppuccin Latte", ghostty: "Catppuccin Latte", isDark: false, base: 0xEFF1F5, surface0: 0xCCD0DA, surface1: 0xBCC0CC, text: 0x4C4F69, subtext0: 0x6C6F85, overlay0: 0x9CA0B0, green: 0x40A02B, teal: 0x179299, yellow: 0xDF8E1D) /// 可选主题(设置里列出)。默认第一个。 static let all: [TXPalette] = [catppuccinMocha, catppuccinMacchiato, catppuccinFrappe, catppuccinLatte] static let `default` = catppuccinMocha static func byID(_ id: String?) -> TXPalette { all.first { $0.id == id } ?? .default } } /// 当前主题的持有者(app + 终端统一)。选择持久化到 UserDefaults,终端侧经 `TerminalTheme.txDefault` 同源读取。 /// 主题选择的持久化键(文件级 nonisolated,供 app 与终端两侧读取)。 let txThemeStorageKey = "txThemeID" @MainActor final class TXThemeManager: ObservableObject { @Published var palette: TXPalette init() { palette = TXPalette.byID(UserDefaults.standard.string(forKey: txThemeStorageKey)) } /// 切换主题:持久化 + 更新(app 视图响应式;终端由 ContentView 观察 palette.id 变化后推送)。 func apply(_ palette: TXPalette) { self.palette = palette UserDefaults.standard.set(palette.id, forKey: txThemeStorageKey) } } extension TerminalTheme { /// 当前所选主题的终端配色(与 app 同源)。model / tmux pane 新建时读取,保证新面板也用当前主题。 static var txDefault: TerminalTheme { TXPalette.byID(UserDefaults.standard.string(forKey: txThemeStorageKey)).terminalTheme } } /// 结构性设计令牌(圆角/间距/字体)——不随主题变。 enum TX { enum Radius { static let card: CGFloat = 16 static let chip: CGFloat = 10 static let field: CGFloat = 12 } enum Space { static let xs: CGFloat = 6 static let s: CGFloat = 10 static let m: CGFloat = 16 static let l: CGFloat = 22 static let xl: CGFloat = 32 } enum Font { static func mono(_ size: CGFloat, _ weight: SwiftUI.Font.Weight = .regular) -> SwiftUI.Font { .system(size: size, weight: weight, design: .monospaced) } } } extension Color { /// 0xRRGGBB 十六进制构造。 init(hex: UInt32) { self.init( .sRGB, red: Double((hex >> 16) & 0xFF) / 255, green: Double((hex >> 8) & 0xFF) / 255, blue: Double(hex & 0xFF) / 255, opacity: 1 ) } }