渲染/tmux 修复(e2e 复现+回归,模拟器 idb 点击+截图取证): - 终端底部残影:ghostty iOS 释放 surface 不摘 IOSurfaceLayer(上游缺陷), platformSetup 清现存子层 + TerminalSurfaceCoordinator.onSurfaceFreed 钩子摘孤儿层 - tmux 切 tab 切回旧 tab 内容消失:改所有窗口常驻挂载(ZStack+opacity+hitTesting), surface 不释放、内容保留、后台窗口持续收 %output - tmux pane 内容钉底/顶部残行:pane grid 由容器几何定(tab 条吃高度),75 行快照喂 73 行 溢出上滚;几何测量上移 TmuxTabbedView + attach kickoff 双条件(attachAcked∧containerKnown) + capture-pane 铺快照前剔尾空行 UI 产品化(对标 Moshi getmoshi.app:深蓝黑 + 终端绿 + Catppuccin): - 设计系统 Theme.swift:TXPalette(色角色 struct)+TXThemeManager(环境注入),颜色封装供主题系统 - 统一主题:抽离 4 套 Catppuccin(Mocha/Macchiato/Frappé/Latte),一套同时驱动 app+终端配色, 设置里选主题 app 与终端一起变色 + UserDefaults 持久化 - 主屏/主机管理页 HomeView:HostCard 主机卡 + FAB 加主机(AddHostSheet) + 设置(主题选择器), HostStore 本地 JSON 持久化(密码暂存/TODO Keychain),点卡片 connect(to:) - 终端外壳 chrome TerminalTopBar:圆形返回钮 + 会话标题 + SSH/mosh 传输徽章 + 状态提示, tmux tab 绿 pill 化,底部工具栏深色自适应,终端默认 Catppuccin Mocha iPad mini(A17 Pro) live 端到端验证:加主机→连接→终端→切 tab→分屏点焦点→主题切换同步。 详见 docs/HANDOFF.md §10。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
5.9 KiB
Swift
154 lines
5.9 KiB
Swift
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
|
||
)
|
||
}
|
||
}
|