渲染/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>
59 lines
1.8 KiB
Swift
59 lines
1.8 KiB
Swift
import Foundation
|
||
|
||
/// 已保存的主机(连接目标)。
|
||
/// 注:`password` 暂存本地 JSON 仅供 demo;产品化后应移入 Keychain(未签名时 SecItem 不可用,见 HANDOFF)。
|
||
struct SavedHost: Identifiable, Codable, Equatable {
|
||
var id: UUID = UUID()
|
||
var name: String
|
||
var host: String
|
||
var port: Int = 22
|
||
var username: String
|
||
var password: String = ""
|
||
/// 登录后引导 mosh 承载会话。
|
||
var useMosh: Bool = false
|
||
|
||
var addressLine: String { "\(username)@\(host):\(port)" }
|
||
}
|
||
|
||
/// 主机列表持久化(Application Support 下 JSON)。凭据不入仓库;此文件只在设备本地。
|
||
@MainActor
|
||
final class HostStore: ObservableObject {
|
||
@Published private(set) var hosts: [SavedHost] = []
|
||
|
||
private let url: URL
|
||
|
||
init() {
|
||
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||
try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true)
|
||
url = base.appendingPathComponent("hosts.json")
|
||
load()
|
||
}
|
||
|
||
func add(_ host: SavedHost) {
|
||
hosts.append(host)
|
||
save()
|
||
}
|
||
|
||
func update(_ host: SavedHost) {
|
||
guard let i = hosts.firstIndex(where: { $0.id == host.id }) else { return }
|
||
hosts[i] = host
|
||
save()
|
||
}
|
||
|
||
func remove(_ host: SavedHost) {
|
||
hosts.removeAll { $0.id == host.id }
|
||
save()
|
||
}
|
||
|
||
private func load() {
|
||
guard let data = try? Data(contentsOf: url),
|
||
let decoded = try? JSONDecoder().decode([SavedHost].self, from: data) else { return }
|
||
hosts = decoded
|
||
}
|
||
|
||
private func save() {
|
||
guard let data = try? JSONEncoder().encode(hosts) else { return }
|
||
try? data.write(to: url, options: .atomic)
|
||
}
|
||
}
|