feat: 修复 3 个渲染/tmux 显示 bug + UI 产品化第一波(对标 Moshi)
渲染/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>
This commit is contained in:
153
apps/TerminalX/iOS/Theme.swift
Normal file
153
apps/TerminalX/iOS/Theme.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user